Skip to content

Rewrite the FAWE configuration system - #3703

Open
MattBDev wants to merge 29 commits into
fix/v3/worldedit-config-loadingfrom
refactor/v3/settings-records
Open

MattBDev wants to merge 29 commits into
fix/v3/worldedit-config-loadingfrom
refactor/v3/settings-records

Conversation

@MattBDev

@MattBDev MattBDev commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

FAWE's config.yml is now loaded into read-only Java records. Each setting's default is written next to the setting, and code changes a setting with a method reference such as Settings.History::compressionLevel.

Removed API

Before After
Settings.settings().QUEUE.PARALLEL_THREADS Settings.settings().queue().parallelThreads()
Settings.IMP Settings.settings()
Settings.settings().EXPERIMENTAL.PERSISTENT_BRUSHES = false Settings.override(Settings.Experimental::persistentBrushes, false)
Settings.settings().reload(File) Settings.set(Settings.load(Path))
Settings.settings().save(File) settings.save(Path)
Settings.settings().LIMITS.get("default") Settings.settings().defaultLimits()
Settings.settings().PROTOCOL_SUPPORT_FIX FaweCache.INSTANCE.setProtocolSupportFix(boolean)
Config and its annotations @Comment, @Default

Performance

The new code loads and reloads about twice as fast, works out limits 2.4 to 3 times as fast, and allocates less memory, except when saving. Reading a setting costs the same.

Saving is slower. It takes about 12% longer on the stock file and about 50% longer with 8 limit groups, which is 0.1 to 0.5 ms more, and it allocates 3 to 4 times as much memory. That's the cost of writing through SnakeYAML instead of by hand. FAWE only saves at startup and on /fawe reload.

Benchmark Old New Speed-up Old alloc New alloc
getLimit, player in no group, stock file 261 ± 19 ns 88 ± 8 ns 2.97× 640 B 264 B
getLimit, player in two groups, 8 groups 1.14 ± 0.04 µs 478 ± 13 ns 2.39× 2 KB 752 B
Six setting reads 2.14 ± 0.05 ns 2.21 ± 0.08 ns 0.97× 0 B 0 B
Load, stock file 1.22 ± 0.06 ms 0.63 ± 0.03 ms 1.95× 1.31 MB 515 KB
Reload, stock file 1.89 ± 0.07 ms 0.95 ± 0.02 ms 1.99× 1.42 MB 830 KB
Forced save, stock file 0.78 ± 0.05 ms 0.88 ± 0.07 ms 0.89× 96 KB 306 KB
Forced save, 8 groups 0.98 ± 0.07 ms 1.48 ± 0.16 ms 0.66× 209 KB 769 KB

A speed-up below 1× means the new code is slower.

Settings becomes a tree of records: each nested record is a section of
config.yml and each component an option, with its comment in @comment
and its default in @default. ConfigMapper binds a parsed file to the
records and writes them back, so key names no longer come from field
names by reflection. config.yml keeps its keys, order, comments and
defaults.

- Settings.override and Settings#with change an option through an
  accessor reference, so a wrong value type is a compile error.
- Reloading builds a new tree and swaps one reference.
- Strings are escaped when written, an unreadable file is not
  overwritten, and an unchanged file is not rewritten.
- LimitResolver prepares each limit group once per loaded
  configuration instead of on every getLimit call.
- PROTOCOL_SUPPORT_FIX moves to FaweCache#setProtocolSupportFix, since
  a record cannot hold a runtime flag.

Config and ConfigOptComputation are removed and every use of the old
Settings fields is migrated.
@MattBDev
MattBDev added this pull request to stack #3704 October 21, 2026 04:54
Settings becomes a tree of records: each nested record is a section of
config.yml and each component an option, with its comment in @comment
and its default in @default. ConfigMapper binds a parsed file to the
records and writes them back, so key names no longer come from field
names by reflection. config.yml keeps its keys, order, comments and
defaults.

- Settings.override and Settings#with change an option through an
  accessor reference, so a wrong value type is a compile error.
- Reloading builds a new tree and swaps one reference.
- Strings are escaped when written, an unreadable file is not
  overwritten, and an unchanged file is not rewritten.
- LimitResolver prepares each limit group once per loaded
  configuration instead of on every getLimit call.
- PROTOCOL_SUPPORT_FIX moves to FaweCache#setProtocolSupportFix, since
  a record cannot hold a runtime flag.

Config and ConfigOptComputation are removed and every use of the old
Settings fields is migrated.
@MattBDev
MattBDev force-pushed the refactor/v3/settings-records branch from 6ef49ee to 4adf724 Compare October 21, 2026 04:55
@MattBDev
MattBDev requested a balanced review from Copilot October 21, 2026 04:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It is an explicitly Work-In-Progress draft that rewrites the core configuration system and mechanically touches ~70 call sites, which warrants human review before approval.

3 open issues
What changed in this PR

This PR replaces FAWE's legacy reflection-based Config.java configuration system with an immutable, record-based Settings API. Configuration sections become nested Java records whose components are annotated with @Comment/@Default, and a new mapping layer (ConfigMapper, ConfigYaml, ConfigTree, LimitResolver) handles record↔YAML serialization, migration of legacy keys, and limit-group resolution. Roughly 70 call sites are mechanically migrated from uppercase field access (Settings.settings().QUEUE.PARALLEL_THREADS) to accessor methods (Settings.settings().queue().parallelThreads()). The PR is explicitly a Work-In-Progress draft.

