Skip to content
2 changes: 2 additions & 0 deletions BaseLib/localization/eng/settings_ui.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"BASELIB.mod_title": "Example mod configuration",

"BASELIB-FIRST_SECTION.title": "These are just example options",
"BASELIB-SECOND_SECTION.title": "Nothing here does anything at all",

Expand Down
115 changes: 82 additions & 33 deletions Config/ModConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@
using System.Text.RegularExpressions;
using BaseLib.Config.UI;
using BaseLib.Extensions;
using BaseLib.Utils;
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.Assets;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Nodes.CommonUi;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
using MegaCrit.Sts2.Core.Nodes.Screens.Settings;

namespace BaseLib.Config;

// NMainMenu is recreated every time you return from a run, etc., so this isn't a run-once as it seems.
// We also check for errors when exiting the mod config submenu, as that won't trigger this code.
[HarmonyPatch(typeof(NMainMenu), nameof(NMainMenu._Ready))]
public static class NMainMenu_Ready_Patch
{
public static void Postfix()
{
if (ModConfig.ModConfigLogger.PendingUserMessages.Count == 0) return;
Callable.From(ModConfig.ShowAndClearPendingErrors).CallDeferred();
}
}

public abstract partial class ModConfig
{
private const string SettingsTheme = "res://themes/settings_screen_line_header.tres";
Expand All @@ -26,7 +39,7 @@ public abstract partial class ModConfig
public event EventHandler? ConfigChanged;

private readonly string _path;
protected string ModPrefix { get; private set; }
public string ModPrefix { get; private set; }

private readonly string _modConfigName;
private bool _savingDisabled;
Expand All @@ -38,12 +51,17 @@ public static class ModConfigLogger
{
public static List<string> PendingUserMessages { get; } = [];

/// <summary>
/// Show a message in the console, and optionally in the GUI. Only use showInGui=true if truly necessary;
/// players won't enjoy having warnings/errors shoved in their faces unless it's something that truly impacts them.
/// </summary>
public static void Warn(string message, bool showInGui = false)
{
MainFile.Logger.Warn(message);
if (showInGui && !PendingUserMessages.Contains(message)) PendingUserMessages.Add(message);
}

/// <inheritdoc cref="Warn" />
public static void Error(string message, bool showInGui = true)
{
MainFile.Logger.Error(message);
Expand Down Expand Up @@ -90,7 +108,7 @@ private void CheckConfigProperties()
if (!property.CanRead || !property.CanWrite) continue;
if (property.GetMethod?.IsStatic != true)
{
ModConfigLogger.Warn($"Ignoring {_modConfigName} property {property.Name}: only static properties are supported", true);
ModConfigLogger.Warn($"Ignoring {_modConfigName} property {property.Name}: only static properties are supported");
continue;
}

Expand All @@ -117,22 +135,36 @@ public void Save()
{
if (_savingDisabled)
{
ModConfigLogger.Error($"Skipping save for {_modConfigName} because the config file is currently in a corrupted, read-only state.");
// No GUI error here, because that would've been shown already when _savingDisabled was set.
ModConfigLogger.Warn($"Skipping save for {_modConfigName} because the config file is currently in a corrupted, read-only state.");
return;
}

Dictionary<string, string> values = [];
foreach (var property in ConfigProperties)

try
{
var value = property.GetValue(null);
foreach (var property in ConfigProperties)
{
var value = property.GetValue(null);

var converter = TypeDescriptor.GetConverter(property.PropertyType);
var stringValue = converter.ConvertToInvariantString(value);
var converter = TypeDescriptor.GetConverter(property.PropertyType);
var stringValue = converter.ConvertToInvariantString(value);

if (stringValue != null)
values.Add(property.Name, stringValue);
else
ModConfigLogger.Warn($"Failed to convert {_modConfigName} property {property.Name} to string for saving; it will be omitted.", true);
if (stringValue != null)
values.Add(property.Name, stringValue);
else
{
ModConfigLogger.Warn(
$"Failed to convert {_modConfigName} property {property.Name} to string for saving; " +
"it will be omitted.");
}
}
}
catch (Exception)
{
// During testing, I have never seen an exception here, but let's avoid a game crash/menu hang, etc.
ModConfigLogger.Error($"Failed to save config {_modConfigName}: unknown error during conversion.", false);
}

try
Expand Down Expand Up @@ -186,17 +218,20 @@ public void Load()
if (!TryApplyPropertyValue(property, value)) hasSoftErrors = true;
}

MainFile.Logger.Info(!hasSoftErrors
? $"Loaded config {_modConfigName} successfully"
: $"Loaded config {_modConfigName} with some missing or invalid fields.");
if (hasSoftErrors)
ModConfigLogger.Warn($"Loaded config {_modConfigName} with some missing or invalid fields.");
}
}
catch (JsonException jsonEx)
{
ModConfigLogger.Error($"Failed to parse config file for {_modConfigName}. The JSON is likely invalid. " +
$"Error: {jsonEx.Message}");
ModConfigLogger.Warn("Config saving has been DISABLED for this session to protect any manual edits. " +
"Please fix the JSON formatting.", true);
// Unlikely to happen except for people who have modified the file manually, so let's be verbose and show in GUI.
var locationText = jsonEx.LineNumber.HasValue
? $"Line {jsonEx.LineNumber + 1}, position {jsonEx.BytePositionInLine + 1}"
: "unknown line";
ModConfigLogger.Error($"Failed to parse config file for {_modConfigName}. The JSON is likely invalid.\n" +
$"File path: {_path}\n" +
$"Error location: {locationText}");
ModConfigLogger.Warn("Config saving has been DISABLED for this mod to protect any manual edits.", true);
_savingDisabled = true;
return;
}
Expand Down Expand Up @@ -224,7 +259,7 @@ private static bool TryApplyPropertyValue(PropertyInfo property, string value)
if (configVal == null)
{
ModConfigLogger.Warn($"Failed to load saved config value \"{value}\" for property {property.Name}:" +
"Converter returned null.", true);
"Converter returned null.");
return false;
}

