Skip to content

[NeoForge 1.21.1] 2.3.7: NPE on a config key containing a dot (path split) #9

Description

@SpaceOddityTr

Environment: Jupiter 2.3.7, Minecraft 1.21.1, NeoForge 21.1.248, Ice and Fire: Community Edition (which is what pulls Jupiter in), Wither: Reincarnated 1.1.0. Client and dedicated server alike; the import runs on both.

What happens

With loadForgeConfigs: true (the default in config/jupiter.json), ConfigSpecLoader.scanConfig walks other mods' NeoForge ModConfigSpec files. For every entry whose key string contains a ., NightConfigHolder.processEntry receives a null value and throws. The entry is then missing from Jupiter's config screen, and each failure prints a stack trace at startup.

This matters beyond the one mod below: Jupiter arrives as a dependency of Ice and Fire: Community Edition, so it is present in packs that never installed it deliberately, and it then tries to import the config of every other mod in the pack. Any single mod with a dot in a config key produces the errors.

Expected

A key that happens to contain a dot is a legal NeoForge config key - NeoForge stores the path verbatim when the mod passes a single-element List - so Jupiter should import it like any other key, and at worst skip it quietly rather than throwing.

Steps to reproduce

  1. Install Jupiter 2.3.7 on NeoForge 1.21.1 with loadForgeConfigs: true.
  2. Add any mod that registers a spec with builder.define(List.of("<key with a dot>"), <default>). A live example is Wither: Reincarnated 1.1.0, which embeds the default value in the key name.
  3. Start the game and watch the log during config import.
"Changes Time (default: true)" = true         # loads fine - no dot
"Max Laser Length (default: 128)" = 128       # loads fine - no dot
"Health (default: 600.0)" = 600.0             # NPE - the dot in "600.0"
"Movement Speed (default: 0.0675)" = 0.0675   # NPE

Exactly the eleven entries in config/witherreincarnated/common.toml whose default is a decimal number fail; every integer, boolean and string entry in the same file loads.

Evidence

[main/ERROR] [com.iafenvoy.jupiter.Jupiter/]: Cannot load key=Health (default: 600.0), type=java.lang.Double in config=witherreincarnated:COMMON
java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "value" is null
    at com.iafenvoy.jupiter.compat.forgeconfigspec.NightConfigHolder.processEntry(NightConfigHolder.java:157)
    at com.iafenvoy.jupiter.compat.forgeconfigspec.NightConfigHolder.process(NightConfigHolder.java:126)
    at com.iafenvoy.jupiter.compat.forgeconfigspec.NightConfigHolder.buildGroup(NightConfigHolder.java:98)
    at com.iafenvoy.jupiter.compat.forgeconfigspec.NightConfigHolder.buildGroups(NightConfigHolder.java:87)
    ...
    at com.iafenvoy.jupiter.compat.ExtraConfigManager.scanConfigs(ExtraConfigManager.java:29)

The correlation is with the dot, not with the value type: the failing entries are exactly those whose key text contains ., and the exception is a null value rather than a type mismatch. Config.get(String) and getRaw(String) in NightConfig treat . as a path separator, so "Health (default: 600.0)" is split into "Health (default: 600" -> "0)" and resolves to nothing. NeoForge itself is unaffected because the mod passes a single-element List path, which is stored verbatim.

In my pack that is eleven ERROR lines on every start. The mod itself reads its own config correctly; only Jupiter's screen loses those entries. Nothing else in a set of 324 mods triggers it.

Suggested fix

The iteration in processEntry already holds the UnmodifiableConfig.Entry, so the value can be taken from it instead of being looked up again by string:

Object value = entry.getRawValue();          // or entry.getValue()

Where a lookup is unavoidable, use the single-element path form so no splitting happens:

Object value = config.get(List.of(key));
config.set(List.of(key), newValue);

The same applies to the write-back path - otherwise saving from Jupiter's screen would create a nested Health (default: 600 table instead of updating the entry.

A null check before value.getClass() would additionally downgrade this from a stack trace to a skipped entry, which is worth having regardless of the path handling.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions