Generic Mod Config Menu/Reference

From The Stardew Modding Wiki
Jump to navigation Jump to search

This article will (hopefully) provide a guide to how to implement GMCM Support in your mod. This article is basically a more in-depth explanation to the snippets provided in GitHub Gist.

Preparations[edit | edit source | hide | hide all]

First of all, you are expected to already understand how to create a SMAPI mod. So, we will not repeat the step-by-step process here.

After you've set up the basics for your mod (installing the NuGet package, preparing a manifest.json, and so on), you must add the ModConfigMenuAPI "Interface" to your project. The easiest way is as follows:

Step 1: Add a "New Item" to the root of your project, of type "Interface" (the icon looks like a small circle connected to a big circle). Change the name to ModConfigMenuAPI, and click "Add". Some people prefer to call it IModConfigMenu, and that's okay; if you change the name, you must use the new name when you invoke Helper.ModRegistry.GetApi later.


Step 2: To the list of using statements, add the following:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI;
using StardewModdingAPI.Utilities;


Step 3: Change the interface definition to the following (note the change from interface to public interface)

public interface GenericModConfigMenuAPI
{
    void RegisterModConfig(IManifest mod, Action revertToDefault, Action saveToFile);
    void UnregisterModConfig(IManifest mod);
    void SetDefaultIngameOptinValue(IManifest mod, bool optedIn);
    void StartNewPage(IManifest mod, string pageName);
    void OverridePageDisplayName(IManifest mod, string pageName, string displayName);
    void RegisterLabel(IManifest mod, string labelName, string labelDesc);
    void RegisterPageLabel(IManifest mod, string labelName, string labelDesc, string newPage);
    void RegisterParagraph(IManifest mod, string paragraph);
    void RegisterImage(IManifest mod, string texPath, Rectangle? texRect = null, int scale = 4);
    void RegisterSimpleOption(IManifest mod, string optionName, string optionDesc,
                              Func<bool> optionGet, Action<bool> optionSet);
    void RegisterSimpleOption(IManifest mod, string optionName, string optionDesc,
                              Func<int> optionGet, Action<int> optionSet);
    void RegisterSimpleOption(IManifest mod, string optionName, string optionDesc,
                              Func<float> optionGet, Action<float> optionSet);
    void RegisterSimpleOption(IManifest mod, string optionName, string optionDesc,
                              Func<string> optionGet, Action<string> optionSet);
    void RegisterSimpleOption(IManifest mod, string optionName, string optionDesc,
                              Func<SButton> optionGet, Action<SButton> optionSet);
    void RegisterSimpleOption(IManifest mod, string optionName, string optionDesc,
                              Func<KeybindList> optionGet, Action<KeybindList> optionSet);
    void RegisterClampedOption(IManifest mod, string optionName, string optionDesc,
                               Func<int> optionGet, Action<int> optionSet,
                               int min, int max);
    void RegisterClampedOption(IManifest mod, string optionName, string optionDesc,
                               Func<float> optionGet, Action<float> optionSet,
                               float min, float max);
    void RegisterClampedOption(IManifest mod, string optionName, string optionDesc,
                               Func<int> optionGet, Action<int> optionSet,
                               int min, int max, int interval);
    void RegisterClampedOption(IManifest mod, string optionName, string optionDesc,
                               Func<float> optionGet, Action<float> optionSet,
                               float min, float max, float interval);
    void RegisterChoiceOption(IManifest mod, string optionName, string optionDesc,
                              Func<string> optionGet, Action<string> optionSet,
                              string[] choices);
    void RegisterComplexOption(IManifest mod, string optionName, string optionDesc,
                               Func<Vector2, object, object> widgetUpdate,
                               Func<SpriteBatch, Vector2, object, object> widgetDraw,
                               Action<object> onSave);
    void SubscribeToChange(IManifest mod, Action<string, bool> changeHandler);
    void SubscribeToChange(IManifest mod, Action<string, int> changeHandler);
    void SubscribeToChange(IManifest mod, Action<string, float> changeHandler);
    void SubscribeToChange(IManifest mod, Action<string, string> changeHandler);
    void OpenModMenu(IManifest mod);
}


Step 4: Save the file. You're now prepared.

 


Registering the Config Menu[edit | edit source | hide]

Again basing this explanation from the aforementioned Gist, you must first add an event handler for the GameLaunched event. Let's call it OnGameLaunched, like so:

private void OnGameLaunched(object sender, GameLaunchedEventArgs args)
{
    var api = Helper.ModRegistry.GetApi<GenericModConfigMenuAPI>("spacechase0.GenericModConfigMenu");
    if (api == null)
    {
        Monitor.Log("GenericModConfigMenu not installed, skipping menu registry.");
        return;
    }
    Config = Helper.ReadConfig<ModConfig>();
    api.RegisterModConfig(ModManifest, () => Config = new ModConfig(), () => Helper.WriteConfig<ModConfig>(Config));
}

Note: The sample code above assumes that (1) you're creating the event handler inside the ModEntry class, and (2) your ModEntry class keeps the mod's configuration in a variable named Config.

If you're creating the event handler in a different class, you'll need to somehow get the Helper, Monitor, and ModManifest properties from the ModEntry class. There are many ways to do so, one way to do it is to provide those properties as initialization arguments to the class constructor, as can be seen here.

At this point, try (Re)Building your solution. If (re)building fails, fix the errors first, before continuing with the next section.


Custom Config Writer[edit | edit source | hide]

The third argument to api.RegisterModConfig can also be a parameterless void function. You might want to do this if your mod needs to take some actions when config changes in-game. In such case, you'll rewrite that line like this (adjust function names as you see fit):

            // .. other GMCM prep code here
            api.RegisterModConfig(ModManifest, () => config = new ModConfig(), CommitConfig);
            // .. the rest of Config Menu building


        private void CommitConfig()
        {
            Helper.WriteConfig<ModConfig>(Config);
            MyMod.DoThingsBecauseConfigHasChanged();
        }

You can see an example of that here.

 


Splitting your Config[edit | edit source | hide]

Now comes an important step: Which config options do you deem to be editable in-game, and which are not?

In-game editable options means that during gameplay, player can edit it by going into the Mod Options menu. Non-in-game editable options can only be changed through the Mod Options menu in the Title Screen.

Once you've determined which option is which, you'll do a call to the api.SetDefaultIngameOptinValue like so:

api.SetDefaultIngameOptinValue(ModManifest, bool optedIn);
  • If optedIn is true, then options after this statement will be changeable in-game.
  • Vice versa, if optedIn is false, then options after this statement will NOT be changeable in-game.

This command affects all options in the following statements, until the function is called again with a different optedIn value.


Building your Menu[edit | edit source | hide]

Giving a Heading[edit | edit source | hide]

A 'heading' is text formatted to be a bit bigger than 'normal' text, usually used to separate between sections of a menu. If your config menu is short, it might not be necessary to split them into sections. This is just a guideline, though. You're free to build your menu the way you want, just don't go overboard.

To add a 'heading', call the following:

api.RegisterLabel(ModManifest, string headingText, string headingDescription);
  • headingText -- The section heading that will appear in the Config Menu.
  • headingDescription -- A description that will pop-up when you hover over the heading.


Adding plain text[edit | edit source | hide]

Plain text, or "paragraph text", is a text with a smaller, non-boldfaced font compared to the Section Heading. Usually used to provide a more detailed description regarding the options on-screen.

api.RegisterParagraph(ModManifest, string paragraphText);


Simple Options[edit | edit source | hide]

Simple options simply checks the type of the attached configuration, and emits a configuration knob that's suitable for the configuration. So, it will show a checkbox for bool configuration, a textbox for int/float/string configuration, and a special clickable text for SButton/KeybindList configuration.

api.RegisterSimpleOption(
        ModManifest,
        string labelText,
        string hoverText,
        Func<T> getConfigFunc,   // a function with no input, but outputs value of type T
        Action<T> setConfigFunc  // a function with input of type T, but no outputs (void)
        );
  • labelText -- The label that will be drawn to the left of the option
  • hoverText -- A description that will pop-up when you hover over the label
  • getConfigFunc -- A function that will be called to get the configuration for this option. Can be an anonymous function; see below
  • setConfigFunc -- A function that will be invoked with an argument to set the configuration for this option. Can be an anonymous function; see below

For getConfigFunc and setConfigFunc, you usually don't need to write a separate function; just use an anonymous function, like so:

api.RegisterSimpleOption(
    ModManifest,
    "frob value",
    "Set frob to impact baz",
    () => Config.Frob,
    (int val) => Config.Frob = val
    );

One thing to make sure is that the type of the parameter of setConfigFunc must match with the type of the configuration value retrieved by getConfigFunc. If not, you will see a compiler error.


Clamped Options[edit | edit source | hide]

Applicable only for configuration options with type int or float, 'Clamped Options' uses a slider instead of a textbox; that is why it's called "clamped".

api.RegisterClampedOption(ModManifest, labelText, hoverText, getConfigFunc, setConfigFunc, min, max);
api.RegisterClampedOption(ModManifest, labelText, hoverText, getConfigFunc, setConfigFunc, min, max, interval);
  • min -- the minimum allowable value (left end of the slider)
  • max -- the maximum allowable value (right end of the slider)
  • interval -- if provided, the slider will "jump" around according to the interval. For example, setting min = 0; max = 100; interval = 10 means the user can only set the configuration to multiples of 10, and cannot move it to, say 15.

These three arguments must have the same type as the configuration value retrieved by getConfigFunc.


Choice Options[edit | edit source | hide]

This is basically a drop-down box, in which player can only select between several provided values. This is only applicable for string config options.

api.RegisterChoiceOption(ModManifest, labelText, hoverText, getConfigFunc, setConfigFunc, choices);
  • choices -- an array of string (that is, a string[] type) containing the allowable choices


Complex Options[edit | edit source | hide]

TBD


Configuration Pages[edit | edit source | hide]

Although GMCM will automatically create a scrollbar for you if the options within the menu extends too much vertically, you can also split your configuration into several pages.


Creating Pages[edit | edit source | hide]

TBD


Linking Pages[edit | edit source | hide]

TBD


Get Notified of Changes[edit | edit source | hide]

TBD -- SubscribeToChange