Expand All @@ -239,7 +274,7 @@ private static bool TryApplyPropertyValue(PropertyInfo property, string value)
catch (Exception ex)
{
ModConfigLogger.Warn($"Failed to load saved config value \"{value}\" for property {property.Name}. " +
$"Error: {ex.Message}", true);
$"Error: {ex.Message}");
return false;
}
}
Expand All @@ -250,27 +285,27 @@ protected string GetLabelText(string labelName)
return loc != null ? loc.GetFormattedText() : labelName;
}

// Creates a raw toggle control, with no layout (see SimpleModConfig.CreateToggleOption unless you want custom layout)
// Creates a raw toggle control, with no layout (use SimpleModConfig.CreateToggleOption unless you want custom layout)
protected NConfigTickbox CreateRawTickboxControl(PropertyInfo property)
{
var tickbox = new NConfigTickbox().TransferAllNodes(SceneHelper.GetScenePath("screens/settings_tickbox"));
var tickbox = new NConfigTickbox();
tickbox.Initialize(this, property);
return tickbox;
}

// Creates a raw slider control, with no layout (see SimpleModConfig.CreateSliderOption unless you want custom layout)
// Creates a raw slider control, with no layout (use SimpleModConfig.CreateSliderOption unless you want custom layout)
protected NConfigSlider CreateRawSliderControl(PropertyInfo property)
{
var slider = new NConfigSlider().TransferAllNodes(SceneHelper.GetScenePath("screens/settings_slider"));
var slider = new NConfigSlider();
slider.Initialize(this, property);
return slider;
}

