diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index 17e09da..6feac1d 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -740,3 +740,59 @@ removed. The Java loop produces no replacement class. - Java 8 Forge dedicated-server suite: 5 tests, 0 failures, 0 errors, covering real part registration and instantiation of `mc_torch` and `mcr_face` plus composite tile generation. - Ray tracing, rendering and particle paths need a client and stay on the manual checklist. + +## 2026-08-14 — TickScheduler Java port + +No jar in the pack references this class, so the ABI constraint was minimal, but the reference shape was preserved +anyway for consistency with the other ports. + +### Observable behavior + +No known divergence. Ticks scheduled while the world is mid-tick are still queued and applied after processing rather +than during it, an existing entry for the same part is still only overridden when going from random to scheduled, +random-update parts still reschedule themselves 800 to 1600 ticks out, chunks are still dropped from the active set +once their list empties, and only non-random entries are still written to chunk NBT. + +`processTicks` filtered a `ListBuffer` with a side-effecting predicate that fires the part callbacks and mutates the +entry. The Java version iterates and rebuilds the list in the same order, so callback order and the reschedule +mutation are unchanged. + +### The singleton carries behavior here + +Unlike the other companion singletons in this migration, `TickScheduler$` is not a pure forwarder. The object extends +`WorldExtensionInstantiator` and `WorldExtensionManager.registerWorldExtension` is handed the instance itself, so the +two instantiator hooks have to live on the singleton. It stays a Java class extending `WorldExtensionInstantiator` +with `MODULE$`, delegating into the statics. + +### Supported JVM API + +- `TickScheduler$` and `TickScheduler$PartTickEntry` are descriptor-identical to the reference, including + `PartTickEntry`'s Scala-style `time()`, `time_$eq`, `random()` and `random_$eq` accessor pairs and both + constructors. +- `TickScheduler` adds only a private constructor. +- `WorldTickScheduler` and `ChunkTickScheduler` were Scala `private class` and are now package-private static nested + classes, keeping their `TickScheduler$WorldTickScheduler` and `TickScheduler$ChunkTickScheduler` binary names. + +### Recompiled Scala consumers + +One call site. `WorldExtensionManager.registerWorldExtension(TickScheduler)` passed the object as a value and must now +pass `TickScheduler$.MODULE$`. + +`TRandomUpdateTick` is a Scala trait extending `TMultiPart`, so at bytecode level it is a bare interface that does not +extend the class. The Java code casts where the reference relied on Scala's view of the type, the same pattern already +recorded for `JIconHitEffects`. + +### Compiler artifacts + +Accepted divergence: the five `$$anonfun$` classes under `WorldTickScheduler.postTick` and `ChunkTickScheduler`'s +`processTicks`, `saveData` and `loadData` are removed. The Java loops produce no replacement classes. + +### Validation + +- Complete plain-JVM suite: 90 tests, 0 failures, 0 errors. +- Clean `spotlessApply checkstyleTest build`: passing. +- Java 8 Forge dedicated-server suite: 5 tests, 0 failures, 0 errors. This exercises the port directly rather than + incidentally: the scheduler is registered as a world extension during startup, so `createWorldExtension`, `load`, + and the `preTick`/`postTick` pair all run for every server tick of the suite. +- Scheduled and random tick firing over time, and the chunk NBT round trip, are not asserted by any test and stay on + the manual checklist. diff --git a/src/main/scala/codechicken/multipart/TickScheduler$.java b/src/main/scala/codechicken/multipart/TickScheduler$.java new file mode 100644 index 0000000..f76ee94 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TickScheduler$.java @@ -0,0 +1,41 @@ +package codechicken.multipart; + +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; + +import codechicken.lib.world.ChunkExtension; +import codechicken.lib.world.WorldExtension; +import codechicken.lib.world.WorldExtensionInstantiator; + +/** + * Scala companion singleton. This one is not just a forwarder: WorldExtensionManager registers the instance itself, so + * the instantiator hooks have to live here. + */ +public final class TickScheduler$ extends WorldExtensionInstantiator { + + public static final TickScheduler$ MODULE$ = new TickScheduler$(); + + private TickScheduler$() {} + + @Override + public WorldExtension createWorldExtension(World world) { + return TickScheduler.createWorldExtension(world); + } + + @Override + public ChunkExtension createChunkExtension(Chunk chunk, WorldExtension world) { + return TickScheduler.createChunkExtension(chunk, world); + } + + public void loadRandomTick(TRandomUpdateTick part) { + TickScheduler.loadRandomTick(part); + } + + public void scheduleTick(TMultiPart part, int ticks) { + TickScheduler.scheduleTick(part, ticks); + } + + public long getSchedulerTime(World world) { + return TickScheduler.getSchedulerTime(world); + } +} diff --git a/src/main/scala/codechicken/multipart/TickScheduler.java b/src/main/scala/codechicken/multipart/TickScheduler.java new file mode 100644 index 0000000..a93d791 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TickScheduler.java @@ -0,0 +1,344 @@ +package codechicken.multipart; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.ChunkPosition; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.storage.SaveHandler; +import net.minecraftforge.common.DimensionManager; + +import codechicken.lib.vec.BlockCoord; +import codechicken.lib.world.ChunkExtension; +import codechicken.lib.world.WorldExtension; +import codechicken.multipart.handler.MultipartProxy; + +/** + * Used for scheduling delayed callbacks to parts. Do not use this for redstone applications that require precise + * timing. If 2 parts are both scheduled for an update on the same tick, there is no guarantee which one will update + * first. These parts should not depend on a state of another part that may have changed before/after them. + */ +public final class TickScheduler { + + private TickScheduler() {} + + public static class PartTickEntry { + + private final TMultiPart part; + private long time; + private boolean random; + + public PartTickEntry(TMultiPart part, long time, boolean random) { + this.part = part; + this.time = time; + this.random = random; + } + + public PartTickEntry(TMultiPart part, int ticks) { + this(part, ticks, false); + } + + public TMultiPart part() { + return part; + } + + public long time() { + return time; + } + + public void time_$eq(long time) { + this.time = time; + } + + public boolean random() { + return random; + } + + public void random_$eq(boolean random) { + this.random = random; + } + } + + static class WorldTickScheduler extends WorldExtension { + + private long schedTime = 0L; + private Set tickChunks = new HashSet<>(); + private boolean processing = false; + private final List pending = new ArrayList<>(); + + WorldTickScheduler(World world) { + super(world); + } + + void scheduleTick(TMultiPart part, int ticks, boolean random) { + if (processing) { + pending.add(new PartTickEntry(part, schedTime + ticks, random)); + } else { + doScheduleTick(part, schedTime + ticks, random); + } + } + + void doScheduleTick(TMultiPart part, long time, boolean random) { + if (part.tile() != null) { + ((ChunkTickScheduler) getChunkExtension(part.tile().xCoord >> 4, part.tile().zCoord >> 4)) + .scheduleTick(part, time, random); + } + } + + void loadRandom(TRandomUpdateTick part) { + scheduleTick((TMultiPart) part, nextRandomTick(), true); + } + + @Override + public void preTick() { + processing = true; + } + + @Override + public void postTick() { + if (!tickChunks.isEmpty()) { + Set remaining = new HashSet<>(); + for (ChunkTickScheduler chunk : tickChunks) { + if (chunk.processTicks()) { + remaining.add(chunk); + } + } + tickChunks = remaining; + } + + processing = false; + for (PartTickEntry e : pending) { + doScheduleTick(e.part(), e.time(), e.random()); + } + pending.clear(); + + schedTime += 1; + } + + File saveDir() { + // Calling DimensionManager.getCurrentSaveRootDirectory too early breaks game saves, we have a world + // reference, use it + if (world.provider.dimensionId == 0) { + return ((SaveHandler) world.getSaveHandler()).getWorldDirectory(); + } + + return new File(DimensionManager.getCurrentSaveRootDirectory(), world.provider.getSaveFolder()); + } + + File saveFile() { + return new File(saveDir(), "multipart.dat"); + } + + @Override + public void load() { + try { + FileInputStream in = new FileInputStream(saveFile()); + loadTag(CompressedStreamTools.readCompressed(in)); + in.close(); + } catch (Exception e) { + // Matches the reference, which swallows any failure to read the saved schedule. + } + + loadTag(new NBTTagCompound()); + } + + void loadTag(NBTTagCompound tag) { + if (tag.hasKey("schedTime")) { + schedTime = tag.getLong("schedTime"); + } else { + schedTime = world.getTotalWorldTime(); + } + } + + NBTTagCompound saveTag() { + NBTTagCompound tag = new NBTTagCompound(); + tag.setLong("schedTime", schedTime); + return tag; + } + + @Override + public void save() { + try { + File file = saveFile(); + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + if (!file.exists()) { + file.createNewFile(); + } + + DataOutputStream dout = new DataOutputStream(new FileOutputStream(file)); + CompressedStreamTools.writeCompressed(saveTag(), dout); + dout.close(); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + int nextRandomTick() { + return world.rand.nextInt(800) + 800; + } + } + + static class ChunkTickScheduler extends ChunkExtension { + + private List tickList = new ArrayList<>(); + + ChunkTickScheduler(Chunk chunk, WorldTickScheduler world) { + super(chunk, world); + } + + private WorldTickScheduler scheduler() { + return (WorldTickScheduler) world; + } + + long schedTime() { + return scheduler().schedTime; + } + + void scheduleTick(TMultiPart part, long time, boolean random) { + for (PartTickEntry e : tickList) { + if (e.part() == part) { + // only override an existing tick if we're going from random to scheduled + if (e.random() && !random) { + e.time_$eq(time); + e.random_$eq(random); + } + return; + } + } + tickList.add(new PartTickEntry(part, time, random)); + if (tickList.size() == 1) { + scheduler().tickChunks.add(this); + } + } + + int nextRandomTick() { + return scheduler().nextRandomTick(); + } + + boolean processTicks() { + List kept = new ArrayList<>(); + for (PartTickEntry e : tickList) { + if (processTick(e)) { + kept.add(e); + } + } + tickList = kept; + return !tickList.isEmpty(); + } + + boolean processTick(PartTickEntry e) { + if (e.time() <= schedTime()) { + if (e.part().tile() != null) { + if (e.random() && e.part() instanceof TRandomUpdateTick) { + ((TRandomUpdateTick) e.part()).randomUpdate(); + } else { + e.part().scheduledTick(); + } + + if (e.part() instanceof TRandomUpdateTick) { + e.time_$eq(schedTime() + nextRandomTick()); + e.random_$eq(true); + return true; + } + } + return false; + } + return true; + } + + @Override + public void saveData(NBTTagCompound data) { + NBTTagList tagList = new NBTTagList(); + for (PartTickEntry e : tickList) { + TMultiPart part = e.part(); + if (part.tile() != null && !e.random()) { + NBTTagCompound tag = new NBTTagCompound(); + tag.setShort("pos", (short) MultipartProxy.indexInChunk(new BlockCoord(part.tile()))); + tag.setByte("i", (byte) part.tile().jPartList().indexOf(part)); + tag.setLong("time", e.time()); + tagList.appendTag(tag); + } + } + if (tagList.tagCount() > 0) { + data.setTag("multipartTicks", tagList); + } + } + + @Override + public void loadData(NBTTagCompound data) { + tickList.clear(); + if (!data.hasKey("multipartTicks")) { + return; + } + + NBTTagList tagList = data.getTagList("multipartTicks", 10); + ChunkCoordIntPair cc = new ChunkCoordIntPair(0, 0); + for (int i = 0; i < tagList.tagCount(); i++) { + NBTTagCompound tag = tagList.getCompoundTagAt(i); + BlockCoord pos = MultipartProxy.indexInChunk(cc, tag.getShort("pos")); + Object tile = chunk.chunkTileEntityMap.get(new ChunkPosition(pos.x, pos.y, pos.z)); + if (tile instanceof TileMultipart) { + tickList.add( + new PartTickEntry( + ((TileMultipart) tile).jPartList().get(tag.getByte("i")), + tag.getLong("time"), + false)); + } + } + } + + @Override + public void unload() { + if (!tickList.isEmpty()) { + scheduler().tickChunks.remove(this); + } + } + } + + public static WorldExtension createWorldExtension(World world) { + return new WorldTickScheduler(world); + } + + public static ChunkExtension createChunkExtension(Chunk chunk, WorldExtension world) { + return new ChunkTickScheduler(chunk, (WorldTickScheduler) world); + } + + public static WorldExtension getExtension(World world) { + return TickScheduler$.MODULE$.getExtension(world); + } + + /** Start random ticking for a part. Should be called from TMultiPart.onWorldJoin. */ + public static void loadRandomTick(TRandomUpdateTick part) { + TMultiPart multiPart = (TMultiPart) part; + ((WorldTickScheduler) getExtension(multiPart.tile().getWorldObj())).loadRandom(part); + } + + /** Schedule a tick for part relative to the current time. */ + public static void scheduleTick(TMultiPart part, int ticks) { + ((WorldTickScheduler) getExtension(part.tile().getWorldObj())).scheduleTick(part, ticks, false); + } + + /** + * Returns the current scheduler time. Like the world time, but unaffected by the time set command and other things + * changing time of day. + * + * @deprecated in favor of world.getTotalWorldTime + */ + @Deprecated + public static long getSchedulerTime(World world) { + return ((WorldTickScheduler) getExtension(world)).schedTime; + } +} diff --git a/src/main/scala/codechicken/multipart/TickScheduler.scala b/src/main/scala/codechicken/multipart/TickScheduler.scala deleted file mode 100644 index 632d096..0000000 --- a/src/main/scala/codechicken/multipart/TickScheduler.scala +++ /dev/null @@ -1,259 +0,0 @@ -package codechicken.multipart - -import codechicken.lib.world.WorldExtensionInstantiator -import codechicken.lib.world.WorldExtension -import codechicken.lib.world.ChunkExtension -import net.minecraft.world.chunk.Chunk -import net.minecraft.world.World -import net.minecraft.nbt.{NBTTagCompound, NBTTagList, CompressedStreamTools} -import scala.collection.mutable.ListBuffer -import codechicken.lib.vec.BlockCoord -import net.minecraft.world.ChunkCoordIntPair -import net.minecraft.world.ChunkPosition -import scala.collection.mutable.HashSet -import java.io.DataOutputStream -import net.minecraftforge.common.DimensionManager -import java.io.File -import java.io.FileOutputStream -import java.io.FileInputStream -import net.minecraft.world.storage.SaveHandler - -/** Used for scheduling delayed callbacks to parts. Do not use this for redstone - * applications that require precise timing. If 2 parts are both scheduled for - * an update on the same tick, there is no guarantee which one will update - * first. These parts should not depend on a state of another part that may - * have changed before/after them. - */ -object TickScheduler extends WorldExtensionInstantiator { - class PartTickEntry( - val part: TMultiPart, - var time: Long, - var random: Boolean - ) { - def this(part: TMultiPart, ticks: Int) = this(part, ticks, false) - } - - private class WorldTickScheduler(world$ : World) - extends WorldExtension(world$) { - var schedTime = 0L - var tickChunks = HashSet[ChunkTickScheduler]() - private var processing = false - private val pending = ListBuffer[PartTickEntry]() - - def scheduleTick(part: TMultiPart, ticks: Int, random: Boolean) { - if (processing) - pending += new PartTickEntry(part, schedTime + ticks, random) - else - _scheduleTick(part, schedTime + ticks, random) - } - - def _scheduleTick(part: TMultiPart, time: Long, random: Boolean) { - if (part.tile != null) - getChunkExtension(part.tile.xCoord >> 4, part.tile.zCoord >> 4) - .asInstanceOf[ChunkTickScheduler] - .scheduleTick(part, time, random) - } - - def loadRandom(part: TRandomUpdateTick) = - scheduleTick(part, nextRandomTick, true) - - override def preTick() { - processing = true - } - - override def postTick() { - if (!tickChunks.isEmpty) - tickChunks = tickChunks.filter(_.processTicks()) - - processing = false - pending.foreach(e => _scheduleTick(e.part, e.time, e.random)) - pending.clear() - - schedTime += 1 - } - - def saveDir: File = { - if ( - world.provider.dimensionId == 0 - ) // Calling DimensionManager.getCurrentSaveRootDirectory too early breaks game saves, we have a world reference, use it - return world.getSaveHandler.asInstanceOf[SaveHandler].getWorldDirectory - - return new File( - DimensionManager.getCurrentSaveRootDirectory, - world.provider.getSaveFolder - ) - } - - def saveFile: File = new File(saveDir, "multipart.dat") - - override def load() { - try { - val in = new FileInputStream(saveFile) - loadTag(CompressedStreamTools.readCompressed(in)) - in.close() - } catch { - case e: Exception => - } - - loadTag(new NBTTagCompound) - } - - def loadTag(tag: NBTTagCompound) { - if (tag.hasKey("schedTime")) - schedTime = tag.getLong("schedTime") - else - schedTime = world.getTotalWorldTime - } - - def saveTag: NBTTagCompound = { - val tag = new NBTTagCompound - tag.setLong("schedTime", schedTime) - return tag - } - - override def save() { - val file = saveFile - if (!file.getParentFile.exists) - file.getParentFile.mkdirs() - if (!file.exists) - file.createNewFile() - - val dout = new DataOutputStream(new FileOutputStream(file)) - CompressedStreamTools.writeCompressed(saveTag, dout) - dout.close() - } - - def nextRandomTick = world.rand.nextInt(800) + 800 - } - - def createWorldExtension(world: World): WorldExtension = - new WorldTickScheduler(world) - - private class ChunkTickScheduler(chunk$ : Chunk, world: WorldTickScheduler) - extends ChunkExtension(chunk$, world) { - import codechicken.multipart.handler.MultipartProxy._ - - var tickList = ListBuffer[PartTickEntry]() - - def schedTime = world.schedTime - - def scheduleTick(part: TMultiPart, time: Long, random: Boolean) { - val it = tickList.iterator - while (it.hasNext) { - val e = it.next() - if (e.part == part) { - if ( - e.random && !random - ) // only override an existing tick if we're going from random->scheduled - { - e.time = time - e.random = random - } - return - } - } - tickList += new PartTickEntry(part, time, random) - if (tickList.size == 1) - world.tickChunks += this - } - - def nextRandomTick = world.nextRandomTick - - def processTicks(): Boolean = { - tickList = tickList.filter(processTick) - return !tickList.isEmpty - } - - def processTick(e: PartTickEntry): Boolean = { - if (e.time <= schedTime) { - if (e.part.tile != null) { - if (e.random && e.part.isInstanceOf[TRandomUpdateTick]) - e.part.asInstanceOf[TRandomUpdateTick].randomUpdate() - else - e.part.scheduledTick() - - if (e.part.isInstanceOf[TRandomUpdateTick]) { - e.time = schedTime + nextRandomTick - e.random = true - return true - } - } - return false - } - return true - } - - override def saveData(data: NBTTagCompound) { - val tagList = new NBTTagList - tickList.foreach { e => - val part = e.part - if (part.tile != null && !e.random) { - val tag = new NBTTagCompound - tag.setShort("pos", indexInChunk(new BlockCoord(part.tile)).toShort) - tag.setByte("i", part.tile.partList.indexOf(part).toByte) - tag.setLong("time", e.time) - tagList.appendTag(tag) - } - } - if (tagList.tagCount > 0) - data.setTag("multipartTicks", tagList) - } - - override def loadData(data: NBTTagCompound) { - tickList.clear() - if (!data.hasKey("multipartTicks")) - return - - val tagList = data.getTagList("multipartTicks", 10) - val cc = new ChunkCoordIntPair(0, 0) - for (i <- 0 until tagList.tagCount) { - val tag = tagList.getCompoundTagAt(i) - val pos = indexInChunk(cc, tag.getShort("pos")) - val tile = - chunk.chunkTileEntityMap.get(new ChunkPosition(pos.x, pos.y, pos.z)) - if (tile.isInstanceOf[TileMultipart]) - tickList += new PartTickEntry( - tile.asInstanceOf[TileMultipart].partList.apply(tag.getByte("i")), - tag.getLong("time"), - false - ) - } - } - - override def unload() { - if (!tickList.isEmpty) - world.tickChunks -= this - } - } - - def createChunkExtension( - chunk: Chunk, - world: WorldExtension - ): ChunkExtension = - new ChunkTickScheduler(chunk, world.asInstanceOf[WorldTickScheduler]) - - /** Start random ticking for a part. Should be called from - * TMultiPart.onWorldJoin - */ - def loadRandomTick(part: TRandomUpdateTick) { - getExtension(part.tile.getWorldObj) - .asInstanceOf[WorldTickScheduler] - .loadRandom(part) - } - - /** Schedule a tick for part relative to the current time. - */ - def scheduleTick(part: TMultiPart, ticks: Int) { - getExtension(part.tile.getWorldObj) - .asInstanceOf[WorldTickScheduler] - .scheduleTick(part, ticks, false) - } - - /** Returns the current scheduler time. Like the world time, but unaffected by - * the time set command and other things changing time of day. Deprecated in - * favor of world.getTotalWorldTime - */ - @Deprecated - def getSchedulerTime(world: World): Long = - getExtension(world).asInstanceOf[WorldTickScheduler].schedTime -} diff --git a/src/main/scala/codechicken/multipart/handler/proxies.scala b/src/main/scala/codechicken/multipart/handler/proxies.scala index 282ab2f..45c1c4a 100644 --- a/src/main/scala/codechicken/multipart/handler/proxies.scala +++ b/src/main/scala/codechicken/multipart/handler/proxies.scala @@ -86,7 +86,7 @@ class MultipartProxy_serverImpl { MultipartSPH ) - WorldExtensionManager.registerWorldExtension(TickScheduler) + WorldExtensionManager.registerWorldExtension(TickScheduler$.MODULE$) TileChunkLoadHook.init() MultipartCompatiblity.load()