BL Coding Custom Escape Menu Button

正在查看此主题的用户

To assist with mod development, I want to add a button to the escape menu that can be used to call code within a C# function. So far I have added a DLL that includes a couple of functions, both of which are confirmed working by calling them in the OnCampaignStart event:
插入代码块:
        public void TestClick()
        {
            ShowMessage("Click", "#ffffffff");
        }

        /// <summary>
        /// Displays a message in a specified colour.
        /// Colour string format is "#RRGGBBAA". Alpha is required but not used.
        /// </summary>
        public void ShowMessage(string strText, string strColour)
        {
            TaleWorlds.Library.Color xColor = TaleWorlds.Library.Color.ConvertStringToColor(strColour);
            InformationMessage xMessage = new InformationMessage(strText, xColor);
            InformationManager.DisplayMessage(xMessage);
        }

I have also added a button to the escape menu that appears to work (it highlights on mouse over and makes a sound when clicked) by modifying the file
"MyMod\GUI\Prefabs\EscapeMenu.xml" with the following markup, added below the </ListPanel> tag in the original file.
插入代码块:
    <!-- Modified -->
    <EscapeMenuButtonWidget DoNotPassEventsToChildren="true" Command.Click="[HELP]" WidthSizePolicy="Fixed" 
      HeightSizePolicy="Fixed" SuggestedWidth="!ContextButton.Width" SuggestedHeight="!ContextButton.Height" HorizontalAlignment="Right"
      VerticalAlignment="Center" Brush="ButtonBrush2" PositiveBehaviourBrush="ButtonBrush1" IsDisabled="@IsDisabled"
      IsPositiveBehavioured="@IsPositiveBehavioured" MarginBottom="30">
          <Children>
                <TextWidget WidthSizePolicy="StretchToParent" HeightSizePolicy="StretchToParent" PositionYOffset="1" Text="Click Me"
                   Brush="OverlayPopup.ButtonText" ClipContents="false"/>
          </Children>
    </EscapeMenuButtonWidget>
    <!-- Modified -->

Where the button markup shows:
Command.Click="[HELP]"

I have tried:
Command.Click="TestClick"
Command.Click="Main.TestClick"
Command.Click="MyMod.Main.TestClick"

In my "MyMod.dll" file, the "MyMod" namespace contains the "Main" class wherein the "TestClick()" function is defined, so the attempts to call it above where just guesses. None worked so I looked back at the original markup for the "EscapeMenuButtonWidget" which I copied from within the same file.

The original markup uses Command.Click="ExecuteAction" which seems to refer to an action property of a list in the data model because the button is defined as a template for multiple buttons within a ListPanel. The use of the word "Action" led me to look into the System.Action delegate but I'm taking guesses here and have no real idea how to link my button to some code.

Does anyone have any idea how to make an escape menu button that calls C# code?
 
I'm just as clueless as you, so take this with a grain of salt. I believe Command.Click="ExecuteAction" tells the game to run the button's delegate action, but the delegate action has to be set in the code. I found various references to "escapeMenu" spread throughout module .dlls, but I *think* the delegates are set in Sandbox.View.Map.MapScreen under GetEscapeMenuItems(). I assume this applies only to the version of the escape menu that appears when you are in the world view (not in a battle or town scene). If you are looking for a different version of the escape menu then what you find there ought to give you an idea of what you need to be searching for.

Anyway, it sure looks to me like the delegates for the menu options are being set in that method, as well as the actual text on the buttons in the list. I think that if you add a new button to that list it will appear, and you should be able to call your function from there as well. My guess is that you don't need to touch the .xml at all.

You're way ahead of me on figuring out how to get your own .dll in a separate module, I've just been editing the existing ones. I'd test but I can't seem to get it to compile, even before I make a change it says I'm missing an assembly reference or using directive. I'm pretty new to this .dll editing thing.

I'm going to start working on getting my mod into a module. I'll test when I get there, but in the meantime if you can figure it out let me know.

Update: I can confirm that my speculation above is correct. I'm still battling with figuring out how to use Harmony to patch the method in a way that doesn't crash the game when you select any of the buttons, but I did manage to edit the number and text of the buttons in the escape menu.
 
最后编辑:
点赞 0
Thanks quailman, your response is insightful and helpful. I will investigate more over the weekend.

Regarding moving your mod to your own DLLs (the way we should all be doing it), I found this tutorial very helpful in getting started:

Although I did find the coding style hard to follow. Personally I try to keep lines short with few actions per line because it's easier for me to read what the code is doing step by step. This is especially helpful when returning to old code several months later. If you follow the tutorial above to get your module and Visual Studio 2019 Community Edition set up then you can copy this code into your main class and it may be clearer than the code used in the tutorial.
插入代码块:
        protected override void OnSubModuleLoad()
        {
            ExampleAddMainMenuItem();
        }

        /// <summary>
        /// Example of how to add an item to the main menu on game start.
        /// </summary>
        private static void ExampleAddMainMenuItem()
        {
            // This code must be run in the event:
            // protected override void OnSubModuleLoad()

            TextObject xTextObj = new TextObject("Click Me", null);
            Action xAction = new Action(ExampleMessage);
            InitialStateOption iso = new InitialStateOption("TestMainMenu", xTextObj, 9990, xAction, false);
            Module.CurrentModule.AddInitialStateOption(iso);
        }

        /// <summary>
        /// Example of how to display an onscreen message, in colour.
        /// </summary>
        private static void ExampleMessage()
        {
            //Colour string format is "#RRGGBBAA". Alpha is required but not used.
            TaleWorlds.Library.Color xColor = TaleWorlds.Library.Color.ConvertStringToColor("#FF0000FF");

            //Colour instantiation uses floats from 0 to 1. This is probably faster than string conversion.
            //TaleWorlds.Library.Color xColor = new TaleWorlds.Library.Color(0, 0, 1);

            InformationMessage xMessage = new InformationMessage("Ouch!", xColor);
            InformationManager.DisplayMessage(xMessage);
        }

