mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'dt-shmod'
This commit is contained in:
commit
1648bfe424
14 changed files with 167 additions and 20 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit c53955cb6199dd418c5a9538d3251ac5942e7067
|
Subproject commit d9486ae54b5a4b61cf74f79ed27daa659eb1ce5b
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8ee82929fa6c725b8f556904ba022fb418991b5c
|
Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947
|
||||||
|
|
@ -6,11 +6,11 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
|
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
|
||||||
/// <item>Parameter is the associated game object. </item>
|
/// <item>Parameter is the associated game object. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.Priority>(nameof(MtrlShpkLoaded))
|
public sealed class MtrlLoaded() : EventWrapper<nint, nint, MtrlLoaded.Priority>(nameof(MtrlLoaded))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="Interop.Hooks.PostProcessing.ShaderReplacementFixer.OnMtrlShpkLoaded"/>
|
/// <seealso cref="Interop.Hooks.PostProcessing.ShaderReplacementFixer.OnMtrlLoaded"/>
|
||||||
ShaderReplacementFixer = 0,
|
ShaderReplacementFixer = 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ public class HookOverrides
|
||||||
public struct ResourceHooks
|
public struct ResourceHooks
|
||||||
{
|
{
|
||||||
public bool ApricotResourceLoad;
|
public bool ApricotResourceLoad;
|
||||||
public bool LoadMtrlShpk;
|
public bool LoadMtrl;
|
||||||
public bool LoadMtrlTex;
|
public bool LoadMtrlTex;
|
||||||
public bool ResolvePathHooks;
|
public bool ResolvePathHooks;
|
||||||
public bool ResourceHandleDestructor;
|
public bool ResourceHandleDestructor;
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
|
||||||
_modelRendererOnRenderMaterialHook = hooks.CreateHook<ModelRendererOnRenderMaterialDelegate>("ModelRenderer.OnRenderMaterial",
|
_modelRendererOnRenderMaterialHook = hooks.CreateHook<ModelRendererOnRenderMaterialDelegate>("ModelRenderer.OnRenderMaterial",
|
||||||
Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour,
|
Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour,
|
||||||
!HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result;
|
!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);
|
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
|
||||||
{
|
{
|
||||||
_modelRendererOnRenderMaterialHook.Dispose();
|
_modelRendererOnRenderMaterialHook.Dispose();
|
||||||
_humanOnRenderMaterialHook.Dispose();
|
_humanOnRenderMaterialHook.Dispose();
|
||||||
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
|
_communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded);
|
||||||
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
|
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
|
||||||
_hairMaskState.ClearMaterials();
|
_hairMaskState.ClearMaterials();
|
||||||
_characterOcclusionState.ClearMaterials();
|
_characterOcclusionState.ClearMaterials();
|
||||||
|
|
@ -147,7 +147,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
|
||||||
return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan);
|
return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
|
private void OnMtrlLoaded(nint mtrlResourceHandle, nint gameObject)
|
||||||
{
|
{
|
||||||
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
|
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
|
||||||
var shpk = mtrl->ShaderPackageResourceHandle;
|
var shpk = mtrl->ShaderPackageResourceHandle;
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,28 @@ using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks.Resources;
|
namespace Penumbra.Interop.Hooks.Resources;
|
||||||
|
|
||||||
public sealed unsafe class LoadMtrlShpk : FastHook<LoadMtrlShpk.Delegate>
|
public sealed unsafe class LoadMtrl : FastHook<LoadMtrl.Delegate>
|
||||||
{
|
{
|
||||||
private readonly GameState _gameState;
|
private readonly GameState _gameState;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
|
|
||||||
public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator)
|
public LoadMtrl(HookManager hooks, GameState gameState, CommunicatorService communicator)
|
||||||
{
|
{
|
||||||
_gameState = gameState;
|
_gameState = gameState;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
Task = hooks.CreateHook<Delegate>("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, !HookOverrides.Instance.Resources.LoadMtrlShpk);
|
Task = hooks.CreateHook<Delegate>("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 last = _gameState.MtrlData.Value;
|
||||||
var mtrlData = _gameState.LoadSubFileHelper((nint)handle);
|
var mtrlData = _gameState.LoadSubFileHelper((nint)handle);
|
||||||
_gameState.MtrlData.Value = mtrlData;
|
_gameState.MtrlData.Value = mtrlData;
|
||||||
var ret = Task.Result.Original(handle);
|
var ret = Task.Result.Original(handle, unk1, unk2);
|
||||||
_gameState.MtrlData.Value = last;
|
_gameState.MtrlData.Value = last;
|
||||||
_communicator.MtrlShpkLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject);
|
_communicator.MtrlLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ using Penumbra.GameData;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks.Resources;
|
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<LoadMtrlTex.Delegate>
|
public sealed unsafe class LoadMtrlTex : FastHook<LoadMtrlTex.Delegate>
|
||||||
{
|
{
|
||||||
private readonly GameState _gameState;
|
private readonly GameState _gameState;
|
||||||
|
|
|
||||||
88
Penumbra/Interop/Processing/ShpkPathPreProcessor.cs
Normal file
88
Penumbra/Interop/Processing/ShpkPathPreProcessor.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
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.Mods.Manager;
|
||||||
|
using Penumbra.Services;
|
||||||
|
using Penumbra.String;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Processing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path pre-processor for shader packages that reverts redirects to known invalid files, as bad ShPks can crash the game.
|
||||||
|
/// </summary>
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
messager.CleanTaggedMessages(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;
|
||||||
|
|
||||||
|
messager.PrintFileWarning(modManager, 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@ public class ResourceNode : ICloneable
|
||||||
public readonly nint ResourceHandle;
|
public readonly nint ResourceHandle;
|
||||||
public Utf8GamePath[] PossibleGamePaths;
|
public Utf8GamePath[] PossibleGamePaths;
|
||||||
public FullPath FullPath;
|
public FullPath FullPath;
|
||||||
|
public string? ModName;
|
||||||
|
public string? ModRelativePath;
|
||||||
public CiByteString AdditionalData;
|
public CiByteString AdditionalData;
|
||||||
public readonly ulong Length;
|
public readonly ulong Length;
|
||||||
public readonly List<ResourceNode> Children;
|
public readonly List<ResourceNode> Children;
|
||||||
|
|
@ -57,6 +59,8 @@ public class ResourceNode : ICloneable
|
||||||
ResourceHandle = other.ResourceHandle;
|
ResourceHandle = other.ResourceHandle;
|
||||||
PossibleGamePaths = other.PossibleGamePaths;
|
PossibleGamePaths = other.PossibleGamePaths;
|
||||||
FullPath = other.FullPath;
|
FullPath = other.FullPath;
|
||||||
|
ModName = other.ModName;
|
||||||
|
ModRelativePath = other.ModRelativePath;
|
||||||
AdditionalData = other.AdditionalData;
|
AdditionalData = other.AdditionalData;
|
||||||
Length = other.Length;
|
Length = other.Length;
|
||||||
Children = other.Children;
|
Children = other.Children;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Interop;
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Interop.ResourceTree;
|
namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
@ -21,7 +22,8 @@ public class ResourceTreeFactory(
|
||||||
ObjectIdentification objectIdentifier,
|
ObjectIdentification objectIdentifier,
|
||||||
Configuration config,
|
Configuration config,
|
||||||
ActorManager actors,
|
ActorManager actors,
|
||||||
PathState pathState) : IService
|
PathState pathState,
|
||||||
|
ModManager modManager) : IService
|
||||||
{
|
{
|
||||||
private TreeBuildCache CreateTreeBuildCache()
|
private TreeBuildCache CreateTreeBuildCache()
|
||||||
=> new(objects, gameData, actors);
|
=> 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:
|
// This is currently unneeded as we can resolve all paths by querying the draw object:
|
||||||
// ResolveGamePaths(tree, collectionResolveData.ModCollection);
|
// ResolveGamePaths(tree, collectionResolveData.ModCollection);
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
|
{
|
||||||
ResolveUiData(tree);
|
ResolveUiData(tree);
|
||||||
|
ResolveModData(tree);
|
||||||
|
}
|
||||||
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? config.ModDirectory : null);
|
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? config.ModDirectory : null);
|
||||||
Cleanup(tree);
|
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)
|
private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath)
|
||||||
{
|
{
|
||||||
foreach (var node in tree.FlatNodes)
|
foreach (var node in tree.FlatNodes)
|
||||||
|
|
|
||||||
|
|
@ -350,4 +350,22 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
Penumbra.Log.Error($"Could not scan for mods:\n{ex}");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ public class CommunicatorService : IDisposable, IService
|
||||||
/// <inheritdoc cref="Communication.CreatedCharacterBase"/>
|
/// <inheritdoc cref="Communication.CreatedCharacterBase"/>
|
||||||
public readonly CreatedCharacterBase CreatedCharacterBase = new();
|
public readonly CreatedCharacterBase CreatedCharacterBase = new();
|
||||||
|
|
||||||
/// <inheritdoc cref="Communication.MtrlShpkLoaded"/>
|
/// <inheritdoc cref="Communication.MtrlLoaded"/>
|
||||||
public readonly MtrlShpkLoaded MtrlShpkLoaded = new();
|
public readonly MtrlLoaded MtrlLoaded = new();
|
||||||
|
|
||||||
/// <inheritdoc cref="Communication.ModDataChanged"/>
|
/// <inheritdoc cref="Communication.ModDataChanged"/>
|
||||||
public readonly ModDataChanged ModDataChanged = new();
|
public readonly ModDataChanged ModDataChanged = new();
|
||||||
|
|
@ -87,7 +87,7 @@ public class CommunicatorService : IDisposable, IService
|
||||||
TemporaryGlobalModChange.Dispose();
|
TemporaryGlobalModChange.Dispose();
|
||||||
CreatingCharacterBase.Dispose();
|
CreatingCharacterBase.Dispose();
|
||||||
CreatedCharacterBase.Dispose();
|
CreatedCharacterBase.Dispose();
|
||||||
MtrlShpkLoaded.Dispose();
|
MtrlLoaded.Dispose();
|
||||||
ModDataChanged.Dispose();
|
ModDataChanged.Dispose();
|
||||||
ModOptionChanged.Dispose();
|
ModOptionChanged.Dispose();
|
||||||
ModDiscoveryStarted.Dispose();
|
ModDiscoveryStarted.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@ using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using Lumina.Excel.GeneratedSheets;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
using Notification = OtterGui.Classes.Notification;
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
|
|
@ -38,4 +42,16 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti
|
||||||
Message = payload,
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,10 @@ public class ResourceTreeViewer
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
if (resourceNode.FullPath.FullName.Length > 0)
|
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())
|
if (ImGui.IsItemClicked())
|
||||||
ImGui.SetClipboardText(resourceNode.FullPath.ToPath());
|
ImGui.SetClipboardText(resourceNode.FullPath.ToPath());
|
||||||
ImGuiUtil.HoverTooltip(
|
ImGuiUtil.HoverTooltip(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue