Skip to content

Move the human-editable configs to hconf - #552

Open
Skidamek wants to merge 12 commits into
mainfrom
hconf-configs
Open

Skidamek wants to merge 12 commits into
mainfrom
hconf-configs

Conversation

@Skidamek

@Skidamek Skidamek commented Sep 20, 2026

Copy link
Copy Markdown
Owner

The server and client configs move from JSON to hconf, a small configuration format written for this mod, with an editor that changes config files in place instead of regenerating them. If you run a server or play with the mod, you do not need to do anything: your current config keeps working, and the mod moves it to the new format on the first save.

What was wrong with JSON

The old save path serialized the whole config object back to disk on every save:

  • Comments were impossible.
  • Formatting, key order and blank lines were regenerated.
  • Any line the mod did not recognize was dropped.
  • Deleting a key achieved nothing. It came back with its default value on the next boot.

Users learned not to touch their own config. That is backwards for the one file they are supposed to edit.

The format

# AutoModpack configuration - your edits and comments survive updates

# The name players see in the modpack UI.
modpackName: "My Server Pack"

# Cap on per-client transfer speed in MiB/s; 0 disables limiting.
bandwidthLimit: 0

modpack: {
  # a category, shown to players
  General: {
    # a group id, referenced by requires and breaksWith
    main: {
      syncedFiles: ["mods/*.jar", "kubejs/**"]
      excludedFiles: ["kubejs/server_scripts/**"]
    }
  }
}

Keys and values usually need no quotes. # starts a comment, to the end of the line. Braces nest, one entry per line. A JSON file with an object at the top is already a valid hconf file, so existing JSON configs parse as they are.

The parser is strict. A typo fails with the line, the column and the reason:

Invalid configuration server-config.hconf at line 1:17 (ADJACENT_VALUES):
two key tokens with no separator - hconf writes 'key: value'

What a save does now

The mod parses the current file, applies its changes to the document, and writes the result. Bytes outside a changed value are untouched:

# before: you added a note, a comment after the value, and commented a key out
# my note about the port
bindPort: 25565           # keep it low, my host is shared
# acceptProxyProtocol: true

# after: the mod changed the port and saved

# my note about the port
bindPort: 25566           # keep it low, my host is shared
# acceptProxyProtocol: true

Comments, key order, layout and line endings (LF, CRLF, CR) survive every save. Unknown keys and hand-added entries survive. One rule to know: deleting a documented key's line brings it back with its default value on the next boot. To disable a setting permanently, comment the line out instead; it stays off, and uncommenting it turns it back on.

Files

server-config.json becomes server-config.hconf, and client-config.json becomes client-config.hconf. Reads fall back to the old .json name, and the first save writes the .hconf file and removes the old one. If you have scripts or volume mounts that point at the old filenames, point them at the new ones. A corrupt config is never regenerated: the mod fails loudly, names the line and column, and expects a human to fix or remove the file.

For developers

  • Config fields documented with @Comment get that comment on fresh generation and are materialized into old files that lack them. Other fields never reappear on their own.
  • Saves are reconcile-based: scalars and objects are edited in place. Arrays are written from the model, because the model is read from the same file, so an element missing from the model is a deliberate removal, such as a normalized-away rule or an unpinned mod.
  • The library ships as a zero-dependency jar under libs/. It has its own parser, writer and edit operations.

Testing

  • HconfConfigsTest: fresh generation, legacy JSON reads, byte-preserving saves, idempotence, corrupt-file failures, tombstones staying out, migration on first save, rule removal.
  • Autotester scenarios download-only on 1.21.11-fabric and installed-group-acquisition on 1.20.1-forge pass.

The server and client configs now parse through hconf and save by
reconciliation: user comments, blank lines, layout and line endings survive
every programmatic write, new options materialize with their documentation
via ensure, and a corrupt file fails the save loudly instead of being
overwritten. Historical Gson-written JSON parses unchanged, and the state
documents stay on Gson. hconf ships as a zero-dependency jar shadowed into
the loader (amp_libs relocation); the Gson-tree bridge lives in
HconfConfigs with @comment field declarations driving generated defaults.

Validated: 539 core tests, format and build gates, and autotester
installed-group-acquisition + download-only scenarios on 1.20.1-forge.
(The group-acquisition restart-nudge step fails identically on unmodified
main - pre-existing, unrelated to this change.)
The format is deliberately not HOCON, so .conf would mislead users and
editors; .hconf is self-identifying and gives our own grammar a clean home.
Reads fall back to the legacy .json when the .hconf does not exist yet, and
the first automatic save migrates: a fresh documented file is generated with
the old values carried over, and the .json is removed only after the new one
is written. The autotester seeds legacy .json configs on purpose now - every
scenario exercises the fallback and the migration - and reads live configs
through a minimal hconf reader. Spec status line updated to recommend .hconf.

Validated: 10/10 HconfConfigs tests, 539/539 core tests, format and build
gates, and the installed-group-acquisition + download-only scenarios pass on
1.20.1-forge with the migrated config in place.
The library's comment declarations moved out of the tree into a Comments
value carried beside it, so fresh generation passes the banner and the
per-field comments to canonical form directly. The jar now comes from
the tree-based editor rewrite; behavior and public surface are otherwise
the same for this bridge.
Pulls in the tombstone-scope, move-classification, and BOM fixes from the
differential fuzzing pass on the concrete-syntax-tree architecture.
The vendored jar picks up the hconf queue closure: reconcile is
insert-only over user array content, the migration verbs fail before a
partial application, and tombstone scope excludes nested blocks.

The javadoc on readState now states the split the code already had:
machine-written rebuildable state may set a torn file aside (a torn
repair journal must never block a boot), while user-owned configs take
the strict read path that fails with a position and never regenerates.
Reconcile is insert-only, and this save path's model is read from the
very file being saved - so an element missing from the model is a
deliberate removal (a normalized-away rule, a pin removed in the UI),
and leaving it standing resurrected it on the next read. The save now
sets array members from the model after reconciling; object members
still recurse and user formatting outside arrays stands.
The vendored jar picks up the consolidation: one removal rule, one
literal classifier, one key spelling, one escape table, move dispatched
from its case table. No API change; the hconf suite and soaks are
byte-identical before and after.
hconf is our library, not a third-party dependency: it now ships under
its own package in the loader and merged jars, the same handling the
mcholepunch jars get. The amp_libs relocation applied only to it and
nothing referenced the relocated names.
The library now follows the standard module naming and versions like the
mcholepunch jars. Imports in HconfConfigs follow the rename; hconf.Path
stays fully qualified because of the java.nio.file.Path import.
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.

1 participant