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
@@ -0,0 +1,92 @@
package com.mythlane.gravityflip.config;
import com.hypixel.hytale.codec.ExtraInfo;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector3d;
import com.mythlane.gravityflip.region.GravityFlipRegion;
import org.bson.BsonValue;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Round-trip tests for {@link GravityFlipConfig#CODEC}. Critical guarantees:
* <ul>
* <li>Regions list is always non-null (empty by default).</li>
* <li>List elements survive encode -> decode in order.</li>
* <li>Decoded list is MUTABLE — Phase 4 command handlers depend on this.
* Guard against an accidental {@code List.of(arr)} regression.</li>
* </ul>
*/
class GravityFlipConfigCodecTest {
@Test
void newConfigHasNonNullEmptyRegions() {
GravityFlipConfig cfg = new GravityFlipConfig();
assertNotNull(cfg.getRegions());
assertEquals(0, cfg.getRegions().size());
}
@Test
void roundTripPreservesTwoRegionsInOrder() {
GravityFlipConfig src = new GravityFlipConfig();
src.getRegions().add(region("alpha", 0, 0, 0, 1, 1, 1));
src.getRegions().add(region("beta", 5, 5, 5, 9, 9, 9));
GravityFlipConfig decoded = roundTrip(src);
assertEquals(2, decoded.getRegions().size());
assertEquals("alpha", decoded.getRegions().get(0).getName());
assertEquals("beta", decoded.getRegions().get(1).getName());
}
@Test
void roundTripOfEmptyListYieldsNonNullEmptyList() {
GravityFlipConfig src = new GravityFlipConfig();
// src.regions is the default empty ArrayList.
GravityFlipConfig decoded = roundTrip(src);
assertNotNull(decoded.getRegions(), "decoded regions list must never be null");
assertEquals(0, decoded.getRegions().size());
}
@Test
void decodedListIsMutable() {
GravityFlipConfig src = new GravityFlipConfig();
src.getRegions().add(region("seed", 0, 0, 0, 1, 1, 1));
GravityFlipConfig decoded = roundTrip(src);
// CRITICAL: must not throw UnsupportedOperationException.
// Phase 4 commands (define / delete / toggle) all mutate this list.
assertDoesNotThrow(() -> decoded.getRegions().add(region("added", 2, 2, 2, 3, 3, 3)));
assertDoesNotThrow(() -> decoded.getRegions().remove(0));
assertTrue(decoded.getRegions() instanceof ArrayList,
"decoded regions list should be a mutable ArrayList, not List.of(...)");
}
private static GravityFlipRegion region(String name,
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ) {
return new GravityFlipRegion(
name,
new Box(new Vector3d(minX, minY, minZ), new Vector3d(maxX, maxY, maxZ)),
true);
}
private static GravityFlipConfig roundTrip(GravityFlipConfig src) {
ExtraInfo info = new ExtraInfo();
BsonValue encoded = GravityFlipConfig.CODEC.encode(src, info);
return GravityFlipConfig.CODEC.decode(encoded, info);
}
// Suppress unused-import warning if List is not directly referenced in any final assertion.
@SuppressWarnings("unused")
private static List<GravityFlipRegion> typeAnchor() { return null; }
}