diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..7f513f0
Binary files /dev/null and b/.DS_Store differ
diff --git a/.editorconfig b/.editorconfig
index 844818a..f70ab69 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,3 +2,60 @@ root = true
[*.cs]
file_header_template = SPDX-License-Identifier: LGPL-3.0-or-later
+
+# ---------------------------------------------------------------------------
+# Path-scoped analyzer suppressions
+#
+# Each section below suppresses a specific rule ID in a specific file. The
+# scope is deliberately file-level so that the exception cannot silently
+# leak to unrelated code. If you add a new suppression, document *why* the
+# rule is wrong for that file, not just what is being turned off.
+# ---------------------------------------------------------------------------
+
+# ConfigurationManagerAttributes.cs is the copy-paste template that every
+# LMM/BepInEx plugin bundles into its own project. The class is discovered
+# by simple type name from each plugin's bundled copy, and both this fork
+# and upstream BepInEx.ConfigurationManager read values via
+# Type.GetFields(BindingFlags.Instance | BindingFlags.Public). Encapsulating
+# the fields as properties would break the reflection contract with every
+# plugin that has already copied this template. Public fields are the
+# deliberate wire format, not an oversight.
+[ConfigurationManagerAttributes.cs]
+dotnet_diagnostic.S1104.severity = none
+
+# LmmSettingEntryTests.cs contains a nested stub type that intentionally
+# mirrors the public-field shape of ConfigurationManagerAttributes to
+# exercise SetFromAttributes' reflection path. The stub's fields are
+# populated via object-initializer syntax inside the test body, but because
+# the enclosing type is private, the net35 compiler does not credit that
+# assignment and emits CS0649 ("field is never assigned to"). The whole
+# point of the test is to simulate a foreign plugin's copy of the
+# template, so rewriting the stub to silence the warning would defeat the
+# test.
+[LmmSettingEntryTests.cs]
+dotnet_diagnostic.CS0649.severity = none
+
+# CommonHelpers.OpenWebsite takes a URL as a plain string because the URL
+# arrives as string metadata read out of the game (plugin info, mod config,
+# etc.) where there is no System.Uri available. CA1054 ("URI parameters
+# should not be strings") correctly flags that pattern in general, but the
+# source here is already string-shaped and conversion would just push the
+# same unvalidated data through an extra round-trip. Scope note: this is
+# slightly wider than the previous per-member suppression — every method
+# in CommonHelpers.cs is now exempt from CA1054. OpenWebsite is the only
+# URL-shaped signature in the file today; if a future helper introduces a
+# genuinely validatable URI parameter, prefer fixing that site over
+# leaning on this scope.
+[CommonHelpers.cs]
+dotnet_diagnostic.CA1054.severity = none
+
+# ExcludeFromCodeCoverageAttribute.cs is a net35 BCL polyfill: the real
+# System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute was
+# added in .NET 4.0, so this project ships its own copy under the BCL
+# namespace. IDE0130 ("namespace should match folder structure") fires
+# because the file lives under Polyfills/ but declares
+# namespace System.Diagnostics.CodeAnalysis — that mismatch is the whole
+# point of a polyfill and matching the folder name would break the
+# compatibility story.
+[ExcludeFromCodeCoverageAttribute.cs]
+dotnet_diagnostic.IDE0130.severity = none
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 4330b7e..a4f25c2 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -29,8 +29,9 @@ Tests live in `LobCorp.ConfigurationManager.Test` (xunit.v3, Moq, AwesomeAsserti
**Settings discovery (`Implementations/SettingSearcher.cs`):**
- LMM mods register via `Config/LmmConfigRegistration.cs` static API
-- Auto-scans `BaseMods/{modId}/config.cfg` files
+- Auto-scans `{persistentDataPath}/LobotomyBaseMod/{modId}/config.cfg` files
- Discovers BepInEx plugins via reflection (`Implementations/BepInExInterop.cs`) — no hard dependency
+- Mod authors who want an optional-dependency path (settings UI when ConfigurationManager is installed, in-memory fallback otherwise) use the separately published [`LobotomyCorporation.Mods.ConfigurationManager.Integration`](https://github.com/open-lobotomy/LobotomyCorporation.Mods.ConfigurationManager.Integration) source-generator package. That repo owns the generator, the sample mod, and the author-facing docs.
**Configuration model (`Config/`):**
- `LmmConfigFile` — file I/O and parsing for `config.cfg` files
@@ -43,14 +44,14 @@ Tests live in `LobCorp.ConfigurationManager.Test` (xunit.v3, Moq, AwesomeAsserti
- Type-specific controls: checkboxes, sliders, dropdowns, color pickers, hotkey capture
- `ConfigurationManagerAttributes` controls display (order, visibility, custom drawers)
-**`ConfigurationManagerAttributes` is a copy-paste template, not a referenced API.** Each plugin bundles its own copy of the class and assigns values to instances that get passed as tags to setting descriptions. `SettingEntryBase.SetFromAttributes()` reads these via reflection (`Type.GetProperties`, matching by simple type name — not assembly identity). This fork uses **public auto-properties**; upstream BepInEx.ConfigurationManager uses **public fields**, so upstream's template is not directly compatible — if copying from upstream, convert the fields to auto-properties.
+**`ConfigurationManagerAttributes` is a copy-paste template, not a referenced API.** Each plugin bundles its own copy of the class and assigns values to instances that get passed as tags to setting descriptions. `SettingEntryBase.SetFromAttributes()` reads these via reflection (`Type.GetFields`, matching by simple type name — not assembly identity). This fork uses **public fields**, matching upstream BepInEx.ConfigurationManager, so a template copied from either source works unchanged.
## Key Constraints
- **net35 target**: no LINQ extensions beyond what's available, no `System.ValueTuple`, limited BCL. `LangVersion` is set to `latest` so C# syntax features work but BCL APIs are restricted.
-- **RootNamespace and AssemblyName are both `ConfigurationManager`** (not `LobCorp.ConfigurationManager`) — intentionally matches upstream BepInEx.ConfigurationManager. This is a **DLL-name / namespace collision prevention** mechanism only: the identical DLL name stops both from loading simultaneously, and the shared root namespace avoids dual-load conflicts (double UI entries, duplicate `ConfigurationManagerAttributes` processing). This is **not** a public-API-compatibility contract — the fork can freely change its internal shape (e.g. `ConfigurationManagerAttributes` was moved from fields to properties). Do not change `RootNamespace` or `AssemblyName` without accounting for the loader-collision implications.
+- **RootNamespace and AssemblyName are both `ConfigurationManager`** (not `LobCorp.ConfigurationManager`) — intentionally matches upstream BepInEx.ConfigurationManager. This is a **DLL-name / namespace collision prevention** mechanism only: the identical DLL name stops both from loading simultaneously, and the shared root namespace avoids dual-load conflicts (double UI entries, duplicate `ConfigurationManagerAttributes` processing). This is **not** a public-API-compatibility contract — the fork can freely change its internal shape. Do not change `RootNamespace` or `AssemblyName` without accounting for the loader-collision implications.
- **`Harmony_Patch` class name is load-bearing** — every LMM mod must expose an entry type named `Harmony_Patch`. The analyzer package (`LobotomyCorporation.Mods.Analyzers` globalconfig) suppresses S101 and CA1707 repo-wide so this pattern doesn't trip naming rules.
-- **Game assembly references are `Private=false`** — none are copied to output since they exist in the game's managed folder at runtime. The `LobotomyCorporation.Mods.Common` PackageReference does copy to output, as it must be deployed alongside the mod.
+- **Game assembly references are `Private=false`** — none are copied to output since they exist in the game's managed folder at runtime. No other runtime DLLs are copied alongside `ConfigurationManager.dll` today (the previous `LobotomyCorporation.Mods.Common` bridge has been removed).
- **Implicit usings and nullable are disabled.**
- `Microsoft.NETFramework.ReferenceAssemblies` is pulled in implicitly by the SDK for net35 — do not add it to `Directory.Packages.props`.
@@ -64,3 +65,24 @@ NuGet package publishing is planned for v1.0.0 but not yet implemented.
## Analyzers
Global analyzers (`LobotomyCorporation.Mods.Analyzers`, `OpenLobotomy.Standards`) are configured in `Directory.Packages.props`. All Sonar rules run at their default severity — there are no global suppressions in `.editorconfig`. Rule exceptions that apply to all LMM mods (e.g. S101/CA1707 for the `Harmony_Patch` entry point) live in the shared `LobotomyCorporation.Mods.Analyzers` globalconfig, not here. Fix violations rather than suppressing them; if a suppression is truly needed, scope it as narrowly as possible (file-local `#pragma` or per-member `[SuppressMessage]`).
+
+## Audience & Language
+
+This repo ships two audience-facing surfaces:
+
+- **`ConfigurationManager.dll`** — installed by players as an LMM BaseMod. End-user audience (installers, not coders); release notes and the in-game UI should be readable without developer vocabulary.
+- **Mod-author docs in this repo** (primarily `README.md`) — for authors who take a **direct runtime dependency** on `ConfigurationManager.dll` via `LmmConfigRegistration`. This is the "hard dependency" path.
+
+The **optional-dependency** path — settings UI when ConfigurationManager is installed, in-memory fallback when not — is owned by the separate [`LobotomyCorporation.Mods.ConfigurationManager.Integration`](https://github.com/open-lobotomy/LobotomyCorporation.Mods.ConfigurationManager.Integration) repo, along with the sample mod. Author-facing docs, NuGet packaging, and analyzer diagnostics for that path live there, not here.
+
+**Assume the reader is a first-time mod author whose first language is not English.** Most consumers are Korean-speaking modders reading English as a second language or through machine translation, and many have no prior professional development experience. Every error message, README, and code comment that an author will see must pass that bar before shipping.
+
+### Project facts that shape documentation
+
+- **Lobotomy Corporation itself will never update.** The base game is final. Do not pitch wrappers, adapters, or analyzers on "survives game updates" or "keeps working when the game changes" — those claims are factually wrong and will mislead readers. The honest value props for typed wrappers over reflection are: (a) the compiler checks names and types at build time, so typos fail before you run the game; (b) typed code is shorter and easier to read; (c) the package is community-maintained, so fixes land once for everyone. What *does* still change is LMM (the mod loader) and other mods that patch the same game code via Harmony — if a doc needs to explain why a wrapper helps mods coexist, that is the real reason, not game updates.
+
+### Writing Style
+
+**User-facing text** (README, error messages, release notes): use short sentences with active voice and explicit subjects. Avoid idioms, slang, and culturally specific references. Define technical terms inline or use simpler words. Write in a style that survives machine translation — no ambiguous pronouns, no noun stacking.
+
+**Developer-facing text** (code comments, commit messages): technical terminology is fine, but prefer direct, concise phrasing over unnecessarily complex language.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1e459d2..2eeec29 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,7 +29,7 @@ jobs:
dotnet-version: 10.x
- name: Authenticate GitHub Packages
- run: dotnet nuget update source github --username open-lobotomy --password ${{ secrets.PACKAGES_TOKEN }} --store-password-in-clear-text || true
+ run: dotnet nuget update source github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text || true
- name: Extract version from tag
id: version
@@ -48,7 +48,6 @@ jobs:
run: |
mkdir -p staging/ConfigurationManager/Info/en
cp LobCorp.ConfigurationManager/bin/net35/ConfigurationManager.dll staging/ConfigurationManager/
- cp LobCorp.ConfigurationManager/bin/net35/LobotomyCorporation.Mods.Common.*.dll staging/ConfigurationManager/
cp LobCorp.ConfigurationManager/bin/net35/Info/GlobalInfo.xml staging/ConfigurationManager/Info/
cp LobCorp.ConfigurationManager/bin/net35/Info/en/Info.xml staging/ConfigurationManager/Info/en/
diff --git a/.gitignore b/.gitignore
index 6e45246..7aa0ccb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -210,6 +210,7 @@ _pkginfo.txt
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
+*.lscache
# Others
ClientBin/
diff --git a/Directory.Build.props b/Directory.Build.props
index a39f47f..db7f754 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,9 +8,9 @@
In-game configuration manager for LMM and BepInEx mods
- https://github.com/BepInEx/BepInEx.ConfigurationManager
+ BepInEx contributors, Open Lobotomy
- Copyright 2019 / LGPL-3.0
+ Copyright 2019 BepInEx contributors; Copyright 2026 Open Lobotomytruebin\
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 66d4a4a..9fc1c06 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,14 +1,9 @@
true
- 0.1.0-preview.20
-
diff --git a/LobCorp.ConfigurationManager.Test/LobCorp.ConfigurationManager.Test.csproj b/LobCorp.ConfigurationManager.Test/LobCorp.ConfigurationManager.Test.csproj
index bacd62f..9751ebe 100644
--- a/LobCorp.ConfigurationManager.Test/LobCorp.ConfigurationManager.Test.csproj
+++ b/LobCorp.ConfigurationManager.Test/LobCorp.ConfigurationManager.Test.csproj
@@ -7,8 +7,9 @@
net10.0Library$(NoWarn);NU1702
-
+
falsefalseenable
@@ -17,19 +18,11 @@
-
-
- $(PkgLobotomyCorporation_Mods_Common)\lib\net35\LobotomyCorporation.Mods.Common.$(LobotomyCorporationModsCommonVersion).dll
- ..\external\LobotomyCorp_Data\Managed\0Harmony.dll
diff --git a/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmConfigurationProviderTests.cs b/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmConfigurationProviderTests.cs
deleted file mode 100644
index f759c40..0000000
--- a/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmConfigurationProviderTests.cs
+++ /dev/null
@@ -1,349 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-#region
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using AwesomeAssertions;
-using ConfigurationManager.Config;
-using ConfigurationManager.Implementations;
-using LobotomyCorporation.Mods.Common;
-using Xunit;
-
-#endregion
-
-namespace LobCorp.ConfigurationManager.Test.ModTests.ConfigurationManagerTests
-{
- public sealed class LmmConfigurationProviderTests : IDisposable
- {
- private readonly List _tempPaths = [];
-
- public void Dispose()
- {
- foreach (var path in _tempPaths)
- {
- if (File.Exists(path))
- {
- File.Delete(path);
- }
- }
- }
-
- private LmmConfigFile CreateTempConfigFile()
- {
- var path = Path.GetTempFileName();
- _tempPaths.Add(path);
- return new LmmConfigFile(path);
- }
-
- private LmmConfigurationProvider CreateProvider(LmmConfigFile? reusableFile = null)
- {
- var file = reusableFile ?? CreateTempConfigFile();
- return new LmmConfigurationProvider((_, _, _) => file);
- }
-
- [Fact]
- public void Constructor_NullFactory_ShouldThrowArgumentNullException()
- {
- Action act = () =>
- {
- var _ = new LmmConfigurationProvider(
- (Func)null!
- );
- };
-
- act.Should().Throw();
- }
-
- [Fact]
- public void Constructor_Default_ShouldNotThrow()
- {
- Action act = () =>
- {
- var _ = new LmmConfigurationProvider();
- };
-
- act.Should().NotThrow();
- }
-
- [Fact]
- public void LoadPersistedValues_NullEntries_ShouldThrowArgumentNullException()
- {
- var provider = CreateProvider();
-
- Action act = () => provider.LoadPersistedValues(null!);
-
- act.Should().Throw();
- }
-
- [Fact]
- public void LoadPersistedValues_ShouldBindEntryToConfigFile()
- {
- var file = CreateTempConfigFile();
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- };
-
- provider.LoadPersistedValues([entry]);
-
- var bound = file.Bind("General", "Volume", 0);
- bound.BoxedValue.Should().Be(50);
- }
-
- [Fact]
- public void LoadPersistedValues_PersistedValueDifferentFromDefault_ShouldSyncValue()
- {
- var file = CreateTempConfigFile();
- file.Bind("General", "Volume", 50).Value = 75;
-
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- };
-
- provider.LoadPersistedValues([entry]);
-
- entry.Value.Should().Be(75);
- }
-
- [Fact]
- public void LoadPersistedValues_PersistedValueMatchesDefault_ShouldNotChangeValue()
- {
- var provider = CreateProvider();
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- Value = 50,
- };
-
- provider.LoadPersistedValues([entry]);
-
- // Value should still be the default — provider should not have overwritten it.
- entry.Value.Should().Be(50);
- }
-
- [Fact]
- public void LoadPersistedValues_ChangingBoundEntry_ShouldPropagateToConfigEntry()
- {
- var file = CreateTempConfigFile();
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- };
-
- provider.LoadPersistedValues([entry]);
- var bound = file.Bind("General", "Volume", 0);
- bound.BoxedValue = 99;
-
- entry.Value.Should().Be(99);
- }
-
- [Fact]
- public void LoadPersistedValues_EntryWithDisplayName_ShouldAttachDisplayNameTag()
- {
- var file = CreateTempConfigFile();
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- DisplayName = "Volume Level",
- Description = "Controls volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- };
-
- provider.LoadPersistedValues([entry]);
-
- var bound = file.Bind("General", "Volume", 0);
- bound.Description.Description.Should().Be("Controls volume");
- bound.Description.Tags.OfType().Should().ContainSingle();
- }
-
- [Fact]
- public void LoadPersistedValues_EntryWithUseSlider_ShouldAttachConfigurationManagerAttributes()
- {
- var file = CreateTempConfigFile();
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- UseSlider = true,
- };
-
- provider.LoadPersistedValues([entry]);
-
- var bound = file.Bind("General", "Volume", 0);
- var attrs = bound
- .Description.Tags.OfType()
- .Single();
- attrs.UseIntegerSlider.Should().Be(true);
- }
-
- [Fact]
- public void LoadPersistedValues_EntryWithoutUseSlider_ShouldNotAttachConfigurationManagerAttributes()
- {
- var file = CreateTempConfigFile();
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- UseSlider = false,
- };
-
- provider.LoadPersistedValues([entry]);
-
- var bound = file.Bind("General", "Volume", 0);
- bound
- .Description.Tags.OfType()
- .Should()
- .BeEmpty();
- }
-
- [Fact]
- public void LoadPersistedValues_EntryWithAcceptableValueRange_ShouldSetAcceptableValues()
- {
- var file = CreateTempConfigFile();
- var provider = CreateProvider(file);
-
- // Use a real ModConfig + Bind to get a real IConfigEntry with Range set.
- var config = new ModConfig("mod1", "Mod", "1.0");
- var realEntry = config.Bind(
- "General",
- "Volume",
- 50,
- "Volume setting",
- range: new LobotomyCorporation.Mods.Common.AcceptableValueRange(0, 100)
- );
-
- provider.LoadPersistedValues([realEntry]);
-
- var bound = file.Bind("General", "Volume", 0);
- bound
- .Description.AcceptableValues.Should()
- .BeOfType>();
- }
-
- [Fact]
- public void GetOrCreateConfigFile_SameModId_ShouldReuseFile()
- {
- var factoryCallCount = 0;
- var file = CreateTempConfigFile();
- var provider = new LmmConfigurationProvider(
- (_, _, _) =>
- {
- factoryCallCount++;
- return file;
- }
- );
-
- var entry1 = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "A",
- SettingType = typeof(int),
- DefaultValue = 1,
- };
- var entry2 = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "B",
- SettingType = typeof(int),
- DefaultValue = 2,
- };
-
- provider.LoadPersistedValues([entry1, entry2]);
-
- factoryCallCount.Should().Be(1);
- }
-
- [Fact]
- public void Save_ShouldInvokeSaveOnAllRegisteredConfigFiles()
- {
- var path = Path.GetTempFileName();
- _tempPaths.Add(path);
- var file = new LmmConfigFile(path);
- var provider = CreateProvider(file);
- var entry = new StubConfigEntry
- {
- ModId = "mod1",
- Section = "General",
- Key = "Volume",
- SettingType = typeof(int),
- DefaultValue = 50,
- };
- provider.LoadPersistedValues([entry]);
- file.Bind("General", "Volume", 0).Value = 77;
-
- File.WriteAllText(path, string.Empty);
-
- provider.Save();
-
- var contents = File.ReadAllText(path);
- contents.Should().Contain("Volume = 77");
- }
-
- [Fact]
- public void Save_WithNoLoadedEntries_ShouldNotThrow()
- {
- var provider = CreateProvider();
-
- Action act = provider.Save;
-
- act.Should().NotThrow();
- }
-
- ///
- /// Minimal stub implementing for testing the provider
- /// without needing a real .
- ///
- private sealed class StubConfigEntry : IConfigEntry
- {
- public string ModId { get; set; } = "mod";
- public string ModName { get; set; } = "Mod";
- public string ModVersion { get; set; } = "1.0";
- public string Section { get; set; } = "Section";
- public string Key { get; set; } = "Key";
- public string DisplayName { get; set; } = string.Empty;
- public string Description { get; set; } = string.Empty;
- public Type SettingType { get; set; } = typeof(int);
- public object DefaultValue { get; set; } = 0;
- public bool UseSlider { get; set; }
- public object Value { get; set; } = 0;
- }
- }
-}
diff --git a/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmSettingEntryTests.cs b/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmSettingEntryTests.cs
index 795d023..b3c795f 100644
--- a/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmSettingEntryTests.cs
+++ b/LobCorp.ConfigurationManager.Test/ModTests/ConfigurationManagerTests/LmmSettingEntryTests.cs
@@ -257,16 +257,16 @@ public void Constructor_NullEntry_ShouldThrowArgumentNullException()
///
/// Stand-in for a plugin that bundles its own copy of ConfigurationManagerAttributes.
/// SetFromAttributes matches by simple type name, not assembly identity, so this
- /// property-shape stub should populate the entry exactly like the real class.
+ /// field-shape stub should populate the entry exactly like the real class.
///
private sealed class ConfigurationManagerAttributes
{
- public bool? IsAdvanced { get; set; }
- public int? Order { get; set; }
+ public bool? IsAdvanced;
+ public int? Order;
}
[Fact]
- public void Constructor_ForeignConfigurationManagerAttributesStub_ShouldPopulateFromProperties()
+ public void Constructor_ForeignConfigurationManagerAttributesStub_ShouldPopulateFromFields()
{
var stub = new ConfigurationManagerAttributes { IsAdvanced = true, Order = 7 };
var desc = new LmmConfigDescription("test", null, stub);
diff --git a/LobCorp.ConfigurationManager/Config/LmmConfigRegistration.cs b/LobCorp.ConfigurationManager/Config/LmmConfigRegistration.cs
index 63c05fc..482aba1 100644
--- a/LobCorp.ConfigurationManager/Config/LmmConfigRegistration.cs
+++ b/LobCorp.ConfigurationManager/Config/LmmConfigRegistration.cs
@@ -15,6 +15,16 @@ namespace ConfigurationManager.Config
)]
public static class LmmConfigRegistration
{
+ ///
+ /// Version of the registration contract between this runtime and the
+ /// LobotomyCorporation.Mods.ConfigurationManager.Integration source generator.
+ /// The generator probes this constant at runtime and refuses to register against
+ /// a mismatched value, so that breaking changes to the registration surface fail
+ /// loudly instead of silently corrupting bindings. Bump only when the shape of
+ /// the Register overloads changes in a way incompatible with older generators.
+ ///
+ public const int ApiVersion = 1;
+
private static readonly Dictionary RegisteredMods =
new Dictionary();
@@ -57,7 +67,7 @@ public static LmmConfigFile GetConfigFile(
}
///
- /// Register a single setting for a mod.
+ /// Register a single setting for a mod using a plain description string.
///
/// The type of the setting value.
/// Unique mod identifier.
@@ -66,7 +76,7 @@ public static LmmConfigFile GetConfigFile(
/// Setting key within the section.
/// Default value used when no saved value exists.
/// Optional plain-text description shown in the settings UI.
- /// Optional version string shown alongside the mod name.
+ /// Optional version string shown in the settings UI.
public static LmmConfigEntry Register(
string modId,
string modName,
@@ -82,7 +92,8 @@ public static LmmConfigEntry Register(
}
///
- /// Register a single setting with full description.
+ /// Register a single setting for a mod with a full description including
+ /// acceptable-value constraints and UI-hint tags.
///
/// The type of the setting value.
/// Unique mod identifier.
@@ -90,7 +101,7 @@ public static LmmConfigEntry Register(
/// Config section name to group the setting under.
/// Setting key within the section.
/// Default value used when no saved value exists.
- /// Full description including acceptable value constraints.
+ /// Description carrying optional UI-hint tags and acceptable-value constraints.
/// Optional version string shown in the settings UI.
public static LmmConfigEntry Register(
string modId,
diff --git a/LobCorp.ConfigurationManager/ConfigurationManagerAttributes.cs b/LobCorp.ConfigurationManager/ConfigurationManagerAttributes.cs
index 9162f5c..6a8fa07 100644
--- a/LobCorp.ConfigurationManager/ConfigurationManagerAttributes.cs
+++ b/LobCorp.ConfigurationManager/ConfigurationManagerAttributes.cs
@@ -7,11 +7,11 @@ namespace ConfigurationManager
///
/// Usage:
/// This class template has to be copied inside the plugin's project and referenced by its code directly.
- /// Make a new instance, assign any properties that you want to override, and pass it as a tag for your setting.
+ /// Make a new instance, assign any fields that you want to override, and pass it as a tag for your setting.
///
///
- /// If a property is null (default), it will be ignored and won't change how the setting is displayed.
- /// If a property is non-null (you assigned a value to it), it will override default behavior.
+ /// If a field is null (default), it will be ignored and won't change how the setting is displayed.
+ /// If a field is non-null (you assigned a value to it), it will override default behavior.
///
///
///
@@ -27,9 +27,9 @@ namespace ConfigurationManager
///
///
/// This fork of ConfigurationManager reads attribute values from a plugin's copy of this class via public
- /// properties (Type.GetProperties). Upstream BepInEx.ConfigurationManager uses public fields, so its
- /// template is not directly compatible — if copying from upstream, convert the fields to auto-properties.
- /// You can optionally remove properties that you won't use from this class, it's the same as leaving them null.
+ /// fields (Type.GetFields), aligned with upstream BepInEx.ConfigurationManager so that a template
+ /// copied from either source works without modification. You can optionally remove fields that you won't
+ /// use from this class, it's the same as leaving them null.
///
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(
Justification = "Data holder class for plugin configuration attributes"
@@ -39,24 +39,24 @@ public sealed class ConfigurationManagerAttributes
///
/// Should the setting be shown as a percentage (only use with value range settings).
///
- public bool? ShowRangeAsPercent { get; set; }
+ public bool? ShowRangeAsPercent;
///
/// If true, the slider snaps to whole numbers while the text field still accepts decimals.
///
- public bool? UseIntegerSlider { get; set; }
+ public bool? UseIntegerSlider;
///
/// Custom setting editor (OnGUI code that replaces the default editor provided by ConfigurationManager).
/// See below for a deeper explanation. Using a custom drawer will cause many of the other fields to do nothing.
///
- public System.Action CustomDrawer { get; set; }
+ public System.Action CustomDrawer;
///
/// Custom setting editor that allows polling keyboard input with the Input class.
/// Use either CustomDrawer or CustomHotkeyDrawer, using both at the same time leads to undefined behaviour.
///
- public CustomHotkeyDrawerFunc CustomHotkeyDrawer { get; set; }
+ public CustomHotkeyDrawerFunc CustomHotkeyDrawer;
///
/// Custom setting draw action that allows polling keyboard input with the Input class.
@@ -71,62 +71,62 @@ ref bool isCurrentlyAcceptingInput
///
/// Show this setting in the settings screen at all? If false, don't show.
///
- public bool? Browsable { get; set; }
+ public bool? Browsable;
///
/// Category the setting is under. Null to be directly under the plugin.
///
- public string Category { get; set; }
+ public string Category;
///
/// If set, a "Default" button will be shown next to the setting to allow resetting to default.
///
- public object DefaultValue { get; set; }
+ public object DefaultValue;
///
/// Force the "Reset" button to not be displayed, even if a valid DefaultValue is available.
///
- public bool? HideDefaultButton { get; set; }
+ public bool? HideDefaultButton;
///
/// Force the setting name to not be displayed.
///
- public bool? HideSettingName { get; set; }
+ public bool? HideSettingName;
///
/// Optional description shown when hovering over the setting.
///
- public string Description { get; set; }
+ public string Description;
///
/// Name of the setting.
///
- public string DispName { get; set; }
+ public string DispName;
///
/// Order of the setting on the settings list relative to other settings in a category.
/// 0 by default, higher number is higher on the list.
///
- public int? Order { get; set; }
+ public int? Order;
///
/// Only show the value, don't allow editing it.
///
- public bool? ReadOnly { get; set; }
+ public bool? ReadOnly;
///
/// If true, don't show the setting by default. User has to turn on showing advanced settings or search for it.
///
- public bool? IsAdvanced { get; set; }
+ public bool? IsAdvanced;
///
/// Custom converter from setting type to string for the built-in editor textboxes.
///
- public System.Func