From b046f44ab5074ada0ab4674f770b97ea0493e323 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Thu, 23 Apr 2026 00:41:36 +0200 Subject: [PATCH] feat(02-01): add GravityFlipRegion POJO + BuilderCodec with round-trip tests - GravityFlipRegion holds Name (String) + Box (Hytale AABB) + Enabled (boolean), serialised through a BuilderCodec keyed on "Name"/"Box"/"Enabled". - Validators.nonNull() applied to Name and Box (correct package is com.hypixel.hytale.codec.validation.Validators, not codec.builder). - Server jar 2026.03.26 uses com.hypixel.hytale.math.vector.Vector3d for Box.min/max (NOT org.joml.Vector3d, which only appeared in newer builds). - 3 JUnit 5 round-trip tests cover name/box/enabled, enabled=false, and empty name. - testImplementation added for the Server artifact so codec encode/decode is reachable from unit tests. --- build.gradle.kts | 2 + .../gravityflip/region/GravityFlipRegion.java | 68 ++++++++++++++++++ .../region/GravityFlipRegionCodecTest.java | 72 +++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java create mode 100644 src/test/java/com/mythlane/gravityflip/region/GravityFlipRegionCodecTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 95521fb..67346ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation("com.google.code.gson:gson:2.10.1") implementation("org.jetbrains:annotations:24.1.0") + // Tests need the Hytale Server API (codecs, ExtraInfo, BsonValue) on the classpath. + testImplementation("com.hypixel.hytale:Server:$hytaleServerVersion") testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java b/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java new file mode 100644 index 0000000..fa99eba --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java @@ -0,0 +1,68 @@ +package com.mythlane.gravityflip.region; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.validation.Validators; +import com.hypixel.hytale.math.shape.Box; +import com.hypixel.hytale.math.vector.Vector3d; + +/** + * A named axis-aligned region in which gravity is inverted for any entity inside. + * + *

Persisted as part of {@code GravityFlipConfig} via {@link #CODEC}. The region + * is stored on disk as three keys: + *

+ * + *

Note: this build pins {@code com.hypixel.hytale:Server:2026.03.26-89796e57b}, in which + * {@code Box.min}/{@code Box.max} are {@code com.hypixel.hytale.math.vector.Vector3d} + * (Hytale's own type — NOT {@code org.joml.Vector3d}, which only appeared in later builds). + * + *

Fields are package-private mutable so the codec can write into them directly, + * mirroring the canonical {@code MythLoggerConfig} pattern. Public getters/setters + * are provided for runtime callers (Phase 4 commands). + */ +public final class GravityFlipRegion { + + public static final BuilderCodec CODEC = + BuilderCodec.builder(GravityFlipRegion.class, GravityFlipRegion::new) + .append(new KeyedCodec<>("Name", Codec.STRING), + (r, v) -> r.name = v, r -> r.name) + .addValidator(Validators.nonNull()).add() + .append(new KeyedCodec<>("Box", Box.CODEC), + (r, v) -> r.box = v, r -> r.box) + .addValidator(Validators.nonNull()).add() + .append(new KeyedCodec<>("Enabled", Codec.BOOLEAN), + (r, v) -> r.enabled = v, r -> r.enabled).add() + .build(); + + // Package-private mutable fields written directly by the codec setters. + String name = ""; + Box box = new Box(new Vector3d(), new Vector3d()); + boolean enabled = true; + + public GravityFlipRegion() {} + + public GravityFlipRegion(String name, Box box, boolean enabled) { + this.name = name; + this.box = box; + this.enabled = enabled; + } + + public String getName() { return name; } + public Box getBox() { return box; } + public Vector3d getMin() { return box.min; } + public Vector3d getMax() { return box.max; } + public boolean isEnabled() { return enabled; } + + public void setName(String n) { this.name = n; } + public void setBox(Box b) { this.box = b; } + public void setEnabled(boolean v) { this.enabled = v; } + + /** Convenience accessor for tick-loop / physics consumers in Phase 02-02. */ + public Box asBox() { return box; } +} diff --git a/src/test/java/com/mythlane/gravityflip/region/GravityFlipRegionCodecTest.java b/src/test/java/com/mythlane/gravityflip/region/GravityFlipRegionCodecTest.java new file mode 100644 index 0000000..68c5c4e --- /dev/null +++ b/src/test/java/com/mythlane/gravityflip/region/GravityFlipRegionCodecTest.java @@ -0,0 +1,72 @@ +package com.mythlane.gravityflip.region; + +import com.hypixel.hytale.codec.ExtraInfo; +import com.hypixel.hytale.math.shape.Box; +import org.bson.BsonValue; +import com.hypixel.hytale.math.vector.Vector3d; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Round-trip tests for {@link GravityFlipRegion#CODEC}. Verifies the codec preserves + * Name + Box + Enabled fields across encode -> decode cycles via the BSON intermediate + * representation that all Hytale codecs share. + */ +class GravityFlipRegionCodecTest { + + @Test + void roundTripPreservesNameBoxEnabled() { + GravityFlipRegion src = new GravityFlipRegion( + "zone1", + new Box(new Vector3d(1, 2, 3), new Vector3d(4, 5, 6)), + true); + + GravityFlipRegion decoded = roundTrip(src); + + assertEquals("zone1", decoded.getName()); + assertNotNull(decoded.getBox()); + assertEquals(1.0, decoded.getMin().x); + assertEquals(2.0, decoded.getMin().y); + assertEquals(3.0, decoded.getMin().z); + assertEquals(4.0, decoded.getMax().x); + assertEquals(5.0, decoded.getMax().y); + assertEquals(6.0, decoded.getMax().z); + assertTrue(decoded.isEnabled()); + } + + @Test + void roundTripPreservesEnabledFalse() { + GravityFlipRegion src = new GravityFlipRegion( + "off-zone", + new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), + false); + + GravityFlipRegion decoded = roundTrip(src); + + assertEquals("off-zone", decoded.getName()); + assertFalse(decoded.isEnabled(), "default-true field must not clobber loaded false"); + } + + @Test + void roundTripPreservesEmptyName() { + GravityFlipRegion src = new GravityFlipRegion( + "", + new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), + true); + + GravityFlipRegion decoded = roundTrip(src); + + assertNotNull(decoded.getName()); + assertEquals("", decoded.getName(), "empty name must survive round-trip without substitution"); + } + + private static GravityFlipRegion roundTrip(GravityFlipRegion src) { + ExtraInfo info = new ExtraInfo(); + BsonValue encoded = GravityFlipRegion.CODEC.encode(src, info); + return GravityFlipRegion.CODEC.decode(encoded, info); + } +}