feat(02-01): wire GravityFlipConfig to regions.json via named withConfig

- GravityFlipConfig wraps List<GravityFlipRegion> via ArrayCodec under the
  KeyedCodec("Regions", ...) entry. Decoded list is a mutable ArrayList
  (NOT List.of(arr)) so Phase 4 commands can mutate at runtime.
- GravityFlipPlugin.configHolder uses the named overload
  withConfig("regions", GravityFlipConfig.CODEC) — produces
  <dataDirectory>/regions.json. The 1-arg overload would hardcode config.json.
- configHolder() javadoc documents the save contract (no auto-save on
  shutdown; mutators must call configHolder.save() explicitly) and the shared
  mutable reference caveat that 02-02's RegionRegistry will resolve.
- 4 round-trip tests cover: empty default, two-region order preservation,
  empty list, and list mutability (regression guard against List.of).
This commit is contained in:
2026-04-23 00:43:26 +02:00
parent b046f44ab5
commit 216f544d9b
3 changed files with 177 additions and 5 deletions
@@ -2,6 +2,8 @@ package com.mythlane.gravityflip;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.util.Config;
import com.mythlane.gravityflip.config.GravityFlipConfig;
import java.util.logging.Level;
@@ -9,25 +11,53 @@ import java.util.logging.Level;
* Entry point for the Gravity Flip plugin.
*
* <p>Extends {@link JavaPlugin} from the resolved Hytale Server API
* ({@code com.hypixel.hytale:Server:2026.03.26-89796e57b}). The lifecycle
* hooks in this API version are {@code setup()} and {@code shutdown()}
* (NOT {@code onEnable()} / {@code onDisable()}, which belong to older
* docs). See {@code .planning/phases/01-scaffold-load/JAVAPLUGIN_RESOLUTION.md}
* for the empirical resolution.
* ({@code com.hypixel.hytale:Server:2026.03.26-89796e57b}). Lifecycle hooks
* for this version are {@code setup()} / {@code shutdown()} (not the legacy
* {@code onEnable()} / {@code onDisable()}). See
* {@code .planning/phases/01-scaffold-load/JAVAPLUGIN_RESOLUTION.md} for the
* empirical resolution.
*/
public class GravityFlipPlugin extends JavaPlugin {
/**
* Persisted region store, materialised on disk as
* {@code <dataDirectory>/regions.json} (i.e. {@code Plugins/GravityFlip/regions.json}).
*
* <p>The named {@code withConfig(name, codec)} overload is REQUIRED — the
* 1-arg overload hardcodes the filename to {@code config.json}.
*/
private final Config<GravityFlipConfig> configHolder =
withConfig("regions", GravityFlipConfig.CODEC);
public GravityFlipPlugin(JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
// NOTE: do NOT call configHolder.get() here — it blocks until preLoad() completes.
// Safe call sites are start() and any later lifecycle phase (incl. tick loop).
getLogger().at(Level.INFO).log("Gravity Flip enabled");
}
@Override
protected void shutdown() {
// No auto-save contract: any mutation made during the session must already
// have been persisted via configHolder().save() by the command handler that
// performed it. See configHolder() javadoc.
getLogger().at(Level.INFO).log("Gravity Flip disabled");
}
/**
* Accessor for the region config holder. <strong>SAVE CONTRACT:</strong> any
* caller that mutates {@code configHolder().get().getRegions()} MUST call
* {@code configHolder().save()} afterwards. There is no lifecycle hook that
* auto-saves on shutdown. {@code Config.get()} returns a SHARED MUTABLE
* reference; concurrent writers corrupt state — Phase 02-02's
* {@code RegionRegistry} snapshots it into an {@code AtomicReference} for
* tick-loop reads.
*/
public Config<GravityFlipConfig> configHolder() {
return configHolder;
}
}
@@ -0,0 +1,50 @@
package com.mythlane.gravityflip.config;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import com.mythlane.gravityflip.region.GravityFlipRegion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Root config wrapping the persisted list of {@link GravityFlipRegion}s.
*
* <p>Persisted as {@code <dataDirectory>/regions.json} via the named
* {@code withConfig("regions", GravityFlipConfig.CODEC)} overload on
* {@code PluginBase}. The 1-arg overload would hardcode {@code config.json} —
* deliberately avoided.
*
* <p>The decoded list is wrapped in a <em>mutable</em> {@code ArrayList}
* (NOT {@code List.of(arr)}, which is immutable) so Phase 4 command handlers
* can {@code add}/{@code remove} regions at runtime. Callers that mutate the
* list MUST call {@code Config.save()} afterwards — there is no auto-save
* lifecycle hook.
*/
public final class GravityFlipConfig {
public static final BuilderCodec<GravityFlipConfig> CODEC =
BuilderCodec.builder(GravityFlipConfig.class, GravityFlipConfig::new)
.append(
new KeyedCodec<>("Regions",
new ArrayCodec<>(GravityFlipRegion.CODEC, GravityFlipRegion[]::new)),
// Decode setter: wrap in a MUTABLE ArrayList so commands can add/remove.
(c, arr) -> c.regions = new ArrayList<>(Arrays.asList(arr)),
c -> c.regions.toArray(GravityFlipRegion[]::new)
).add()
.build();
private List<GravityFlipRegion> regions = new ArrayList<>();
public GravityFlipConfig() {}
public List<GravityFlipRegion> getRegions() {
return regions;
}
public void setRegions(List<GravityFlipRegion> r) {
this.regions = (r == null) ? new ArrayList<>() : r;
}
}