Changes:

  • Introduce an immutable record-based Settings model plus a new serialization/migration framework and comprehensive unit tests.
  • Rewrite Fawe.setupConfigs() to load/save via Settings.load/save with a BuildInfo, and move the FaweCache.protocolSupportFix flag off the global settings object.
  • Migrate ~70 call sites to the new accessor API and delete Config.java/ConfigOptComputation.java.
File Description
.../​configuration/​Settings.java New immutable record config model, load/save/migrate/defaults and BuildInfo (context only; not in diff).
.../​configuration/​ConfigMapper.java Reflection-based record↔YAML mapping, coercion, and defaults.
.../​configuration/​ConfigYaml.java Hand-rolled UTF-8 YAML reader/writer with quoting/escaping.
.../​configuration/​ConfigTree.java Dotted-path tree used by migration.
.../​configuration/​LimitResolver.java Merges limit groups into a resolved FaweLimit ("most generous" semantics).
.../​configuration/​{Option,Default,Comment}.java Annotations/metadata driving option paths, defaults and comments.
.../​core/​Fawe.java Rewritten setupConfigs() using Path, Settings.load/save, and BuildInfo.
.../​core/​FaweCache.java New protocolSupportFix field + setter replacing the old global setting.
.../​test/​.../​SettingsTest.java, ConfigMapperTest.java New unit tests covering save/load, migration, limits and fuzzed text round-tripping.
.../​test/​resources/​.../​config.yml Expected-output fixture mirroring the @Comment/@Default text; carries over several comment typos.
~70 migrated call sites Mechanical switch from uppercase fields to accessor methods.

Review effort: Balanced

@MattBDev
MattBDev marked this pull request as ready for review October 23, 2026 00:35
@MattBDev
MattBDev requested a review from a team as a code owner October 23, 2026 00:35
@MattBDev
MattBDev marked this pull request as draft October 23, 2026 00:35
Copilot AI and others added 2 commits October 28, 2026 20:51
Co-authored-by: MattBDev <4009945+MattBDev@users.noreply.github.com>
Co-authored-by: MattBDev <4009945+MattBDev@users.noreply.github.com>
@MattBDev

Copy link
Copy Markdown
Contributor Author

Marking the PR as ready for review but I still have to update the PR body. Should be done Saturday. I think I have a minor tweak or two to I have to push as well.

@MattBDev
MattBDev marked this pull request as ready for review November 1, 2026 18:43
@MattBDev
MattBDev requested a balanced review from Copilot November 1, 2026 18:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An unresolved critical finding remains: a typo in the config.yml comment (clipboad.on-disk instead of clipboard.use-disk) that must be mirrored in the Settings.java @Comment or the defaultsAreWrittenAsExpected test will fail.

1 open issue
3 resolved since last review

Review effort: Balanced

Co-authored-by: MattBDev <4009945+MattBDev@users.noreply.github.com>

@SirYwell SirYwell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the future updating strategy with this? E.g., if we want to rename entries etc.

@MattBDev

MattBDev commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

What is the future updating strategy with this? E.g., if we want to rename entries etc.

Migration is handled in Settings.migrate(). It is a fairly straightforward process.

…consistency.

This work isn't complete but to complete it would go outside the scope of this PR.
@MattBDev

MattBDev commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Apply the following suggested code to the Settings.java file.

private static final MutableCallSite INSTANCE =
        new MutableCallSite(MethodHandles.constant(Settings.class, defaults()));
private static final MethodHandle GET = INSTANCE.dynamicInvoker();

public static Settings settings() {
    try {
        return (Settings) GET.invokeExact();
    } catch (Throwable e) {
        throw new AssertionError(e);
    }
}

public static void set(Settings settings) {
    INSTANCE.setTarget(MethodHandles.constant(Settings.class, Objects.requireNonNull(settings)));
    // Makes other threads see the new settings
    MutableCallSite.syncAll(new MutableCallSite[] {INSTANCE});
}

Co-authored-by: MattBDev <4009945+MattBDev@users.noreply.github.com>

Copilot AI commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

@copilot Apply the following suggested code to the Settings.java file.

private static final MutableCallSite INSTANCE =

...

Applied the suggested MutableCallSite pattern in Settings.java and kept the overrideOption path consistent with the updated call-site-backed settings holder. 9ab76e9

moveIntoLimits addressed each group by a dotted path while iterating the
limits map. A group such as vip.plus created a vip section in that map,
which threw ConcurrentModificationException and made the whole config be
ignored. Groups are now written to through their own maps.
@MattBDev
MattBDev requested a review from SirYwell November 8, 2026 21:35
Comment on lines +166 to +167
INSTANCE.setTarget(MethodHandles.constant(Settings.class, updated));
MutableCallSite.syncAll(new MutableCallSite[] {INSTANCE});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably delegate to the set method?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to delegate through set(...), matching the shared update path and keeping this change minimal.

Co-authored-by: MattBDev <4009945+MattBDev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants