Skip to content

Commit 1648bfe

Browse files
committed
Merge branch 'dt-shmod'
2 parents df58ac7 + fe4a046 commit 1648bfe

File tree

14 files changed

+167
-20
lines changed

14 files changed

+167
-20
lines changed

Penumbra.GameData

Penumbra/Communication/MtrlShpkLoaded.cs renamed to Penumbra/Communication/MtrlLoaded.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ namespace Penumbra.Communication;
66
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
77
/// <item>Parameter is the associated game object. </item>
88
/// </list> </summary>
9-
public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.Priority>(nameof(MtrlShpkLoaded))
9+
public sealed class MtrlLoaded() : EventWrapper<nint, nint, MtrlLoaded.Priority>(nameof(MtrlLoaded))
1010
{
1111
public enum Priority
1212
{
13-
/// <seealso cref="Interop.Hooks.PostProcessing.ShaderReplacementFixer.OnMtrlShpkLoaded"/>
13+
/// <seealso cref="Interop.Hooks.PostProcessing.ShaderReplacementFixer.OnMtrlLoaded"/>
1414
ShaderReplacementFixer = 0,
1515
}
1616
}

Penumbra/Interop/Hooks/HookSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public struct ResourceLoadingHooks
9999
public struct ResourceHooks
100100
{
101101
public bool ApricotResourceLoad;
102-
public bool LoadMtrlShpk;
102+
public bool LoadMtrl;
103103
public bool LoadMtrlTex;
104104
public bool ResolvePathHooks;
105105
public bool ResourceHandleDestructor;

Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor,
110110
_modelRendererOnRenderMaterialHook = hooks.CreateHook<ModelRendererOnRenderMaterialDelegate>("ModelRenderer.OnRenderMaterial",
111111
Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour,
112112
!HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result;
113-
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer);
113+
_communicator.MtrlLoaded.Subscribe(OnMtrlLoaded, MtrlLoaded.Priority.ShaderReplacementFixer);
114114
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer);
115115
}
116116

117117
public void Dispose()
118118
{
119119
_modelRendererOnRenderMaterialHook.Dispose();
120120
_humanOnRenderMaterialHook.Dispose();
121-
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
121+
_communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded);
122122
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
123123
_hairMaskState.ClearMaterials();
124124
_characterOcclusionState.ClearMaterials();
@@ -147,7 +147,7 @@ private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, Rea
147147
return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan);
148148
}
149149

150-
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
150+
private void OnMtrlLoaded(nint mtrlResourceHandle, nint gameObject)
151151
{
152152
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
153153
var shpk = mtrl->ShaderPackageResourceHandle;

Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs renamed to Penumbra/Interop/Hooks/Resources/LoadMtrl.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@
55

66
namespace Penumbra.Interop.Hooks.Resources;
77

8-
public sealed unsafe class LoadMtrlShpk : FastHook<LoadMtrlShpk.Delegate>
8+
public sealed unsafe class LoadMtrl : FastHook<LoadMtrl.Delegate>
99
{
1010
private readonly GameState _gameState;
1111
private readonly CommunicatorService _communicator;
1212

13-
public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator)
13+
public LoadMtrl(HookManager hooks, GameState gameState, CommunicatorService communicator)
1414
{
1515
_gameState = gameState;
1616
_communicator = communicator;
17-
Task = hooks.CreateHook<Delegate>("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, !HookOverrides.Instance.Resources.LoadMtrlShpk);
17+
Task = hooks.CreateHook<Delegate>("Load Material", Sigs.LoadMtrl, Detour, !HookOverrides.Instance.Resources.LoadMtrl);
1818
}
1919

20-
public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle);
20+
public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle, void* unk1, byte unk2);
2121

22-
private byte Detour(MaterialResourceHandle* handle)
22+
private byte Detour(MaterialResourceHandle* handle, void* unk1, byte unk2)
2323
{
2424
var last = _gameState.MtrlData.Value;
2525
var mtrlData = _gameState.LoadSubFileHelper((nint)handle);
2626
_gameState.MtrlData.Value = mtrlData;
27-
var ret = Task.Result.Original(handle);
27+
var ret = Task.Result.Original(handle, unk1, unk2);
2828
_gameState.MtrlData.Value = last;
29-
_communicator.MtrlShpkLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject);
29+
_communicator.MtrlLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject);
3030
return ret;
3131
}
3232
}

Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Penumbra.Interop.Hooks.Resources;
66