I've also put together a template that includes all the module events with some helpful comments. These were dug out of the game DLLs so I have confirmed that all are intended to be overridden by modders (overriding will NOT conflict with game code or other module code). The template can be found here:


It also includes a version that displays a windows message box when events fire, to help understand what happens when.
 
点赞 0
That event template will definitely come in handy.

I actually just finished getting the module to patch in correctly using Harmony. At great difficulty I was even able to replicate the escape menu. There's a whole lot of reflection voodoo in there that I'm not sure I understand correctly, but it does work. A lot of Stack Overflow searches and trial and error went into this. If you want to add an item to the escape menu, all you should have to do is add another element to the list in the position you want it. It's not commented or clean, but here it is:
C#:
using HarmonyLib;
using System;
using SandBox.View.Map;
using TaleWorlds.CampaignSystem;
using TaleWorlds.Core;
using System.Collections.Generic;
using TaleWorlds.Localization;
using TaleWorlds.MountAndBlade.ViewModelCollection;
using System.Reflection;



namespace CCM.Patch
{
    
    [HarmonyPatch(typeof(MapScreen), "GetEscapeMenuItems")]
    public class EscapeMenuPatch

    {
        
        public static bool Prefix(MapScreen __instance, ref List<EscapeMenuItemVM> __result)
        {

            __result = new List<EscapeMenuItemVM>
            {

                new EscapeMenuItemVM(new TextObject("{=XzZFhRwr}Return To Game", null), delegate(object o)
                {
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                    privMethod.Invoke(__instance, new object[] { false });
                }, null, false, true),
                new EscapeMenuItemVM(new TextObject("{=PXT6aA4J}Campaign Options", null), delegate(object o)
                {
                    typeof(MapScreen).GetField("_campaignOptionsView", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(__instance, __instance.AddMapView<MapCampaignOptions>(Array.Empty<object>()));
                    __instance.IsInCampaignOptions = true;

                }, null, false, false),
                new EscapeMenuItemVM(new TextObject("{=NqarFr4P}Options", null), delegate(object o)
                {
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                    privMethod.Invoke(__instance, new object[] { false });
                    __instance.OpenOptions();
                }, null, false, false),
                new EscapeMenuItemVM(new TextObject("{=bV75iwKa}Save", null), delegate(object o)
                {
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                    privMethod.Invoke(__instance, new object[] { false });
                    Campaign.Current.SaveHandler.QuickSaveCurrentGame();
                }, null, false, false),
                new EscapeMenuItemVM(new TextObject("{=e0KdfaNe}Save As", null), delegate(object o)
                {
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                    privMethod.Invoke(__instance, new object[] { false });
                    __instance.OpenSaveLoad(true);
                }, null, false, false),
                new EscapeMenuItemVM(new TextObject("{=9NuttOBC}Load", null), delegate(object o)
                {
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                    privMethod.Invoke(__instance, new object[] { false });
                    __instance.OpenSaveLoad(false);
                }, null, false, false),
                new EscapeMenuItemVM(new TextObject("{=AbEh2y8o}Save And Exit", null), delegate(object o)
                {
                    Campaign.Current.SaveHandler.QuickSaveCurrentGame();
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                    privMethod.Invoke(__instance, new object[] { false });
                    InformationManager.HideInquiry();
                    __instance.OnExit();
                }, null, false, false),
                new EscapeMenuItemVM(new TextObject("{=RamV6yLM}Exit to Main Menu", null), delegate(object o)
                {
                    MethodInfo privMethod = __instance.GetType().GetMethod("OnExitToMainMenu", BindingFlags.NonPublic | BindingFlags.Instance);
                    Action cring = (Action) privMethod.CreateDelegate(typeof(Action), __instance);
                    InformationManager.ShowInquiry(new InquiryData(GameTexts.FindText("str_exit", null).ToString(), GameTexts.FindText("str_mission_exit_query", null).ToString(), true, true, GameTexts.FindText("str_yes", null).ToString(), GameTexts.FindText("str_no", null).ToString(), new Action(cring), delegate()
                    {
                        MethodInfo privMethod2 = __instance.GetType().GetMethod("OnEscapeMenuToggled", BindingFlags.NonPublic | BindingFlags.Instance);
                        privMethod2.Invoke(__instance, new object[] { false });
                    }, ""), false);
                }, null, false, false)
            };
            return false;

        }

    }
}

Some caveats: Adding an button will make the bottom button awkwardly hang off the bottom of the escape menu panel. Also, this method would conflict with any other module adding to the escape menu. I'm not sure if it helps you or not, but hopefully all of this information will be useful to somebody.
 
点赞 0
后退
顶部 底部