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); } }
// 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); } } }
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; } }
public static MultiModGroup?Load(JObject json, DirectoryInfo basePath) { 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, }; if (ret.Name.Length == 0) { return(null); } if (options != null) { 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)); } } return(ret); }
private void 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."); } 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."); } ImportV1ModPack(modPackFile, extractedModPack, modRaw); } }
/// <summary> /// Initializes a new instance of the <see cref="ContextMenuReaderWriter"/> class. /// </summary> /// <param name="agentContextInterface">The AgentContextInterface to act upon.</param> /// <param name="atkValueCount">The number of ATK values to consider.</param> /// <param name="atkValues">Pointer to the array of ATK values.</param> public ContextMenuReaderWriter(AgentContextInterface *agentContextInterface, int atkValueCount, AtkValue *atkValues) { PluginLog.Warning($"{(IntPtr)atkValues:X}"); this.agentContextInterface = agentContextInterface; this.atkValueCount = atkValueCount; this.atkValues = atkValues; }
// 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) { PluginLog.Warning(e.Message); } } 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); }
// 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)) { PluginLog.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); newCollection.Save(); PluginLog.Debug("Added collection {Name:l}.", newCollection.Name); CollectionChanged.Invoke(Type.Inactive, null, newCollection); SetCollection(newCollection.Index, Type.Current); return(true); }
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 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.InvariantCultureIgnoreCase))) { if (message) { PluginLog.Warning($"Could not name option {newName} because option with same filename {path} already exists."); } return(false); } return(true); }
public SkipCutscene() { if (Interface.GetPluginConfig() is not Config configuration || configuration.Version == 0) { configuration = new Config { IsEnabled = true, Version = 1 } } ; _config = configuration; Address = new CutsceneAddressResolver(); Address.Setup(SigScanner); if (Address.Offset1 != IntPtr.Zero && Address.Offset2 != IntPtr.Zero) { PluginLog.Information("Cutscene Offset Found."); if (_config.IsEnabled) { SetEnabled(true); } } else { PluginLog.Error("Cutscene Offset Not Found."); PluginLog.Warning("Plugin Disabling..."); Dispose(); return; } _csp = new RNGCryptoServiceProvider(); CommandManager.AddHandler("/sc", new CommandInfo(OnCommand) { HelpMessage = "/sc: Roll your sanity check dice." }); }
// 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(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); } }
public IReadOnlyDictionary <string, object?> GetChangedItemsForCollection(string collectionName) { CheckInitialized(); try { 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)); } 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; } }
/// <summary> /// Initialises an object's fields and properties that are annotated with a /// <see cref="SignatureAttribute"/>. /// </summary> /// <param name="self">The object to initialise.</param> /// <param name="log">If warnings should be logged using <see cref="PluginLog"/>.</param> public static void Initialise(object self, bool log = true) { var scanner = Service <SigScanner> .Get(); var selfType = self.GetType(); var fields = selfType.GetFields(Flags).Select(field => (IFieldOrPropertyInfo) new FieldInfoWrapper(field)) .Concat(selfType.GetProperties(Flags).Select(prop => new PropertyInfoWrapper(prop))) .Select(field => (field, field.GetCustomAttribute <SignatureAttribute>())) .Where(field => field.Item2 != null); foreach (var(info, sig) in fields) { var wasWrapped = false; var actualType = info.ActualType; if (actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Nullable <>)) { // unwrap the nullable actualType = actualType.GetGenericArguments()[0]; wasWrapped = true; } var fallibility = sig !.Fallibility; if (fallibility == Fallibility.Auto) { fallibility = info.IsNullable || wasWrapped ? Fallibility.Fallible : Fallibility.Infallible; } var fallible = fallibility == Fallibility.Fallible; void Invalid(string message, bool prepend = true) { var errorMsg = prepend ? $"Invalid Signature attribute for {selfType.FullName}.{info.Name}: {message}" : message; if (fallible) { PluginLog.Warning(errorMsg); } else { throw new SignatureException(errorMsg); } } IntPtr ptr; var success = sig.ScanType == ScanType.Text ? scanner.TryScanText(sig.Signature, out ptr) : scanner.TryGetStaticAddressFromSig(sig.Signature, out ptr); if (!success) { if (log) { Invalid($"Failed to find {sig.ScanType} signature \"{info.Name}\" for {selfType.FullName} ({sig.Signature})", false); } continue; } switch (sig.UseFlags) { case SignatureUseFlags.Auto when actualType == typeof(IntPtr) || actualType.IsPointer || actualType.IsAssignableTo(typeof(Delegate)): case SignatureUseFlags.Pointer: { if (actualType.IsAssignableTo(typeof(Delegate))) { info.SetValue(self, Marshal.GetDelegateForFunctionPointer(ptr, actualType)); } else { info.SetValue(self, ptr); } break; } case SignatureUseFlags.Auto when actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Hook <>): case SignatureUseFlags.Hook: { if (!actualType.IsGenericType || actualType.GetGenericTypeDefinition() != typeof(Hook <>)) { Invalid($"{actualType.Name} is not a Hook<T>"); continue; } var hookDelegateType = actualType.GenericTypeArguments[0]; Delegate?detour; if (sig.DetourName == null) { var matches = selfType.GetMethods(Flags) .Select(method => method.IsStatic ? Delegate.CreateDelegate(hookDelegateType, method, false) : Delegate.CreateDelegate(hookDelegateType, self, method, false)) .Where(del => del != null) .ToArray(); if (matches.Length != 1) { Invalid("Either found no matching detours or found more than one: specify a detour name"); continue; } detour = matches[0] !; } else { var method = selfType.GetMethod(sig.DetourName, Flags); if (method == null) { Invalid($"Could not find detour \"{sig.DetourName}\""); continue; } var del = method.IsStatic ? Delegate.CreateDelegate(hookDelegateType, method, false) : Delegate.CreateDelegate(hookDelegateType, self, method, false); if (del == null) { Invalid($"Method {sig.DetourName} was not compatible with delegate {hookDelegateType.Name}"); continue; } detour = del; } var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType }); if (ctor == null) { PluginLog.Error("Error in SignatureHelper: could not find Hook constructor"); continue; } var hook = ctor.Invoke(new object?[] { ptr, detour }); info.SetValue(self, hook); break; } case SignatureUseFlags.Auto when actualType.IsPrimitive: case SignatureUseFlags.Offset: { var offset = Marshal.PtrToStructure(ptr + sig.Offset, actualType); info.SetValue(self, offset); break; } default: { if (log) { Invalid("could not detect desired signature use, set SignatureUseFlags manually"); } break; } } } }
private unsafe ContextMenuOpenedArgs?NotifyContextMenuOpened(AddonContextMenu *addonContextMenu, AgentContextInterface *agentContextInterface, string?title, ContextMenus.ContextMenuOpenedDelegate contextMenuOpenedDelegate, IEnumerable <ContextMenuItem> initialContextMenuItems) { var parentAddonName = this.GetParentAddonName(&addonContextMenu->AtkUnitBase); Log.Warning($"AgentContextInterface at: {new IntPtr(agentContextInterface):X}"); InventoryItemContext?inventoryItemContext = null; GameObjectContext? gameObjectContext = null; if (IsInventoryContext(agentContextInterface)) { var agentInventoryContext = (AgentInventoryContext *)agentContextInterface; inventoryItemContext = new InventoryItemContext(agentInventoryContext->InventoryItemId, agentInventoryContext->InventoryItemCount, agentInventoryContext->InventoryItemIsHighQuality); } else { var agentContext = (AgentContext *)agentContextInterface; uint?id = agentContext->GameObjectId; if (id == 0) { id = null; } ulong?contentId = agentContext->GameObjectContentId; if (contentId == 0) { contentId = null; } var name = MemoryHelper.ReadSeStringNullTerminated((IntPtr)agentContext->GameObjectName.StringPtr).TextValue; if (string.IsNullOrEmpty(name)) { name = null; } ushort?worldId = agentContext->GameObjectWorldId; if (worldId == 0) { worldId = null; } if (id != null || contentId != null || name != null || worldId != null) { gameObjectContext = new GameObjectContext(id, contentId, name, worldId); } } // Temporarily remove the < Return item, for UX we should enforce that it is always last in the list. var lastContextMenuItem = initialContextMenuItems.LastOrDefault(); if (lastContextMenuItem is GameContextMenuItem gameContextMenuItem && gameContextMenuItem.SelectedAction == 102) { initialContextMenuItems = initialContextMenuItems.SkipLast(1); } var contextMenuOpenedArgs = new ContextMenuOpenedArgs(addonContextMenu, agentContextInterface, parentAddonName, initialContextMenuItems) { Title = title, InventoryItemContext = inventoryItemContext, GameObjectContext = gameObjectContext, }; try { contextMenuOpenedDelegate.Invoke(contextMenuOpenedArgs); } catch (Exception ex) { PluginLog.LogError(ex, "NotifyContextMenuOpened"); return(null); } // Readd the < Return item if (lastContextMenuItem is GameContextMenuItem gameContextMenuItem1 && gameContextMenuItem1.SelectedAction == 102) { contextMenuOpenedArgs.Items.Add(lastContextMenuItem); } foreach (var contextMenuItem in contextMenuOpenedArgs.Items.ToArray()) { // TODO: Game doesn't support nested sub context menus, but we might be able to. if (contextMenuItem is OpenSubContextMenuItem && contextMenuOpenedArgs.Title != null) { contextMenuOpenedArgs.Items.Remove(contextMenuItem); PluginLog.Warning($"Context menu '{contextMenuOpenedArgs.Title}' item '{contextMenuItem}' has been removed because nested sub context menus are not supported."); } } if (contextMenuOpenedArgs.Items.Count > MaxContextMenuItemsPerContextMenu) { PluginLog.LogWarning($"Context menu requesting {contextMenuOpenedArgs.Items.Count} of max {MaxContextMenuItemsPerContextMenu} items. Resizing list to compensate."); contextMenuOpenedArgs.Items.RemoveRange(MaxContextMenuItemsPerContextMenu, contextMenuOpenedArgs.Items.Count - MaxContextMenuItemsPerContextMenu); } return(contextMenuOpenedArgs); }
private unsafe void ContextMenuOpenedImplementation(AddonContextMenu *addonContextMenu, ref int atkValueCount, ref AtkValue *atkValues) { if (this.ContextMenuOpened == null || this.currentAgentContextInterface == null) { return; } var contextMenuReaderWriter = new ContextMenuReaderWriter(this.currentAgentContextInterface, atkValueCount, atkValues); // Check for a title. string?title = null; if (this.selectedOpenSubContextMenuItem != null) { title = this.selectedOpenSubContextMenuItem.Name.TextValue; // Write the custom title var titleAtkValue = &atkValues[1]; fixed(byte *titlePtr = this.selectedOpenSubContextMenuItem.Name.Encode().NullTerminate()) { titleAtkValue->SetString(titlePtr); } } else if (contextMenuReaderWriter.Title != null) { title = contextMenuReaderWriter.Title.TextValue; } // Determine which event to raise. var contextMenuOpenedDelegate = this.ContextMenuOpened; // this.selectedOpenSubContextMenuItem is OpenSubContextMenuItem openSubContextMenuItem if (this.selectedOpenSubContextMenuItem != null) { contextMenuOpenedDelegate = this.selectedOpenSubContextMenuItem.Opened; } // Get the existing items from the game. // TODO: For inventory sub context menus, we take only the last item -- the return item. // This is because we're doing a hack to spawn a Second Tier sub context menu and then appropriating it. var contextMenuItems = contextMenuReaderWriter.Read(); if (IsInventoryContext(this.currentAgentContextInterface) && this.selectedOpenSubContextMenuItem != null) { contextMenuItems = contextMenuItems.TakeLast(1).ToArray(); } var beforeHashCode = GetContextMenuItemsHashCode(contextMenuItems); // Raise the event and get the context menu changes. this.currentContextMenuOpenedArgs = this.NotifyContextMenuOpened(addonContextMenu, this.currentAgentContextInterface, title, contextMenuOpenedDelegate, contextMenuItems); if (this.currentContextMenuOpenedArgs == null) { return; } var afterHashCode = GetContextMenuItemsHashCode(this.currentContextMenuOpenedArgs.Items); PluginLog.Warning($"{beforeHashCode}={afterHashCode}"); // Only write to memory if the items were actually changed. if (beforeHashCode != afterHashCode) { // Write the new changes. contextMenuReaderWriter.Write(this.currentContextMenuOpenedArgs.Items); // Update the addon. atkValueCount = *(&addonContextMenu->AtkValuesCount) = (ushort)contextMenuReaderWriter.AtkValueCount; atkValues = *(&addonContextMenu->AtkValues) = contextMenuReaderWriter.AtkValues; } }
private static bool MigrateV0ToV1(Mod 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; var seenMetaFiles = new HashSet <FullPath>(); 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)) { PluginLog.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(group, index) in mod.Groups.WithIndex()) { IModGroup.Save(group, mod.ModPath, index); } // 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.ModPath.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); }