// Creates a raw dropdown control, with no layout (see SimpleModConfig.CreateDropdownOption unless you want custom layout)
// Creates a raw dropdown control, with no layout (use SimpleModConfig.CreateDropdownOption unless you want custom layout)
private static readonly FieldInfo DropdownNode = AccessTools.DeclaredField(typeof(NDropdownPositioner), "_dropdownNode");
protected NDropdownPositioner CreateRawDropdownControl(PropertyInfo property)
{
var dropdown = new NConfigDropdown().TransferAllNodes(SceneHelper.GetScenePath("screens/settings_dropdown"));
var dropdown = new NConfigDropdown();
var items = CreateDropdownItems(property, out var currentIndex);
dropdown.SetItems(items, currentIndex);

Expand Down Expand Up @@ -323,7 +358,7 @@ protected NDropdownPositioner CreateRawDropdownControl(PropertyInfo property)

// Creates a raw label control, with no layout (see SimpleModConfig.Create*Option and CreateSectionHeader for
// layout-ready controls)
protected static MegaRichTextLabel CreateRawLabelControl(string labelText, int fontSize)
public static MegaRichTextLabel CreateRawLabelControl(string labelText, int fontSize)
{
var kreonNormal = PreloadManager.Cache.GetAsset<Font>("res://themes/kreon_regular_shared.tres");
var kreonBold = PreloadManager.Cache.GetAsset<Font>("res://themes/kreon_bold_shared.tres");
Expand All @@ -334,6 +369,7 @@ protected static MegaRichTextLabel CreateRawLabelControl(string labelText, int f
Theme = PreloadManager.Cache.GetAsset<Theme>(SettingsTheme),
AutoSizeEnabled = false,
MouseFilter = Control.MouseFilterEnum.Ignore,
FocusMode = Control.FocusModeEnum.None,
BbcodeEnabled = true,
ScrollActive = false,
VerticalAlignment = VerticalAlignment.Center,
Expand All @@ -342,11 +378,7 @@ protected static MegaRichTextLabel CreateRawLabelControl(string labelText, int f

label.AddThemeFontOverride("normal_font", kreonNormal);
label.AddThemeFontOverride("bold_font", kreonBold);
label.AddThemeFontSizeOverride("normal_font_size", fontSize);
label.AddThemeFontSizeOverride("bold_font_size", fontSize);
label.AddThemeFontSizeOverride("bold_italics_font_size", fontSize);
label.AddThemeFontSizeOverride("italics_font_size", fontSize);
label.AddThemeFontSizeOverride("mono_font_size", fontSize);
label.AddThemeFontSizeOverrideAll(fontSize);

return label;
}
Expand All @@ -362,6 +394,23 @@ protected static ColorRect CreateDividerControl()
};
}

public static void ShowAndClearPendingErrors()
{
var pendingMessages = ModConfigLogger.PendingUserMessages;
if (pendingMessages.Count <= 0) return;

var errorPopup = NErrorPopup.Create("Mod configuration error",
string.Join('\n', pendingMessages), false);
if (errorPopup == null || NModalContainer.Instance == null) return;
NModalContainer.Instance.Add(errorPopup);

var vertPopup = errorPopup.GetNodeOrNull<NVerticalPopup>("VerticalPopup");
if (vertPopup == null) return;
vertPopup.BodyLabel.AddThemeFontSizeOverrideAll(22);

pendingMessages.Clear();
}

[GeneratedRegex("[^a-zA-Z0-9_.]")]
private static partial Regex SpecialCharRegex();
}
13 changes: 6 additions & 7 deletions Config/SimpleModConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ protected MarginContainer CreateSectionHeader(string labelName, bool alignToTop
container.AddThemeConstantOverride("margin_left", 24);
container.AddThemeConstantOverride("margin_right", 24);
container.MouseFilter = Control.MouseFilterEnum.Ignore;
container.FocusMode = Control.FocusModeEnum.None;

var label = CreateRawLabelControl($"[center][b]{GetLabelText(labelName)}[/b][/center]", 40);
label.Name = "SectionLabel_" + labelName.Replace(" ", "");
Expand Down Expand Up @@ -147,14 +148,12 @@ protected void GenerateOptionsForAllProperties(Control targetContainer)

if (previousSetting != null)
{
var path = currentSetting.GetPathTo(previousSetting);
if (currentSetting.FocusNeighborLeft?.IsEmpty != false) currentSetting.FocusNeighborLeft = path;
if (currentSetting.FocusNeighborTop?.IsEmpty != false) currentSetting.FocusNeighborTop = path;

path = previousSetting.GetPathTo(currentSetting);
if (previousSetting.FocusNeighborRight?.IsEmpty != false) previousSetting.FocusNeighborRight = path;
if (previousSetting.FocusNeighborBottom?.IsEmpty != false) previousSetting.FocusNeighborBottom = path;
currentSetting.FocusNeighborTop = currentSetting.GetPathTo(previousSetting);
previousSetting.FocusNeighborBottom = previousSetting.GetPathTo(currentSetting);
}

currentSetting.FocusNeighborLeft = currentSetting.GetPath();
currentSetting.FocusNeighborRight = currentSetting.GetPath();
}
catch (NotSupportedException ex)
{
Expand Down
37 changes: 24 additions & 13 deletions Config/UI/NConfigButton.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using BaseLib.Patches.UI;
using System.Reflection;
using BaseLib.Patches.UI;
using BaseLib.Utils;
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Assets;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
using MegaCrit.Sts2.Core.Nodes.Screens.ModdingScreen;
using MegaCrit.Sts2.Core.Nodes.TopBar;

Expand Down Expand Up @@ -59,6 +62,7 @@ public override void _Process(double delta)
}
}

private static readonly FieldInfo ModdingScreenStack = AccessTools.Field(typeof(NModdingScreen), "_stack");
protected override void OnRelease()
{
base.OnRelease();
Expand All @@ -74,17 +78,7 @@ protected override void OnRelease()
var modConfig = ModConfigRegistry.Get(mod.manifest?.id);
if (modConfig != null)
{
// Find NModdingScreen
var parent = GetParent();
while (parent != null && parent is not NModdingScreen)
{
parent = parent.GetParent();
}

if (parent is not NModdingScreen screen) return;

NModConfigPopup.ShowModConfig(screen, modConfig, this);
IsConfigOpen = true;
OpenModConfigSubmenu(modConfig);
}
}

Expand All @@ -105,10 +99,27 @@ public override void OnUnfocus()
base.OnUnfocus();
NHoverTipSet.Remove(this);
}*/


protected override bool IsOpen()
{
return IsConfigOpen;
}

private void OpenModConfigSubmenu(ModConfig modConfig)
{
var stackField = AccessTools.Field(typeof(NSubmenu), "_stack");

if (FindParent("ModdingScreen") is not NModdingScreen moddingScreen ||
stackField.GetValue(moddingScreen) is not NMainMenuSubmenuStack stackInstance)
{
ModConfig.ModConfigLogger.Error("Unable to locate the game's modding screen!\n" +
"Please report a bug at:\nhttps://github.com/Alchyr/BaseLib-StS2");
return;
}

var modConfigSubmenu = stackInstance.PushSubmenuType<NModConfigSubmenu>();
modConfigSubmenu.LoadModConfig(modConfig, this);

IsConfigOpen = true;
}
}
Loading