/// <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);
 }
Beispiel #2
0
        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));
        }
Beispiel #3
0
        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));
        }
Beispiel #4
0
        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;
        }
Beispiel #5
0
        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);
        }
Beispiel #6
0
 private unsafe void SubContextMenuOpenedImplementation(AddonContextMenu *addonContextMenu, ref int atkValueCount, ref AtkValue *atkValues)
 {
     this.ContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues);
 }
Beispiel #7
0
        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;
            }
        }