Feature Carriage Weighting - bh679/dungeon-train-mc GitHub Wiki

Weighted carriage selection

v0.37.0 โ€” PR #48 โ€” per-variant integer weights bias the seeded random pick in RANDOM + RANDOM_GROUPED modes, /dungeontrain editor weight in-game subcommand writing config/dungeontrain/weights.json, weight = N line on the editor status HUD with orange "excluded" tint when zero. v0.45.0 โ€” PR #58 โ€” adds inc / dec subcommands (/dt editor weight <variant> inc|dec) and a [-] Weight (N) [+] row to the in-world editor menu that nudges by ยฑ1 while keeping the menu open, so authors can balance weights live without re-typing the full set command. v0.51.0 โ€” PR #65 โ€” the same [-] Weight (N) [+] menu row now appears for Tracks (see Feature-Track-Variants) and Carriage-Contents (see Feature-Carriage-Contents ยง Weighted contents pick (v0.51.0)). The contents pick is now weighted in the same shape as carriages โ€” same MIN/MAX/DEFAULT contract, weight 0 excludes a contents from the spawn pool. New CLI verbs: /dt editor tracks weight <kind> <name> ... and /dt editor contents weight <id> .... v0.58.0 โ€” PR #73 โ€” in-game weight tweaks now auto-promote to the source tree alongside the config-dir write when running from a checkout (./gradlew runClient). Closes the silent-loss gap where dev-mode tuning was lost on commit unless weights.json was hand-copied. Production installs unaffected โ€” the auto-write is a no-op when the source tree is unreachable. See Development ยง Authoring weight tweaks that ship with the mod.

Before v0.37.0, the carriage variant at index i was a uniform seeded pick across every registered variant โ€” adding a custom to the registry diluted every other variant equally and there was no way to say "standard twice as often" or "never flatbed in the random pool". Weighting keeps the selection deterministic (same (seed, index, weights) โ†’ same variant) while letting you bias the distribution per-variant. Weight 0 excludes a variant; the default weight (1) matches the pre-feature uniform behaviour exactly, so worlds saved on 0.36.x with no weights.json are unchanged.

Usage

Edit weights in-game

/dungeontrain editor weight <variant> <n>     # 0..100; 0 excludes the variant, 1 is the default
/dungeontrain editor weight <variant> inc     # +1 (clamped to 100)   โ€” v0.45.0
/dungeontrain editor weight <variant> dec     # -1 (clamped to 0)     โ€” v0.45.0

Tab-completes against every registered carriage id (built-ins + customs). The subcommand writes the full weights map to config/dungeontrain/weights.json (keys sorted, pretty-printed) and reloads the in-memory map immediately โ€” no restart needed. Existing carriages in the rolling window keep their variant until they scroll out; the new mix appears as you ride forward.

Nudge by 1 (v0.45.0)

The inc / dec literals are siblings of the integer-arg setter โ€” Brigadier dispatches the literal first, so all three forms coexist without ambiguity. They read the current value via CarriageWeights.current().weightFor(<variant>), apply the delta, and route through the same runWeightSet path (clamp, persist, success-message, JSON write). Saturating at 0 / 100 is a silent no-op.

The in-world editor menu renders these as a [-] Weight (N) [+] triple row when the player is standing in a carriage plot โ€” side cells run inc / dec and keep the menu open so the author can spam-click while watching the HUD weight = N line update live.

/dungeontrain editor list                   # now shows weight=N per variant

Edit the JSON directly

// config/dungeontrain/weights.json
{
  "standard":   5,
  "windowed":   0,
  "open":       3,
  "flatbed":    1
}
  • Non-negative integers, clamped to [0, 100] on load.
  • Missing entries default to 1 โ€” no need to list every variant.
  • Unknown ids (no registered variant) are kept in the map silently with a warn log, so you can pre-register weights for customs that aren't installed yet.
  • Malformed entries (non-numeric, NaN) fall back to 1 with a warn log.

After a file edit, restart the server (close โ†’ reopen the world) so ServerStartingEvent reloads the overlay. In-game command edits don't require a restart.

Editor HUD

While standing in any carriage plot (built-in or custom), the centred status bar at the top shows:

