ConfigMachine
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)
All UIelements have bottom-left pos
ition 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)
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.
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
OpScrollBox
s 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 UIelement
s and add them to OpTab
(or OpScrollBox
). The Z-order (the order in which they’re drawn) of UIelement
s 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
UIconfig
s 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
UItrigger
s are the other subcategory of UIelement
s which have signal
s. 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 OpColorPicker
s 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:
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.