7+
// TODO check if this is still needed, as our hooked function is called by LoadMtrl's hooked function
78
public sealed unsafe class LoadMtrlTex : FastHook<LoadMtrlTex.Delegate>
89
{
910
private readonly GameState _gameState;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using FFXIVClientStructs.FFXIV.Client.System.Resource;
2+
using Penumbra.Api.Enums;
3+
using Penumbra.Collections;
4+
using Penumbra.GameData.Files;
5+
using Penumbra.GameData.Files.Utility;
6+
using Penumbra.Interop.Hooks.ResourceLoading;
7+
using Penumbra.Mods.Manager;
8+
using Penumbra.Services;
9+
using Penumbra.String;
10+
using Penumbra.String.Classes;
11+
12+
namespace Penumbra.Interop.Processing;
13+
14+
/// <summary>
15+
/// Path pre-processor for shader packages that reverts redirects to known invalid files, as bad ShPks can crash the game.
16+
/// </summary>
17+
public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, MessageService messager, ModManager modManager)
18+
: IPathPreProcessor
19+
{
20+
public ResourceType Type
21+
=> ResourceType.Shpk;
22+
23+
public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault,
24+
FullPath? resolved)
25+
{
26+
messager.CleanTaggedMessages(false);
27+
28+
if (!resolved.HasValue)
29+
return null;
30+
31+
// 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.
32+
var resolvedPath = resolved.Value;
33+
if (!resolvedPath.IsRooted)
34+
return resolvedPath;
35+
36+
// If the ShPk is already loaded, it means that it already passed the sanity check.
37+
var existingResource =
38+
resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32));
39+
if (existingResource != null)
40+
return resolvedPath;
41+
42+
var checkResult = SanityCheck(resolvedPath.FullName);
43+
if (checkResult == SanityCheckResult.Success)
44+
return resolvedPath;
45+
46+
messager.PrintFileWarning(modManager, resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult));
47+
48+
return null;
49+
}
50+
51+
private static SanityCheckResult SanityCheck(string path)
52+
{
53+
try
54+
{
55+
using var file = MmioMemoryManager.CreateFromFile(path);
56+
var bytes = file.GetSpan();
57+
58+
return ShpkFile.FastIsLegacy(bytes)
59+
? SanityCheckResult.Legacy
60+
: SanityCheckResult.Success;
61+
}
62+
catch (FileNotFoundException)
63+
{
64+
return SanityCheckResult.NotFound;
65+
}
66+
catch (IOException)
67+
{
68+
return SanityCheckResult.IoError;
69+
}
70+
}
71+
72+
private static string WarningMessageComplement(SanityCheckResult result)
73+
=> result switch
74+
{
75+
SanityCheckResult.IoError => "Cannot read the modded file.",
76+
SanityCheckResult.NotFound => "The modded file does not exist.",
77+
SanityCheckResult.Legacy => "This mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.",
78+
_ => string.Empty,
79+
};
80+
81+
private enum SanityCheckResult
82+
{
83+
Success,
84+
IoError,
85+
NotFound,
86+
Legacy,
87+
}
88+
}

Penumbra/Interop/ResourceTree/ResourceNode.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class ResourceNode : ICloneable
1515
public readonly nint ResourceHandle;
1616
public Utf8GamePath[] PossibleGamePaths;
1717
public FullPath FullPath;
18+
public string? ModName;
19+
public string? ModRelativePath;
1820
public CiByteString AdditionalData;
1921
public readonly ulong Length;
2022
public readonly List<ResourceNode> Children;
@@ -57,6 +59,8 @@ private ResourceNode(ResourceNode other)
5759
ResourceHandle = other.ResourceHandle;
5860
PossibleGamePaths = other.PossibleGamePaths;
5961
FullPath = other.FullPath;
62+
ModName = other.ModName;
63+
ModRelativePath = other.ModRelativePath;
6064
AdditionalData = other.AdditionalData;
6165
Length = other.Length;
6266
Children = other.Children;

Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Penumbra.GameData.Interop;
1010
using Penumbra.Interop.PathResolving;
1111
using Penumbra.Meta;
12+
using Penumbra.Mods.Manager;
1213
using Penumbra.String.Classes;
1314

1415
namespace Penumbra.Interop.ResourceTree;
@@ -21,7 +22,8 @@ public class ResourceTreeFactory(
2122
ObjectIdentification objectIdentifier,
2223
Configuration config,
2324
ActorManager actors,
24-
PathState pathState) : IService
25+
PathState pathState,
26+
ModManager modManager) : IService
2527
{
2628
private TreeBuildCache CreateTreeBuildCache()
2729
=> new(objects, gameData, actors);
@@ -93,7 +95,10 @@ public IEnumerable<ICharacter> GetLocalPlayerRelatedCharacters()
9395
// This is currently unneeded as we can resolve all paths by querying the draw object:
9496
// ResolveGamePaths(tree, collectionResolveData.ModCollection);
9597
if (globalContext.WithUiData)
98+
{
9699
ResolveUiData(tree);
100+
ResolveModData(tree);
101+
}
97102
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? config.ModDirectory : null);
98103
Cleanup(tree);
99104

