From 65d0cd00909c64c8663c022d57888d808a5ff103 Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 4 Sep 2026 14:12:34 +0200 Subject: [PATCH] fix: preserve equality when scheduling part ticks --- JAVA_MIGRATION_HANDOFF.md | 9 +- docs/migration/HISTORY.md | 11 +++ .../codechicken/multipart/TickScheduler.java | 3 +- .../TickSchedulerEqualityRegressionTest.java | 90 +++++++++++++++++++ 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/test/java/codechicken/multipart/TickSchedulerEqualityRegressionTest.java diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index 7e2d5bc..78d5e90 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -16,8 +16,13 @@ migration checkout; `codex/tile-compatibility-fixes` was deleted after its fixes ## Current state and next target -**392 plain-JVM tests and 235 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java -files and 9 Scala files / 782 nonblank Scala lines**. The packaged inventory has 444 classes. +**398 plain-JVM tests and 237 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java +files and 9 Scala files / 782 nonblank Scala lines**. The packaged inventory has 445 classes. + +Review follow-up: restored packet-scheduler callback mutation behavior with the original Scala hash-map traversal, +virtual tile accessor dispatch throughout `TMultiPart`, and null-safe equality for scheduled-tick deduplication. +Six JVM and two Forge regression cases cover the fixes. Callable signatures remain unchanged; the packet traversal +callback adds one anonymous class. See the latest history entries for the individual fixes and validation. Latest bounded target: `TMicroOcclusionClient` delegates bounds copying and mask updates to `TMicroOcclusionClientLogic.java`. Scala retains inheritance metadata, mutable state/accessors and the three diff --git a/docs/migration/HISTORY.md b/docs/migration/HISTORY.md index 58d2e65..c9d8052 100644 --- a/docs/migration/HISTORY.md +++ b/docs/migration/HISTORY.md @@ -1286,3 +1286,14 @@ differences belong in the [divergence ledger](../../JAVA_MIGRATION_DIVERGENCES.m - Added three JVM regression cases covering overridden binding/unbinding, convenience methods and description reads that publish a replacement tile before the render update. All three failed before the fix. - Formatting, test checkstyle, the three new cases and all seven existing tile equality cases pass. + +### 2026-09-04 — Tick scheduler equality regression + +- Restored Scala's null-safe part equality in `ChunkTickScheduler.scheduleTick` with `Objects.equals`. Distinct but + equal parts share the original entry and callback target, and random-to-scheduled promotion updates that entry. + Existing explicit ticks keep their original timing. +- Added three JVM regression cases for deduplication, promotion and retaining an existing explicit tick. All three + failed before the fix. Together with the accessor and packet fixes, this adds six JVM and two Forge cases. +- Formatting, test checkstyle, build and Forge pass with 398 JVM / 237 Forge tests and zero failures/errors/skips. + The four touched class surfaces retain their callable member names and descriptors. The packaged inventory grows + from 444 to 445 classes solely for the packet traversal callback; no existing class is removed. diff --git a/src/main/scala/codechicken/multipart/TickScheduler.java b/src/main/scala/codechicken/multipart/TickScheduler.java index ef5dafe..b93ae64 100644 --- a/src/main/scala/codechicken/multipart/TickScheduler.java +++ b/src/main/scala/codechicken/multipart/TickScheduler.java @@ -7,6 +7,7 @@ import java.io.FileOutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import net.minecraft.nbt.CompressedStreamTools; @@ -209,7 +210,7 @@ public final class TickScheduler { void scheduleTick(TMultiPart part, long time, boolean random) { for (PartTickEntry e : tickList) { - if (e.part() == part) { + if (Objects.equals(e.part(), part)) { // only override an existing tick if we're going from random to scheduled if (e.random() && !random) { e.time_$eq(time); diff --git a/src/test/java/codechicken/multipart/TickSchedulerEqualityRegressionTest.java b/src/test/java/codechicken/multipart/TickSchedulerEqualityRegressionTest.java new file mode 100644 index 0000000..faab6b0 --- /dev/null +++ b/src/test/java/codechicken/multipart/TickSchedulerEqualityRegressionTest.java @@ -0,0 +1,90 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.*; + +import net.minecraft.world.chunk.Chunk; + +import org.junit.jupiter.api.Test; + +class TickSchedulerEqualityRegressionTest { + + @Test + void equalPartsShareOneEntryAndKeepTheOriginalCallbackTarget() { + TickScheduler.ChunkTickScheduler scheduler = scheduler(); + EqualPart first = new EqualPart("same"); + EqualPart equal = new EqualPart("same"); + EqualPart different = new EqualPart("different"); + + scheduler.scheduleTick(first, 0, false); + scheduler.scheduleTick(equal, 0, false); + scheduler.scheduleTick(different, 0, false); + + assertFalse(scheduler.processTicks()); + assertEquals(1, first.calls); + assertEquals(0, equal.calls); + assertEquals(1, different.calls); + } + + @Test + void anEqualPartCanPromoteARandomEntryToAnImmediateScheduledTick() { + TickScheduler.ChunkTickScheduler scheduler = scheduler(); + EqualPart first = new EqualPart("same"); + EqualPart equal = new EqualPart("same"); + + scheduler.scheduleTick(first, 100, true); + scheduler.scheduleTick(equal, 0, false); + + assertFalse(scheduler.processTicks()); + assertEquals(1, first.calls); + assertEquals(0, equal.calls); + } + + @Test + void anEqualPartDoesNotReplaceAnExistingExplicitTick() { + TickScheduler.ChunkTickScheduler scheduler = scheduler(); + EqualPart first = new EqualPart("same"); + EqualPart equal = new EqualPart("same"); + + scheduler.scheduleTick(first, 100, false); + scheduler.scheduleTick(equal, 0, false); + + assertTrue(scheduler.processTicks()); + assertEquals(0, first.calls); + assertEquals(0, equal.calls); + } + + private static TickScheduler.ChunkTickScheduler scheduler() { + return new TickScheduler.ChunkTickScheduler(new Chunk(null, 0, 0), new TickScheduler.WorldTickScheduler(null)); + } + + private static final class EqualPart extends TMultiPart { + + private final String key; + int calls; + + EqualPart(String key) { + this.key = key; + bind(new TileMultipart()); + } + + @Override + public String getType() { + return "test:equal-scheduled"; + } + + @Override + public boolean equals(Object other) { + return other instanceof EqualPart && key.equals(((EqualPart) other).key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public void scheduledTick() { + calls++; + } + } +}