ConfigMachine

From Rain World Modding
Jump to navigation Jump to search

Legacy article
This article contains information that is only relevant for legacy versions of the game (1.5 or lower)

Configmachine is a dependency mod that allows other mods to easily create in-game config GUI. It's currently shipped with BepInEx installation.

For Remix config screen, see MixedOI.

Features

OptionInterface

This is the most common method of adding config screen to your mod.

Registering

OptionalUI.OptionInterface is the base class for option interfaces.

public class MyOI : OptionInterface
{
    public MyOI() : base (plugin: MyPlugin.instance) // Your BaseUnityPlugin instance. Examples assume you have a static singleton instance of your mod.
    { }
}

Now add LoadOI static method in your BaseUnityPlugin class that returns your OptionInterface.

public static OptionInterface LoadOI() => new MyOI();
//set return type to "object" if you want your mod to be loaded even if CM is not. In that case, you will need to handle CM's absence on your own.

Designing GUI

For designing your GUI, using Inkscape is strongly suggested. With Inkscape, create a file with 600 x 600 pixel resolution, then enable [View] - [Canvas Orientation] - [Flip Vertically]. (Alternatively, you can use any CAD program which has up and right as positive axis by default)


Colorfoot's GUI as an example
Colorfoot's GUI as an example

All UIelements have bottom-left position and rectangular size. (Some have fixedSize which you can see from the summary of their constructors). Think every element like rectangles, and arrange them as you need. (Circular UIelements have rad instead; You can consider them as a square with 2 * rad for its dimensions)

Example GUI Positioning
Example GUI Positioning

For pos parameters, use the relative coordinate from bottom-left from the canvas to your item. For size parameters, use their size in pixel. If you’re using Inkscape with [Flip Vertically] setting on, you can get those from their properties without any calculation.

Example GUI Inkscape
Example GUI in Inkscape

Here are some tips for designing GUI:

  • If you have many things to display, organise them with categories and use OpRect to surround them.
  • You don’t have to put everything in a single OpTab. If the canvas gets crowded, disperse your items for easier readability.
  • Try to avoid using smaller OpScrollBoxs to store many things in one place even if that’s an option: The users cannot see everything in a single screen which reduces usability greatly.

Initialization

OptionInterface.Initialize is called in IntroRoll(for loading configuration) and ConfigMenu (for displaying to users). You can check whether Initialize is called in ConfigMenu or not with OptionInterface.isOptionMenu. It usually goes like this following example:

public override void Initialize()
{
    base.Initialize(); // This should be called before everything else
    Tabs = new OpTab[2]; // you can have up to 20 tabs
    Tabs[0] = new OpTab("Main"); // Each OpTab is 600 x 600 pixel sized canvas
    Tabs[1] = new OpTab("Second")
    { color = Color.cyan }; // You can change its button and canvas colour too
    // After Initializing Tabs, create UIelements and add them to tabs
}

Now initialize UIelements and add them to OpTab (or OpScrollBox). The Z-order (the order in which they’re drawn) of UIelements is the order of constructors, not the order they’re added to OpTab.

    Tabs[0].AddItems(new OpLabel(100f, 500f, "My Text")
    { description = "This is My Text" });
    // Any UIelement can have description, which will be shown
    // at the bottom of the screen when the mouse cursor is over it

UIconfigs are a subcategory of UIelement which have key and value for storing user input values, both of them get saved automatically by Config Machine. Their constructor accepts key and defaultValue on top of everything else. key must be unique for all elements in your OI unless they are cosmetic.

Setting the key to empty or anything that starts with _ (underbar) will turn it cosmetic, and the value won’t be saved. Cosmetic UIconfig is useful when you want to have provided UIconfig for UI purpose but not for an actual configurable. (Similarly, ModID that starts with _ will be completely ignored by Config Machine)

    OpCheckBox chkExample = new OpCheckBox(260f, 360f, "Example", true);
    Tabs[0].AddItems(chkExample,
        new OpLabel(260f, 390f, "Enable Example") { bumpBehav = chkExample.bumpBehav });
    // BumpBehaviour is Config Machine's custom class that allows smoothly animated reaction with the input
    // Setting OpLabel's bumpBehav to other UIconfig/UItrigger's bumpBehav allows it to highlight with that element

UItriggers are the other subcategory of UIelements which have signals. When the user activates an UItrigger in some way, it will call the Signal function in OptionInterface which will be explained later.

    Tabs[0].AddItems(new OpSimpleButton(new Vector2(400f, 200f), new Vector2(100f, 24f), "press", "Press Me"));

Update

