ADR-071 - Config Organization and Discoverability¶
Status: Proposed (top-level directory layout in rule 1 superseded by ADR-076; all other rules remain in force) Date: 2026-06-22
Context¶
RTP ships its operator-facing configuration as a split-by-concern tree of YAML
files under rtp-plugin/src/main/resources/ (English baseline) mirrored under
lang/<locale>/ for every shipped locale. As of this ADR the baseline tree is:
| File | Approx. size | Holds |
|---|---|---|
config.yml |
123 lines | teleportDelay, cancelDistance, teleportCooldown, lockAfterUses, lockAfterResetSeconds, setRespawnOnTeleport, consoleCommands, playerCommands, database, network.redis, menu |
safety.yml |
263 lines | invulnerability window, safetyRadius, anvil prefilter, emergency platform, biome/block exclusions, vertical window |
economy.yml |
41 lines | price, priceOther, paramsPrice, biomePrice, refundOnCancel, balanceFloor |
regions/default.yml |
template | world, shape, vert, range/cache caps, price, spatialResolution |
worlds/default.yml |
template | region binding, override |
effects/*.yml |
one file per effect group (default.yml, default-pre.yml, default-cancel.yml) |
per-stage effect groups (when, permission, players, inherit, effects token lists); group name = filename |
network.yml |
225 lines | network.*, transport.* (incl. transport.redis.*), heartbeat, routing, queue, reservation, load balancer |
performance.yml |
275 lines | queue / cache / pipeline tuning |
messages.yml |
614 lines | all user-facing strings (12 # Section N: headers already present) |
logging.yml / metrics.yml / language.yml |
71 / 33 / 13 lines | log routing, bStats, locale selection |
Recurring operator feedback (public thread: a user who praised performance still found the config "could use some work - there are a lot of different files and it can get confusing") identifies three distinct discoverability problems:
- No relevance ordering. Within each file, keys are grouped by internal concern, not by how often an operator touches them. The knobs changed most (range, shape, world, cooldown, cost) are not sorted to the top of their file or the in-game menu.
- Everyday knobs are mixed with advanced tuning. A first-time operator
opening
config.ymlorperformance.ymlis confronted with deep tuning (heap thresholds, pipeline allotments, audit cadences) alongside the handful of knobs they actually need to change. There is no "start here" tier and no deliberate "advanced" door. messages.ymlhas outgrown a flat file. It has 614 lines, 12# Sectionheaders, and a single ~240-constantMessagesKeysenum that is unwieldy to read, edit, and reason about as one unit.
A concrete fourth symptom motivates this ADR now: config.yml carries a
network.redis block (enabled/host/port/password) that overlaps the
authoritative network.yml surface (network.enabled, transport.redis.host
/port/password). Two files describe the same concern, which is exactly the
confusion operators report.
Any change here is not casual. The per-file YAML tree is a parity contract:
- Locale parity. Every baseline key is mirrored into every shipped locale via
the locale TSV pipeline and guarded by
LocaleParityTest. Re-ordering a file changes the TSVindexcolumn; moving a key changes its(relpath, base_key)identity. Both must round-trip throughlocale-files-to-csv->reconcile-locale-csvs->locale-files-from-csv, never by hand-editinglang/<locale>/*.yml. - Locale bootstrap (ADR-020):
language.ymlis read first and every other file is loaded fromlang/<locale>/. File renames/splits ripple through the bootstrap loader. - Comment-as-hover (ADR-064) and comment preservation (ADR-042): per-key comments are operator-facing documentation surfaced in the in-game menu; they must survive any reorganization.
- Enum-keyed parsers. Each baseline file is parsed against a Java enum
(
ConfigKeys,EconomyKeys, etc.); the parser reads keys by name, so physical key order does not affect parsing, but moving a key across files changes which enum/parser owns it.
This ADR exists to settle configuration organization as a concept and the rules any future reorganization must follow. The already-shipped in-game search/prefab surface is a near-term mitigation, not a substitute.
The project is nearing development completion and transitioning to maintenance. That timing informs the decision below: a one-time structural refactor that leaves the config tree uniform and self-similar is worth more than minimizing short-term churn, because the resulting shape is the one maintainers live with for the long tail.
Decision¶
Adopt a tiered, uniform directory configuration model. Every configuration
file - whether an everyday knob file, an advanced-tuning file, or a message
file - is the same shape: one closed Java enum, one YAML file, one
ConfigParser. Directories are organizational only; a directory is simply a
folder of ordinary single-file parsers. There is no special per-directory
loader and no merged multi-file enum.
1. Tiered directory layout¶
The baseline tree is organized into a "start here" common tier at the root, the
existing per-instance directories, and a deliberate advanced/ door:
plugins/RTP/
|-- config.yml # common - everyday teleport knobs; opens first
|-- economy.yml # common - price, perms
|-- language.yml # common - locale selection
|-- safety.yml # common - everyday safety knobs
|-- regions/ # per-region files (existing)
|-- worlds/ # per-world overrides (existing)
|-- advanced/ # opened deliberately
| |-- performance.yml
| |-- blocks.yml # airBlocks, unsafeBlocks (own list-editor pages)
| |-- biomes.yml # safety biome list, recall/weighting
| |-- network.yml # proxy / multi-server transport
| |-- database.yml # storage backend
| |-- logging.yml
| `-- metrics.yml
|-- effects/ schematics/ bundled-addons/ # unchanged
`-- messages/ # per-concern message files (see rule 3)
lang/<locale>/ mirrors the new nested paths exactly (e.g.
lang/<locale>/advanced/performance.yml, lang/<locale>/messages/player.yml).
2. Relevance ordering within each file and menu¶
Keys are ordered by common operator relevance, not alphabetically and not by
internal grouping. The high-touch knobs sort to the top of their file and their
menu page; advanced/rarely-touched settings sort below, under a divider comment.
The in-game menu mirrors the same order. The common/advanced tiering (rule 1)
is relevance ordering applied at file granularity: the knobs an operator changes
most live in the root tier, the rest behind the advanced/ door.
3. messages.yml splits into per-concern files, each with its own enum¶
messages.yml is split into a messages/ directory of per-concern files, and
the single MessagesKeys enum is split into one small enum per file so each
message file is exactly as modular and copyable as any other config file:
| File | Enum |
|---|---|
messages/placeholders.yml |
PlaceholderMessages |
messages/player.yml |
PlayerMessages |
messages/network.yml |
NetworkMessages |
messages/commands.yml |
CommandMessages |
messages/system.yml |
SystemMessages |
The same per-file-enum rule applies wherever an existing enum spans what are now separate tier files:
SafetyKeys->SafetyKeys(commonsafety.yml) +BlocksKeys(advanced/blocks.yml) +BiomesKeys(advanced/biomes.yml).- Performance knobs currently under
config.yml/ConfigKeysmove to the existingPerformanceKeys(advanced/performance.yml).
Consequences of the split that this ADR mandates:
- No merge-loader. The transitional merge-directory mode added to
ConfigParser(a single closedMessagesKeysenum unioned acrossmessages/*.yml) is retired. With per-file enums there is nothing to merge: each file is a plain single-file parser.ConfigParserreturns to its clean one-enum-one-file invariant. ConfigParserbecomes subpath-aware. A parsernamemay carry a relative subdirectory ("advanced/blocks.yml","messages/player.yml"). The JAR-resource extraction path and thelang/<locale>/<name>mirror path must tolerate the subdirectory.- Dynamic message lookups route through a small resolver. Two call patterns
cannot simply reference a single enum after the split: the dynamic
valueOf(name + "_description")lookup inBaseRTPCmd, and thelang()helper that today returns "the messages parser". Both go through a thinMessagesfacade holding aname -> (enum, parser)index built once at load. Every otherMessagesKeys.someKeyreference is re-pointed at the enum that now owns the key (a mechanical, one-time edit across the call sites).
4. Key moves require a migration path, never silent relocation¶
When a key physically moves (across files, across enums, or under a new parent),
the loader shall continue to read the legacy location, apply the value at the
new location, and emit a one-time deprecation log via
RTP.log(Level.WARNING, ...). Existing installs upgrade cleanly without operator
action. A silent relocation that strands an operator's tuned value is prohibited.
This covers both whole-file relocations (a root performance.yml superseded by
advanced/performance.yml) and per-key moves (a knob leaving config.yml for
advanced/performance.yml).
A legacy flat messages.yml present on disk is read once, its operator
customizations folded into the matching per-concern file, the file archived
(renamed, not deleted), and a one-time deprecation log emitted. No operator loses
a tuned string on upgrade.
5. First concrete action: de-duplicate network.redis¶
network.yml (now advanced/network.yml) is the authoritative network surface.
The network.redis block in config.yml is redundant and shall be removed, with
its settings served exclusively from the network file. The loader applies the
migration rule in (4): a value still present under config.yml's network.redis
is read, honored, and logged as deprecated, with the network file taking
precedence. This is the pilot that proves the migration mechanism the broader
reorg relies on.
6. Config-file version bump accompanies any structural change¶
Every baseline file that changes structurally (key order, key move, removed key,
relocation into advanced/) has its version field incremented so the
bundled-vs-on-disk version check fires and the operator is notified per the
existing update path. language.yml and the network file currently carry no
version field; a structural change to either requires adding one first.
7. The admin config editor is a recursive directory walker¶
Because every file is an ordinary single-file parser and directories are plain folders, the in-game config editor is a single recursive tree walk, identical for every directory:
- A directory node lists its child directories (
regions/,worlds/,advanced/,messages/) and its member files; selecting a child directory recurses. - A file node opens the existing per-parser key page; editing and write-back
use that parser's own
set/save- unambiguous, because each key belongs to exactly one enum/parser.
messages/ and advanced/ are handled by the same code because they are the
same structure: folders of ordinary parsers. The existing MultiConfigParser
region/world/effect selectors slot in as their own directory nodes.
Alternatives Considered¶
| Alternative | Why Rejected |
|---|---|
Keep MessagesKeys closed and serve it from messages/*.yml via a merge-loader (one enum unioned across many files) |
Shrinks the file but leaves the ~240-constant enum exactly as unwieldy as before, which is the actual maintainability complaint. It also creates a second, special code path (merge logic) that messages/ uses and the rest of the config tree does not, defeating the goal of one uniform directory primitive. With the project entering maintenance, the one-time cost of splitting the enum is acceptable in exchange for permanently uniform, self-similar, copyable config files and the deletion of the merge-loader special case. Keys are unlikely to move between well-placed files once maintenance begins, so the "moving a key is a recompile" downside is largely theoretical. |
| Keep grouping virtual only (menu/docs group, no physical tiering) | A virtual-only group co-locates the operator's mental model but leaves the everyday-vs-advanced mixing (problem 2) and the oversized message enum (problem 3) unaddressed. The tiered directory plus per-file enums fixes the structural complaints directly; relevance ordering and virtual menu grouping then layer on top. |
| Alphabetical ordering within files | Optimizes for lookup-by-name, which the in-game search surface already provides; does nothing for the actual complaint (the common knobs are buried). Relevance ordering serves the operator's real workflow. |
Flatten everything into a single config.yml |
Reintroduces the 600+ line monolith problem messages.yml already exhibits; the split-by-concern model is sound, the discoverability layer is what is missing. |
Leave config.yml's network.redis in place |
Two files describing one concern is the exact confusion operators report; keeping both perpetuates drift between the redundant block and the authoritative network file. |
| Silently relocate keys on upgrade | Strands operators' tuned values with no warning; violates the operator-trust expectation and the migration discipline in ADR-020's spirit. |
Split messages.yml using the existing MultiConfigParser (as regions/, worlds/, effects/ do) |
MultiConfigParser keys on admin-chosen outer names (one sub-parser per file, arbitrary file set) which is the right shape for region/world/effect-group files but the wrong shape for a fixed, code-referenced key set. Per-concern message files are a fixed set with disjoint key subsets, each a closed enum - ordinary single-file parsers, not a per-entry parser map. |
Consequences¶
- Positive: Every config file is the same shape (one enum, one file, one parser), so files are modular and copyable, the message enum is no longer a 240-constant monolith, and the admin editor is one recursive directory walker with no per-directory special cases. The transitional merge-loader is deleted rather than generalized.
- Positive: The common/advanced tiering gives first-time operators a "start here" surface and a deliberate advanced door, addressing the everyday-vs-tuning mixing complaint directly.
- Positive: The
network.redisde-duplication establishes a reusable read-legacy-warn-prefer-new migration pattern for every future key/file move. - Negative / Trade-offs: Splitting
MessagesKeys(andSafetyKeys) into per-file enums is a one-time, large-blast-radius refactor: everyMessagesKeys.someKeyreference and everygetParser(MessagesKeys.class)lookup acrossrtp-core, the platform adapters, and tests is re-pointed at the owning enum, and the dynamic_description/lang()lookups move behind a smallMessagesresolver. Accepted given the maintenance-phase timing. - Negative / Trade-offs: Moving a key between files later becomes a code change (the constant moves to another enum) rather than a YAML edit. Acceptable because well-placed keys are not expected to move once maintenance begins.
- Negative / Trade-offs: The directory split multiplies the locale surface
(per-concern message files and the
advanced/tier, each with its own synthesized.lang.yml, across every shipped locale). The locale/TSV-pipeline migration - mirroring the nested paths and round-tripping every locale - is the dominant cost of the change; the Java refactor is mechanical by comparison. The pipeline scripts andLocaleParityTestmust understand theadvanced/andmessages/sub-trees.
References¶
- ROADMAP Tier 2: "Config-file organization + discoverability (operator feedback)".
- ADR-020 Language bootstrap and locale-aware ConfigParser.
- ADR-042 YAML comment preservation.
- ADR-064 Config-comment format: summary line as menu hover text;
docs/dev/CONFIG_COMMENT_STYLE.md. - ADR-066 Foreign config importer (precedent for non-destructive config migration).
- Locale Config TSV Pipeline in
.junie/AGENTS.md;LocaleParityTestinrtp-plugin. rtp-plugin/src/main/resources/:config.yml,network.yml,safety.yml,economy.yml,messages/.