@@ -123,6 +128,18 @@ private static void ResolveUiData(ResourceTree tree)
123128
});
124129
}
125130

131+
private void ResolveModData(ResourceTree tree)
132+
{
133+
foreach (var node in tree.FlatNodes)
134+
{
135+
if (node.FullPath.IsRooted && modManager.TryIdentifyPath(node.FullPath.FullName, out var mod, out var relativePath))
136+
{
137+
node.ModName = mod.Name;
138+
node.ModRelativePath = relativePath;
139+
}
140+
}
141+
}
142+
126143
private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath)
127144
{
128145
foreach (var node in tree.FlatNodes)

Penumbra/Mods/Manager/ModManager.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,22 @@ private void ScanMods()
350350
Penumbra.Log.Error($"Could not scan for mods:\n{ex}");
351351
}
352352
}
353+
354+
public bool TryIdentifyPath(string path, [NotNullWhen(true)] out Mod? mod, [NotNullWhen(true)] out string? relativePath)
355+
{
356+
var relPath = Path.GetRelativePath(BasePath.FullName, path);
357+
if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath)))
358+
{
359+
mod = null;
360+
relativePath = null;
361+
return false;
362+
}
363+
364+
var modDirectorySeparator = relPath.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]);
365+
366+
var modDirectory = modDirectorySeparator < 0 ? relPath : relPath[..modDirectorySeparator];
367+
relativePath = modDirectorySeparator < 0 ? string.Empty : relPath[(modDirectorySeparator + 1)..];
368+
369+
return TryGetMod(modDirectory, "\0", out mod);
370+
}
353371
}

Penumbra/Services/CommunicatorService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public CommunicatorService(Logger logger)
2424
/// <inheritdoc cref="Communication.CreatedCharacterBase"/>
2525
public readonly CreatedCharacterBase CreatedCharacterBase = new();
2626

27-
/// <inheritdoc cref="Communication.MtrlShpkLoaded"/>
28-
public readonly MtrlShpkLoaded MtrlShpkLoaded = new();
27+
/// <inheritdoc cref="Communication.MtrlLoaded"/>
28+
public readonly MtrlLoaded MtrlLoaded = new();
2929

3030
/// <inheritdoc cref="Communication.ModDataChanged"/>
3131
public readonly ModDataChanged ModDataChanged = new();
@@ -87,7 +87,7 @@ public void Dispose()
8787
TemporaryGlobalModChange.Dispose();
8888
CreatingCharacterBase.Dispose();
8989
CreatedCharacterBase.Dispose();
90-
MtrlShpkLoaded.Dispose();
90+
MtrlLoaded.Dispose();
9191
ModDataChanged.Dispose();
9292
ModOptionChanged.Dispose();
9393
ModDiscoveryStarted.Dispose();

Penumbra/Services/MessageService.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
using Dalamud.Game.Text.SeStringHandling;
33
using Dalamud.Game.Text.SeStringHandling.Payloads;
44
using Dalamud.Interface;
5+
using Dalamud.Interface.ImGuiNotification;
56
using Dalamud.Plugin.Services;
67
using Lumina.Excel.GeneratedSheets;
78
using OtterGui.Log;
89
using OtterGui.Services;
10+
using Penumbra.Mods.Manager;
11+
using Penumbra.String.Classes;
12+
using Notification = OtterGui.Classes.Notification;
913

1014
namespace Penumbra.Services;
1115

@@ -38,4 +42,16 @@ public void LinkItem(Item item)
3842
Message = payload,
3943
});
4044
}
45+
46+
public void PrintFileWarning(ModManager modManager, string fullPath, Utf8GamePath originalGamePath, string messageComplement)
47+
{
48+
// Don't warn for files managed by other plugins, or files we aren't sure about.
49+
if (!modManager.TryIdentifyPath(fullPath, out var mod, out _))
50+
return;
51+
52+
AddTaggedMessage($"{fullPath}.{messageComplement}",
53+
new Notification(
54+
$"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}",
55+
NotificationType.Warning, 10000));
56+
}
4157
}

Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,10 @@ string GetAdditionalDataSuffix(CiByteString data)
316316
ImGui.TableNextColumn();
317317
if (resourceNode.FullPath.FullName.Length > 0)
318318
{
319-
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
319+
var uiFullPathStr = resourceNode.ModName != null && resourceNode.ModRelativePath != null
320+
? $"[{resourceNode.ModName}] {resourceNode.ModRelativePath}"
321+
: resourceNode.FullPath.ToPath();
322+
ImGui.Selectable(uiFullPathStr, false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
320323
if (ImGui.IsItemClicked())
321324
ImGui.SetClipboardText(resourceNode.FullPath.ToPath());
322325
ImGuiUtil.HoverTooltip(

0 commit comments

Comments
 (0)