Update function is called every frame when the game is in ConfigMenu, and this OptionInterface is currently active (The user has currently selected this mod from the list on left). This is useful to make reactive GUI, especially since each UIconfig instance can have only one value saved. The following example code uses cosmetic OpRadioButtonGroup to have four OpColorPickers in a single spot.

private OpRadioButtonGroup rbgSelect;
private int select;
private OpColorPicker[] cpkPlayers;

public override void Initialize()
{
    base.Initialize();
    Tabs = new OpTab[] { new OpTab() };
    
    select = 0;
    rbgSelect = new OpRadioButtonGroup("_", select);
    Tabs[0].AddItems(rbgSelect);
    rbgSelect.SetButtons(new OpRadioButton[]
        {
            new OpRadioButton(50f, 100f){ description = "The Survivor"},
            new OpRadioButton(100f, 100f){ description = "The Monk"},
            new OpRadioButton(150f, 100f){ description = "The Hunter"},
            new OpRadioButton(200f, 100f){ description = "The Nightcat"}
        }
        ); 
    OpColorPicker[] cpkPlayers = new OpColorPicker[4];
    for (int i = 0; i < 4; i++)
    {
        Tabs[0].AddItems(new OpLabel(50f * i, 70f, rbgSelect.buttons[i].description) { bumpBehav = rbgSelect.buttons[i].bumpBehav });
        cpkPlayers[i] = new OpColorPicker(new Vector2(100f, 300f), $"Color{i}", "FFFFFF");
        cpkPlayers[i].Hide();
    }
    cpkPlayers[select].Show();
    Tabs[0].AddItems(cpkPlayers);
}

public override void Update(float dt)
{
    base.Update(dt); // dt is deltaTime
    if (select != rbgSelect.valueInt)
    {
        for (int i = 0; i < cpkPlayers.Length; i++) { cpkPlayers[i].Hide(); }
        select = rbgSelect.valueInt;
        cpkPlayers[select].Show();
    }
}

ConfigOnChange

ConfigOnChange is called whenever config Dictionary is updated, mainly when Config Machine loads OptionInterfaces in IntroRoll, and when the user saves changes of configuration in ConfigMenu. Override this method to grab your configurable values.

config Dictionary is where UIconfig’s value is stored. The following is an example of how to convert a string value to the corresponding type.

public override void ConfigOnChange()
{
    base.ConfigOnChange();
    MyMod.config.myInt = int.Parse(config["keyInt"]);
    MyMod.config.myFloat = float.Parse(config["keyFloat"]);
    MyMod.config.myKey = OpKeyBinder.StringToKeyCode(config["keyKey"]);
    MyMod.config.myColor = OpColorPicker.HexToColor(config["keyColor"]);
}

Signal

Signal is called when the user has interacted with UItrigger. Override this to have your OI to react.

public override void Signal(UItrigger trigger, string signal)
{
    switch (signal)
    {
        case "reset":
            ConfigMenu.ResetCurrentConfig(); // This is the same as pressing Reset Config Button.
            break;
    }
}

Generated OIs

GeneratedOI is a child class of OptionInterface that helps generating fixed-format GUI with less effort.

Displaying Basic Profile

Simply inheriting GeneratedOI instead of OptionInterface works. This does make your mod dependent to Config Machine, and Config Machine generates GUI to display the basic information of your mod without this dependency. So this is only useful when you’re using OptionInterface for other features, like its Translation API.

public class MyOI : GeneratedOI
{
    private const string desc = "Changes this thing and that thing";
    public MyOI() : base(plugin: MyPlugin.instance, desc)
    { transFile = "MyPlugin.Translation.txt"; }
}

The code above would generate a GUI like the following image:

GeneratedOI Sample
GeneratedOI Sample

You can also use its static method AddBasicProfile for basic profiles on top of the canvas. This also has an overload that accepts OpScrollBox instead of OpTab, if you have OpScrollBox replacing OpTab.

public override void Initialize()
{
    base.Initialize();
    Tabs = new OpTab[] { new OpTab() };
    GeneratedOI.AddBasicProfile(Tabs[0], rwMod);
    Tabs[0].AddItems(new OpCheckBox(100f, 350f, "EnableStuff", false) { description = "Enables this stuff" });
}

BepInEx.Configuration

If you don't provide an OptionInterface, BepInEx can generate a simple config screen from your plugin's config bindings (see BepInEx.Configuration). If this happens, CM will not create a separate file to store your config values.

This method has upsides and downsides.

  • You don’t need to reference Config Machine in your project, and this doesn’t create a dependency for Config Machine.
  • The users can configure the plugin by opening the cfg file in Notepad outside the game.
  • However, you do not have control on detailed GUI design.

First, we need ConfigEntry which you can bind to the Config property of BaseUnityPlugin. Consult BepInEx Documentation for farther detail.