Editor: Carriages / <variant>
weight = 1
  • The second line only renders for carriage models (pillars, tunnels, track omit it โ€” weight isn't meaningful there).
  • Weight 0 renders in orange as a visual cue for "excluded from the random pool".
  • Updates live on /editor weight <variant> <n> because the server includes the weight in the status-packet dedup key.

How the weighted pick is computed

All modes remain pure functions of (seed, index, registry state, weights state) โ€” no Random state escapes the call, so walking back along the track re-places the identical carriages so long as nothing mutates.

RANDOM mode

// CarriageTemplate.weightedSeededPick
int total = 0;
int[] cumulative = new int[pool.size()];
for (int i = 0; i < pool.size(); i++) {
    total += weights.weightFor(pool.get(i).id());   // clamped to [0, 100]
    cumulative[i] = total;
}
long mixed = seed ^ ((long) index * 0x9E3779B97F4A7C15L);
int r = new Random(mixed).nextInt(total);
// linear scan: return pool[i] where r < cumulative[i]

The (seed, index) mixing is the same golden-ratio constant used by the unweighted seeded pick, so with every weight at the default 1, total == pool.size() and Random.nextInt(total) produces an identical output sequence โ€” bit-for-bit backward compatibility, pinned by CarriageTemplateTest#weighted_emptyWeightsMatchUniformSequence.

RANDOM_GROUPED mode

The separator slot (every groupSize + 1th index) stays fixed flatbed regardless of weight โ€” the grouped mode exists specifically for that visual rhythm, and letting weights override it would break the point. Only the group slots pick from the weighted non-flatbed pool.

Setting "flatbed": 0 in the weights map excludes flatbed from the RANDOM pool but has no effect on the RANDOM_GROUPED separator slot โ€” the separator is always flatbed. This is documented in CarriageTemplateTest#weighted_groupedSeparatorIgnoresFlatbedWeight.

All-zero fallback

If every variant in the pool has weight 0, the selector falls back to a uniform pick across the same pool and emits one warn log per server session via CarriageTemplate.warnAllZeroOnce. Prevents an empty train from a bad weights.json.

LOOPING mode

Unchanged. Cycles through CarriageVariantRegistry.allVariants() in order and ignores weights entirely โ€” the deterministic cycle doesn't apply to a random distribution.

Loader โ€” two-tier overlay

Mirrors the CarriageTemplateStore pattern, minus the hardcoded fallback tier:

Tier Location Who writes here
1 โ€” per-install override config/dungeontrain/weights.json /dungeontrain editor weight, hand-edits
2 โ€” bundled default /data/dungeontrain/templates/weights.json (inside the jar) The mod build, sourced from src/main/resources/data/dungeontrain/templates/weights.json (see below)

Config-dir entries override bundled entries for the same id. Missing files at either tier are silently accepted โ€” both are optional. Loaded on ServerStartingEvent, cleared on ServerStoppedEvent.

Since v0.58.0: when the mod is running from a checkout (./gradlew runClient), every /editor weight write also lands in src/main/resources/data/dungeontrain/.../weights.json so dev-mode tuning ships with the next build. The runtime loader is still two-tier โ€” the source-tree write only changes what gets bundled at the next ./gradlew build. See Development ยง Authoring weight tweaks that ship with the mod.

Editor status HUD โ€” data flow

The weight line on the HUD is pushed from server to client via the existing editor status packet, now carrying an extra int weight field:

VariantOverlayRenderer.updateEditorStatus  (per player, per server tick)
  โ†’ resolve (category, model) via EditorCategory.locate
  โ†’ if model is CarriageModel: weight = CarriageWeights.current().weightFor(variant.id())
  โ†’ key = "<category>|<model>|<devmode>|<weight>"
  โ†’ if key changed since last push: send EditorStatusPacket(category, model, devmode, weight)

EditorStatusPacket.handle  (client)
  โ†’ EditorStatusHudOverlay.setStatus(category, model, devmode, weight)

EditorStatusHudOverlay (client, per frame)
  โ†’ drawBar renders "Editor: <category> / <model>"
  โ†’ when weight >= 0: second centred line "weight = N" (orange if 0, else muted white)

Including the weight in the dedup key is what makes /editor weight <n> push an immediate update โ€” the next tick after the command sees a changed key and fires a packet. Without it the HUD would only refresh when the player left and re-entered the plot.

EditorStatusPacket.NO_WEIGHT (= -1) is the sentinel for "weight not applicable to this model" โ€” pillars, tunnels, track. The client draws the weight line only when weight >= 0.

Backward compatibility

  • Existing worlds on 0.36.x open on 0.37.0 unchanged. No weights.json = all defaults = same uniform variant sequence for the same (seed, index). Pinned by weighted_emptyWeightsMatchUniformSequence in the test suite.
  • Multiplayer โ€” the EditorStatusPacket wire format grew a writeVarInt field. Client and server must both be on 0.37.0+, which matches standard Minecraft mod practice.
  • File layout โ€” config/dungeontrain/weights.json is new and optional. Removing it reverts to defaults; the mod does not recreate it unless you run /editor weight.

Files

Known limitations

  • Only RANDOM and RANDOM_GROUPED. LOOPING mode ignores weights by design โ€” its whole purpose is a deterministic cycle through the registry.
  • Integer weights. [0, 100] range. Floats would let 0.5 mean "half as often" but would risk seeded-pick drift across JDK versions; integers keep the distribution byte-stable.
  • Weight drift mid-session. Editing weights.json while a train is rolling replaces the in-memory map on the next ServerStartingEvent (or on /editor weight). The rolling-window erase + redraw produces the new mix only for carriages that scroll in after the change; carriages already placed keep their old variant until they scroll out.
  • Unknown ids are kept silently. A weights.json with {"ghost": 50} and no registered ghost variant logs a warn and keeps the entry in the map โ€” useful for pre-registering customs, but can mask a typo. Check /editor list to see which ids are actually registered.
โš ๏ธ **GitHub.com Fallback** โš ๏ธ