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)) { 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) = ResolvePath(gamePath, *categoryId, *resourceType, *resourceHash); PathResolved?.Invoke(gamePath, resolvedPath, data); if (resolvedPath == null) { var retUnmodified = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, 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; path = resolvedPath.Value.InternalName.Path; var retModified = CallOriginalHandler(isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk); ResourceLoaded?.Invoke((Structs.ResourceHandle *)retModified, gamePath, resolvedPath.Value, data); return(retModified); }
private static void CompareHash(int local, int game, Utf8GamePath path) { if (local != game) { PluginLog.Warning("Hash function appears to have changed. Computed {Hash1:X8} vs Game {Hash2:X8} for {Path}.", local, game, path); } }
private void OnResourceRequested(Utf8GamePath data, bool synchronous) { var path = Match(data.Path); if (path != null) { PluginLog.Information($"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}"); } }
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); }
// 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(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(); }
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)); } if (!Utf8GamePath.FromSpan(fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false) || 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 = 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); }
// 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); }
// 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, }; } } }
// 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)); }
// The modified resolver that handles game path resolving. private bool CharacterResolver(Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, object?) data)
// Force a file resolve to be removed. internal void RemoveFile(Utf8GamePath path) => _cache !.ResolvedFiles.Remove(path);
// 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);
public FullPath?ResolvePath(Utf8GamePath path) => _cache?.ResolvePath(path);
// Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver(Utf8GamePath path) { var resolved = Penumbra.CollectionManager.Default.ResolvePath(path); return(resolved, null); }
// 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);