public static ConfigEntry<string> cfgText;
public static ConfigEntry<bool> cfgCheck;

void Awake()
{
    cfgText = Config.Bind(
            "General", // section: defines which OpTab this will go
            "My Setting", // key: the name of setting
            "Hello, world!" // defaultValue
            "This is a description. It does things."
                // The description will be shown the right side of UIconfig
                // When you hover your mouse on UIconfig,
                // the first sentence will be displayed at the bottom of the screen
        );
    cfgCheck = Config.Bind("General", "My Other Setting", true, "Another description is this.");
}

To access those settings, use Value.

if (cfgCheck.Value)
{ Logger.LogMessage(cfgText.Value); }

Translation Support

While you can support translation for your mod on your own, Config Machine also offers translation solution.

Preparation

OptionInterface has a method called Translate, and when it's initially called, it will load specified (transFile) txt file from your assembly and make Dictionary depending on Rain World's language setting. In BaseUnityPlugin.LoadOI method, store the OptionInterface instance in somewhere before returning it.

public static MyOI oi;
public static MyOI LoadOI()
{
    oi = new MyOI(); return oi;
}

Then make a static method that calls the translation method.

public static string Translate(string orig)
{
    if (oi != null) { return oi.Translate(orig); }
    return orig;
}

Now when you need to translate something, you can pass your string through this method.

Writing Translation txt

Create a txt file that contains translation, too. Add txt file to your project, make sure that its encoding is UTF-8, then set it to be [Embedded Resources] for its compile setting. Then, in the constructor of your OptionInterface, set transFile to that resource name.

If you have not put it in any folder, it's usually <ProjectName>.<TxtFileName>.txt, but you might get it wrong. If it's wrong when OptionInterface.Translate is initially called, Config Machine will log all the resources in your assembly in exceptionLog.txt so you can copy from it.

public MyOI() : base(plugin: MyPlugin.instance)
{
    this.transFile = "MyPlugin.Translation.txt";
    // if you do not know the resource name, just call Translate.
    // ConfigMachine will log all the resources in your assembly in exceptionLog.txt.
}

The format of the translation txt file for Config Machine is quite primitive. First, you add a keyword, then | for language separator, then language ID (eng, ita, ger, spa, por, kor, jap, rus)), $ for another separator, then translation. If there is no translation, the translator will return the keyword, unless there is a translation for eng. (In that case, English "translation" will be used as default translation)

The following is formatting example for txt file.

// If the first two characters are '//', this line will be ignored.
Nightcat Horn Colour|rus$Цвет рога
// If you have variables, avoid using partial sentences. Not every language has the same subject-verb-object order.
Press <ThrowKey> to stab.|rus$Нажмите <ThrowKey> для атаки.
// When translating the shorter-phrase, the translation might be sensitive with context, but English one does not. Duplicate key causes error(You can check exceptionLog.txt for duplicates), so you can set 'eng' translation for these cases.
Property_as of possession|eng$Property|rus$Собственность
Property_as of quality|eng$Property|rus$Свойство
// For line-breaks, use \n. (In code, it'd be \\n) Each line represents a phrase, so you can't use actual line-break in a single chunk.
LINE\nBREAK|rus$ПЕРЕНОС\nСТРОКИ

Running Translation

And here are some examples in code. Yes, running every text through the Translate method is tedious, but this is usually how it's done in Rain World or many other games. Make sure to check your mod in another language to confirm you haven’t missed any item.

labelCpkrRadio.text = MyPlugin.Translate("Nightcat Horn Colour");
instance.room.game.cameras[0].hud.textPrompt.AddMessage(MyPlugin.Translate("Press <ThrowKey> to stab.").Replace("<ThrowKey>", k.ToString())); // k is variable in this example.
labelLineBreak.text = MyPlugin.Translate("LINE\\nBREAK");


Acceptable Types

As of Config Machine v1.5.1, it accepts the following types and uses provided UIconfig. (Height is the pixel height that how much this ConfigEntry would use. And each entry get 20 pixeled gaps in between)

type UIconfig Height Note
bool OpCheckBox 60
byte OpSliderSubtle 90 Range: [0, 20]
uint OpSlider 90 Range: [0, 100]
int OpTextBox 60 Accepts: Int
float OpTextBox 60 Accepts: Float
string(Hex) OpColorPicker 170 When defaultValue is Hex
string OpTextBox 60 When defaultValue is not Hex; Accepts: ASCII
KeyCode OpKeyBinder 100
enumType OpResourceSelector 60
default N/A Will warn the user that this plugin has ConfigEntry that’s not supported by Config Machine

Available UI Elements

Full list of available UI elements can be found here.