/// <summary> /// Initializes a new instance of the <see cref="ContextMenuOpenedArgs"/> class. /// </summary> /// <param name="addon">The addon associated with the context menu.</param> /// <param name="agent">The agent associated with the context menu.</param> /// <param name="parentAddonName">The the name of the parent addon associated with the context menu.</param> /// <param name="items">The items in the context menu.</param> public ContextMenuOpenedArgs(AddonContextMenu *addon, AgentContextInterface *agent, string?parentAddonName, IEnumerable <ContextMenuItem> items) { this.Addon = addon; this.Agent = agent; this.ParentAddonName = parentAddonName; this.Items = new List <ContextMenuItem>(items); }
private unsafe bool ContextMenuItemSelectedDetour(AddonContextMenu *addonContextMenu, int selectedIndex, byte a3) { try { this.ContextMenuItemSelectedImplementation(addonContextMenu, selectedIndex); } catch (Exception ex) { PluginLog.Error(ex, "ContextMenuItemSelectedDetour"); } return(this.contextMenuItemSelectedHook.Original(addonContextMenu, selectedIndex, a3)); }
private unsafe bool SubContextMenuOpenedDetour(AddonContextMenu *addonContextMenu, int atkValueCount, AtkValue *atkValues) { try { this.SubContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues); } catch (Exception ex) { PluginLog.Error(ex, "SubContextMenuOpenedDetour"); } return(this.subContextMenuOpenedHook.Original(addonContextMenu, atkValueCount, atkValues)); }
private unsafe void ContextMenuItemSelectedImplementation(AddonContextMenu *addonContextMenu, int selectedIndex) { if (this.currentContextMenuOpenedArgs == null || selectedIndex == -1) { this.currentContextMenuOpenedArgs = null; this.selectedOpenSubContextMenuItem = null; return; } // Read the selected item directly from the game ContextMenuReaderWriter contextMenuReaderWriter = new ContextMenuReaderWriter(this.currentAgentContextInterface, addonContextMenu->AtkValuesCount, addonContextMenu->AtkValues); var gameContextMenuItems = contextMenuReaderWriter.Read(); var gameSelectedItem = gameContextMenuItems.ElementAtOrDefault(selectedIndex); // This should be impossible if (gameSelectedItem == null) { this.currentContextMenuOpenedArgs = null; this.selectedOpenSubContextMenuItem = null; return; } // Match it with the items we already know about based on its name. // We can get into a state where we have a game item we don't recognize when another plugin has added one. var selectedItem = this.currentContextMenuOpenedArgs.Items.FirstOrDefault(item => item.Name.Encode().SequenceEqual(gameSelectedItem.Name.Encode())); this.selectedOpenSubContextMenuItem = null; if (selectedItem is CustomContextMenuItem customContextMenuItem) { try { var customContextMenuItemSelectedArgs = new CustomContextMenuItemSelectedArgs(this.currentContextMenuOpenedArgs, customContextMenuItem); customContextMenuItem.ItemSelected(customContextMenuItemSelectedArgs); } catch (Exception ex) { PluginLog.LogError(ex, "ContextMenuItemSelectedImplementation"); } } else if (selectedItem is OpenSubContextMenuItem openSubContextMenuItem) { this.selectedOpenSubContextMenuItem = openSubContextMenuItem; } this.currentContextMenuOpenedArgs = null; }
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 SubContextMenuOpenedImplementation(AddonContextMenu *addonContextMenu, ref int atkValueCount, ref AtkValue *atkValues) { this.ContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues); }
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; } }