diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index a14ba82..07f940c 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -1043,3 +1043,12 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec direct iterator loop. - All 191 plain-JVM and all 98 Java 8 Forge dedicated-server tests pass. This startup-only proxy is not a meaningful focused performance target; `multipart/handler/packethandlers.scala` is the last Scala handler unit and is next. +- Characterized the ForgeMultipart packet-handler unit against untouched Scala. Eleven plain-JVM cases freeze all six + emitted retained types, exact facade/companion interfaces, channel identity, private prefixed state accessors, + ordered registry and desync disconnects, control-key packets, coordinate streams and both update terminators. +- Ported the five top-level types and nested byte stream directly to Java. The three Scala mutable collection + descriptors remain where reflection already observes them, while direct loops replace the anonymous `MultiMap` and + thirteen closure classes without changing watcher, batching, framing or cleanup behavior. +- A clean build passes all 202 plain-JVM tests and the Java 8 Forge server passes all 98 tests. All six retained public + surfaces match the reference by name and descriptor; only fourteen unreferenced compiler artifacts disappear. + `microblock/ItemMicroPart.scala` is next. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index c1186bc..b2e9f11 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -2257,3 +2257,61 @@ private implementation class. names/descriptors and side annotations. The sole class-list removal is the private closure above. - Item-renderer registration, material icon loading, client packet registration and Angelica integration require a client and remain on the manual checklist. + +## 2026-09-01 — ForgeMultipart packet-handler Java port + +The shared packet-handler base, both client/server facade-companion pairs and the nested byte stream are now Java. +The multipart proxy continues to register `MultipartCPH$.MODULE$` and `MultipartSPH$.MODULE$`, preserving the exact +singleton objects CodeChickenLib receives. + +### Observable behavior + +No known runtime divergence. The ordinary channel remains `MultipartMod$.MODULE$` and the registry channel remains +`ForgeMultipart`. Registry packet type 1 still replaces the client ID map and disconnects with translation key +`multipart.missing` plus every missing name in server order. Client desynchronization exceptions beginning `DC: ` +still become a plain-text disconnect with that marker removed; other runtime exceptions still escape unchanged. + +Chunk-description packet type 2 still reads chunk coordinates, an unsigned tile count, signed local x/z bytes and +each tile description in sequence. Update packet type 3 still reads absolute coordinates until `Integer.MAX_VALUE` +and part updates until unsigned byte 255. The server callback still maps packet type 1 to the sender's control-key +state, and unknown packet types on either side still throw `scala.MatchError`. + +Server handshakes still send the complete part-ID map on packet type 1. Tile streams remain cached by world and +position, begin with the absolute coordinate, and reject client worlds before publishing state. World unload still +removes only server state. End-of-tick processing still flushes scheduled packets first, sends accumulated updates +only to players watching each chunk, clears every per-world update map, sends descriptions for newly watched chunks, +promotes those watches and finally clears the pending-watch map. Description packets retain their exact logical +framing and deferred CodeChickenLib compression. + +The direct Java loops avoid the closure objects previously created by the Scala comprehensions. No focused profile +claim is made because the current functional profiling fixture does not generate sustained watched-chunk multipart +traffic; a representative full-pack capture remains the authority. + +### ABI and source compatibility + +`MultipartPH` retains its public constructor, `MultipartMod$`-typed `channel` field/accessor and registry-channel +accessor. `MultipartCPH` and `MultipartSPH` retain every static forwarder. Their companions retain `MODULE$`, extend +`MultipartPH`, implement the same CodeChickenLib interfaces in the same order, and expose the same instance methods. +`MultipartSPH$` also retains the exact private prefixed `updateMap`, `chunkWatchers` and `newWatchers` fields and their +public mangled accessors with Scala `Map`/`HashMap` descriptors. + +`MultipartSPH.MCByteStream` remains a public, non-final static member of the facade, so its binary name is still +`MultipartSPH$MCByteStream`. It retains the `(ByteArrayOutputStream)` constructor, private final `bout` field, +`MCDataOutputWrapper` superclass and `getBytes()[B`. No audited shipping consumer references any handler type. + +Both jars retain these six runtime classes. Accepted classfile-only differences are removed Scala signature/marker +attributes, private constructors on the static facades, companion class initializers without Scala's `ACC_PUBLIC` +flag, and removal of the anonymous `MultiMap` plus thirteen `$$anonfun$` classes. The fourteen removed classes are +private compiler artifacts with no downstream reference. + +### Validation + +- `MultipartPacketHandlerCharacterizationTest` and `MultipartPacketHandlerBehaviorTest`: 11 plain-JVM tests, + unchanged from the Scala baseline. +- Existing `MultipartEventHandlerFunctionalTest` and `MultipartHelperFunctionalTest` cases retain unload cleanup, + watcher/tick ordering and exact logical chunk-description framing. +- Clean complete plain-JVM suite: 202 tests, 0 failures, 0 errors. +- Java 8 Forge dedicated-server suite: 98 tests, 0 failures, 0 errors. +- Raw `javap` comparison confirms identical callable public names/descriptors for all six retained classes. The exact + class-list diff contains only the fourteen compiler artifacts described above. +- Real client chunk-description application, update dispatch and rendering remain on the manual checklist. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index ecc8de9..c70718d 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -12,8 +12,8 @@ what breaks, and what is left. The other documents hold the reasoning. | `JAVA_MIGRATION_MANUAL_CHECKS.md` | What no automated test can cover, and must be checked by hand in a client | | `JAVA_MIGRATION_PROFILE.md` | The focused baseline, first measured result, findings, and exact rerun command | -Branch: `algent/java`. Base: `master`. 111 commits including the API-cleanup plan and separate characterization and -port commits for both proxy hierarchies. +Branch: `algent/java`. Base: `master`. 113 commits including the API-cleanup plan and separate characterization and +port commits for both proxy hierarchies and the multipart packet-handler unit. ## The one rule that matters @@ -67,7 +67,7 @@ awk -F'"' '/ missing = MultiPartRegistry.readIDMap(packet); + if (!missing.isEmpty()) { + netHandler.handleDisconnect( + new S40PacketDisconnect( + new ChatComponentTranslation("multipart.missing", String.join(", ", missing)))); + } + } + + public void handleCompressedTileDesc(PacketCustom packet, World world) { + ChunkCoordIntPair chunk = new ChunkCoordIntPair(packet.readInt(), packet.readInt()); + int count = packet.readUShort(); + for (int i = 0; i < count; i++) { + TileMultipart.handleDescPacket( + world, + new BlockCoord( + packet.readByte() + (chunk.chunkXPos << 4), + packet.readInt(), + packet.readByte() + (chunk.chunkZPos << 4)), + packet); + } + } + + public void handleCompressedTileData(PacketCustom packet, World world) { + int x = packet.readInt(); + while (x != Integer.MAX_VALUE) { + BlockCoord position = new BlockCoord(x, packet.readInt(), packet.readInt()); + int part = packet.readUByte(); + while (part < 255) { + TileMultipart.handlePacket(position, world, part, packet); + part = packet.readUByte(); + } + x = packet.readInt(); + } + } +} diff --git a/src/main/scala/codechicken/multipart/handler/MultipartCPH.java b/src/main/scala/codechicken/multipart/handler/MultipartCPH.java new file mode 100644 index 0000000..8e66a47 --- /dev/null +++ b/src/main/scala/codechicken/multipart/handler/MultipartCPH.java @@ -0,0 +1,36 @@ +package codechicken.multipart.handler; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.play.INetHandlerPlayClient; +import net.minecraft.world.World; + +import codechicken.lib.packet.PacketCustom; + +public final class MultipartCPH { + + private MultipartCPH() {} + + public static MultipartMod$ channel() { + return MultipartCPH$.MODULE$.channel(); + } + + public static String registryChannel() { + return MultipartCPH$.MODULE$.registryChannel(); + } + + public static void handlePacket(PacketCustom packet, Minecraft minecraft, INetHandlerPlayClient netHandler) { + MultipartCPH$.MODULE$.handlePacket(packet, minecraft, netHandler); + } + + public static void handlePartRegistration(PacketCustom packet, INetHandlerPlayClient netHandler) { + MultipartCPH$.MODULE$.handlePartRegistration(packet, netHandler); + } + + public static void handleCompressedTileDesc(PacketCustom packet, World world) { + MultipartCPH$.MODULE$.handleCompressedTileDesc(packet, world); + } + + public static void handleCompressedTileData(PacketCustom packet, World world) { + MultipartCPH$.MODULE$.handleCompressedTileData(packet, world); + } +} diff --git a/src/main/scala/codechicken/multipart/handler/MultipartPH.java b/src/main/scala/codechicken/multipart/handler/MultipartPH.java new file mode 100644 index 0000000..fe4eef2 --- /dev/null +++ b/src/main/scala/codechicken/multipart/handler/MultipartPH.java @@ -0,0 +1,15 @@ +package codechicken.multipart.handler; + +public class MultipartPH { + + private final MultipartMod$ channel = MultipartMod$.MODULE$; + private final String registryChannel = "ForgeMultipart"; + + public MultipartMod$ channel() { + return channel; + } + + public String registryChannel() { + return registryChannel; + } +} diff --git a/src/main/scala/codechicken/multipart/handler/MultipartSPH$.java b/src/main/scala/codechicken/multipart/handler/MultipartSPH$.java new file mode 100644 index 0000000..f36e5d7 --- /dev/null +++ b/src/main/scala/codechicken/multipart/handler/MultipartSPH$.java @@ -0,0 +1,235 @@ +package codechicken.multipart.handler; + +import java.io.ByteArrayOutputStream; +import java.util.Iterator; +import java.util.LinkedList; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.play.INetHandlerPlayServer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; + +import codechicken.lib.packet.PacketCustom; +import codechicken.lib.packet.PacketCustom.IHandshakeHandler; +import codechicken.lib.packet.PacketCustom.IServerPacketHandler; +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.ControlKeyModifer; +import codechicken.multipart.MultiPartRegistry; +import codechicken.multipart.PacketScheduler; +import codechicken.multipart.TileMultipart; +import scala.MatchError; +import scala.Option; +import scala.Tuple2; +import scala.collection.mutable.HashMap; +import scala.collection.mutable.HashSet; +import scala.collection.mutable.Map; +import scala.collection.mutable.Set; + +public final class MultipartSPH$ extends MultipartPH implements IServerPacketHandler, IHandshakeHandler { + + public static final MultipartSPH$ MODULE$ = new MultipartSPH$(); + + private final Map> codechicken$multipart$handler$MultipartSPH$$updateMap = new HashMap<>(); + private final HashMap> codechicken$multipart$handler$MultipartSPH$$chunkWatchers = new HashMap<>(); + private final Map> codechicken$multipart$handler$MultipartSPH$$newWatchers = new HashMap<>(); + + private MultipartSPH$() {} + + public Map> codechicken$multipart$handler$MultipartSPH$$updateMap() { + return codechicken$multipart$handler$MultipartSPH$$updateMap; + } + + public HashMap> codechicken$multipart$handler$MultipartSPH$$chunkWatchers() { + return codechicken$multipart$handler$MultipartSPH$$chunkWatchers; + } + + public Map> codechicken$multipart$handler$MultipartSPH$$newWatchers() { + return codechicken$multipart$handler$MultipartSPH$$newWatchers; + } + + @Override + public void handlePacket(PacketCustom packet, EntityPlayerMP sender, INetHandlerPlayServer netHandler) { + int type = packet.getType(); + if (type == 1) { + ControlKeyModifer.map().put(sender, packet.readBoolean()); + return; + } + throw new MatchError(type); + } + + @Override + public void handshakeRecieved(NetHandlerPlayServer netHandler) { + PacketCustom packet = new PacketCustom(registryChannel(), 1); + MultiPartRegistry.writeIDMap(packet); + netHandler.sendPacket(packet.toPacket()); + } + + public void onWorldUnload(World world) { + if (!world.isRemote) { + codechicken$multipart$handler$MultipartSPH$$updateMap.remove(world); + } + } + + public MultipartSPH.MCByteStream getTileStream(World world, BlockCoord position) { + Option> worldOption = codechicken$multipart$handler$MultipartSPH$$updateMap + .get(world); + Map worldUpdates; + if (worldOption.isEmpty()) { + if (world.isRemote) { + throw new IllegalArgumentException("Cannot use MultipartSPH on a client world"); + } + worldUpdates = new HashMap<>(); + codechicken$multipart$handler$MultipartSPH$$updateMap.put(world, worldUpdates); + } else { + worldUpdates = worldOption.get(); + } + + Option streamOption = worldUpdates.get(position); + if (!streamOption.isEmpty()) { + return streamOption.get(); + } + + MultipartSPH.MCByteStream stream = new MultipartSPH.MCByteStream(new ByteArrayOutputStream()); + stream.writeCoord(position); + worldUpdates.put(position, stream); + return stream; + } + + public void onTickEnd(scala.collection.Seq players) { + PacketScheduler.sendScheduled(); + + scala.collection.Iterator playerIterator = players.iterator(); + while (playerIterator.hasNext()) { + EntityPlayerMP player = playerIterator.next(); + Object playerId = player.getEntityId(); + Option> chunkOption = codechicken$multipart$handler$MultipartSPH$$chunkWatchers + .get(playerId); + if (chunkOption.isEmpty()) { + continue; + } + + Option> updatesOption = codechicken$multipart$handler$MultipartSPH$$updateMap + .get(player.worldObj); + if (updatesOption.isEmpty() || updatesOption.get().isEmpty()) { + continue; + } + + Set chunks = chunkOption.get(); + PacketCustom packet = new PacketCustom(channel(), 3).compress(); + boolean send = false; + scala.collection.Iterator> updateIterator = updatesOption + .get().iterator(); + while (updateIterator.hasNext()) { + Tuple2 update = updateIterator.next(); + BlockCoord position = update._1(); + if (chunks.contains(new ChunkCoordIntPair(position.x >> 4, position.z >> 4))) { + send = true; + packet.writeByteArray(update._2().getBytes()); + packet.writeByte(255); + } + } + if (send) { + packet.writeInt(Integer.MAX_VALUE); + packet.sendToPlayer(player); + } + } + + scala.collection.Iterator>> worldIterator = codechicken$multipart$handler$MultipartSPH$$updateMap + .iterator(); + while (worldIterator.hasNext()) { + worldIterator.next()._2().clear(); + } + + playerIterator = players.iterator(); + while (playerIterator.hasNext()) { + EntityPlayerMP player = playerIterator.next(); + Object playerId = player.getEntityId(); + Option> chunksOption = codechicken$multipart$handler$MultipartSPH$$newWatchers + .get(playerId); + if (chunksOption.isEmpty()) { + continue; + } + + for (ChunkCoordIntPair chunkPosition : chunksOption.get()) { + Chunk chunk = player.worldObj.getChunkFromChunkCoords(chunkPosition.chunkXPos, chunkPosition.chunkZPos); + @SuppressWarnings("unchecked") + Iterator tiles = ((java.util.Map) chunk.chunkTileEntityMap).values() + .iterator(); + PacketCustom packet = getDescPacket(chunk, tiles); + if (packet != null) { + packet.sendToPlayer(player); + } + addChunkWatcher(playerId, chunkPosition); + } + } + codechicken$multipart$handler$MultipartSPH$$newWatchers.clear(); + } + + public void onChunkWatch(EntityPlayer player, ChunkCoordIntPair chunk) { + Object playerId = player.getEntityId(); + Option> option = codechicken$multipart$handler$MultipartSPH$$newWatchers + .get(playerId); + LinkedList chunks; + if (option.isEmpty()) { + chunks = new LinkedList<>(); + codechicken$multipart$handler$MultipartSPH$$newWatchers.put(playerId, chunks); + } else { + chunks = option.get(); + } + chunks.add(chunk); + } + + public void onChunkUnWatch(EntityPlayer player, ChunkCoordIntPair chunk) { + Object playerId = player.getEntityId(); + Option> pending = codechicken$multipart$handler$MultipartSPH$$newWatchers + .get(playerId); + if (!pending.isEmpty()) { + pending.get().remove(chunk); + } + + Option> active = codechicken$multipart$handler$MultipartSPH$$chunkWatchers.get(playerId); + if (!active.isEmpty()) { + Set chunks = active.get(); + chunks.remove(chunk); + if (chunks.isEmpty()) { + codechicken$multipart$handler$MultipartSPH$$chunkWatchers.remove(playerId); + } + } + } + + public PacketCustom getDescPacket(Chunk chunk, Iterator tiles) { + MultipartSPH.MCByteStream stream = new MultipartSPH.MCByteStream(new ByteArrayOutputStream()); + int count = 0; + while (tiles.hasNext()) { + TileEntity tile = tiles.next(); + if (tile instanceof TileMultipart) { + stream.writeByte(tile.xCoord & 0xF); + stream.writeInt(tile.yCoord); + stream.writeByte(tile.zCoord & 0xF); + ((TileMultipart) tile).writeDesc(stream); + count++; + } + } + if (count == 0) { + return null; + } + return new PacketCustom(channel(), 2).compress().writeInt(chunk.xPosition).writeInt(chunk.zPosition) + .writeShort(count).writeByteArray(stream.getBytes()); + } + + private void addChunkWatcher(Object playerId, ChunkCoordIntPair chunk) { + Option> option = codechicken$multipart$handler$MultipartSPH$$chunkWatchers.get(playerId); + Set chunks; + if (option.isEmpty()) { + chunks = new HashSet<>(); + codechicken$multipart$handler$MultipartSPH$$chunkWatchers.put(playerId, chunks); + } else { + chunks = option.get(); + } + chunks.add(chunk); + } +} diff --git a/src/main/scala/codechicken/multipart/handler/MultipartSPH.java b/src/main/scala/codechicken/multipart/handler/MultipartSPH.java new file mode 100644 index 0000000..3fec82d --- /dev/null +++ b/src/main/scala/codechicken/multipart/handler/MultipartSPH.java @@ -0,0 +1,77 @@ +package codechicken.multipart.handler; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.util.Iterator; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.play.INetHandlerPlayServer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; + +import codechicken.lib.data.MCDataOutputWrapper; +import codechicken.lib.packet.PacketCustom; +import codechicken.lib.vec.BlockCoord; + +public final class MultipartSPH { + + private MultipartSPH() {} + + public static class MCByteStream extends MCDataOutputWrapper { + + private final ByteArrayOutputStream bout; + + public MCByteStream(ByteArrayOutputStream bout) { + super(new DataOutputStream(bout)); + this.bout = bout; + } + + public byte[] getBytes() { + return bout.toByteArray(); + } + } + + public static MultipartMod$ channel() { + return MultipartSPH$.MODULE$.channel(); + } + + public static String registryChannel() { + return MultipartSPH$.MODULE$.registryChannel(); + } + + public static void handlePacket(PacketCustom packet, EntityPlayerMP sender, INetHandlerPlayServer netHandler) { + MultipartSPH$.MODULE$.handlePacket(packet, sender, netHandler); + } + + public static void handshakeRecieved(NetHandlerPlayServer netHandler) { + MultipartSPH$.MODULE$.handshakeRecieved(netHandler); + } + + public static void onWorldUnload(World world) { + MultipartSPH$.MODULE$.onWorldUnload(world); + } + + public static MCByteStream getTileStream(World world, BlockCoord position) { + return MultipartSPH$.MODULE$.getTileStream(world, position); + } + + public static void onTickEnd(scala.collection.Seq players) { + MultipartSPH$.MODULE$.onTickEnd(players); + } + + public static void onChunkWatch(EntityPlayer player, ChunkCoordIntPair chunk) { + MultipartSPH$.MODULE$.onChunkWatch(player, chunk); + } + + public static void onChunkUnWatch(EntityPlayer player, ChunkCoordIntPair chunk) { + MultipartSPH$.MODULE$.onChunkUnWatch(player, chunk); + } + + public static PacketCustom getDescPacket(Chunk chunk, Iterator tiles) { + return MultipartSPH$.MODULE$.getDescPacket(chunk, tiles); + } +} diff --git a/src/main/scala/codechicken/multipart/handler/packethandlers.scala b/src/main/scala/codechicken/multipart/handler/packethandlers.scala deleted file mode 100644 index 6ff2464..0000000 --- a/src/main/scala/codechicken/multipart/handler/packethandlers.scala +++ /dev/null @@ -1,243 +0,0 @@ -package codechicken.multipart.handler - -import codechicken.lib.packet.PacketCustom.{ - IHandshakeHandler, - IClientPacketHandler, - IServerPacketHandler -} -import codechicken.lib.packet.PacketCustom -import net.minecraft.client.Minecraft -import codechicken.multipart.MultiPartRegistry -import net.minecraft.entity.player.EntityPlayerMP -import codechicken.multipart.ControlKeyModifer -import net.minecraft.world.World -import net.minecraft.world.chunk.Chunk -import java.util.{Map => JMap} -import java.util.Iterator -import net.minecraft.tileentity.TileEntity -import codechicken.multipart.TileMultipart -import net.minecraft.entity.player.EntityPlayer -import scala.collection.mutable.{Map, Set, HashMap, MultiMap} -import codechicken.lib.vec.BlockCoord -import codechicken.lib.data.MCDataOutputWrapper -import java.io.DataOutputStream -import java.io.ByteArrayOutputStream -import net.minecraft.world.ChunkCoordIntPair -import codechicken.multipart.PacketScheduler -import java.util.LinkedList -import scala.collection.JavaConversions._ -import net.minecraft.network.play.{INetHandlerPlayServer, INetHandlerPlayClient} -import net.minecraft.network.play.server.S40PacketDisconnect -import net.minecraft.util.{ChatComponentText, ChatComponentTranslation} -import net.minecraft.network.NetHandlerPlayServer - -class MultipartPH { - val channel = MultipartMod$.MODULE$ - val registryChannel = "ForgeMultipart" -} - -object MultipartCPH extends MultipartPH with IClientPacketHandler { - def handlePacket( - packet: PacketCustom, - mc: Minecraft, - netHandler: INetHandlerPlayClient - ) { - try { - packet.getType match { - case 1 => handlePartRegistration(packet, netHandler) - case 2 => handleCompressedTileDesc(packet, mc.theWorld) - case 3 => handleCompressedTileData(packet, mc.theWorld) - } - } catch { - case e: RuntimeException - if e.getMessage != null && e.getMessage.startsWith("DC: ") => - netHandler.handleDisconnect( - new S40PacketDisconnect( - new ChatComponentText(e.getMessage.substring(4)) - ) - ) - } - } - - def handlePartRegistration( - packet: PacketCustom, - netHandler: INetHandlerPlayClient - ) { - val missing = MultiPartRegistry.readIDMap(packet) - if (!missing.isEmpty) - netHandler.handleDisconnect( - new S40PacketDisconnect( - new ChatComponentTranslation( - "multipart.missing", - missing.mkString(", ") - ) - ) - ) - } - - def handleCompressedTileDesc(packet: PacketCustom, world: World) { - val cc = new ChunkCoordIntPair(packet.readInt, packet.readInt) - val num = packet.readUShort - for (i <- 0 until num) - TileMultipart.handleDescPacket( - world, - new BlockCoord( - packet.readByte + (cc.chunkXPos << 4), - packet.readInt, - packet.readByte + (cc.chunkZPos << 4) - ), - packet - ) - } - - def handleCompressedTileData(packet: PacketCustom, world: World) { - var x = packet.readInt - while (x != Int.MaxValue) { - val pos = new BlockCoord(x, packet.readInt, packet.readInt) - var i = packet.readUByte - while (i < 255) { - TileMultipart.handlePacket(pos, world, i, packet) - i = packet.readUByte - } - x = packet.readInt - } - } -} - -object MultipartSPH - extends MultipartPH - with IServerPacketHandler - with IHandshakeHandler { - class MCByteStream(bout: ByteArrayOutputStream) - extends MCDataOutputWrapper(new DataOutputStream(bout)) { - def getBytes = bout.toByteArray - } - - private val updateMap = Map[World, Map[BlockCoord, MCByteStream]]() - - /** These maps are keyed by entityID so that new player instances with the - * same entity id don't conflict world references - */ - private val chunkWatchers = new HashMap[Int, Set[ChunkCoordIntPair]] - with MultiMap[Int, ChunkCoordIntPair] - private val newWatchers = Map[Int, LinkedList[ChunkCoordIntPair]]() - - def handlePacket( - packet: PacketCustom, - sender: EntityPlayerMP, - netHandler: INetHandlerPlayServer - ) { - packet.getType match { - case 1 => ControlKeyModifer.map.put(sender, packet.readBoolean) - } - } - - def handshakeRecieved(netHandler: NetHandlerPlayServer) { - val packet = new PacketCustom(registryChannel, 1) - MultiPartRegistry.writeIDMap(packet) - netHandler.sendPacket(packet.toPacket) - } - - def onWorldUnload(world: World) { - if (!world.isRemote) - updateMap.remove(world) - } - - def getTileStream(world: World, pos: BlockCoord) = - updateMap - .getOrElseUpdate( - world, { - if (world.isRemote) - throw new IllegalArgumentException( - "Cannot use MultipartSPH on a client world" - ) - Map() - } - ) - .getOrElseUpdate( - pos, { - val s = new MCByteStream(new ByteArrayOutputStream) - s.writeCoord(pos) - s - } - ) - - def onTickEnd(players: Seq[EntityPlayerMP]) { - PacketScheduler.sendScheduled() - - for (p <- players if chunkWatchers.containsKey(p.getEntityId)) { - updateMap.get(p.worldObj) match { - case Some(m) if !m.isEmpty => - val chunks = chunkWatchers(p.getEntityId) - val packet = new PacketCustom(channel, 3).compress() - var send = false - for ( - (pos, stream) <- m - if chunks(new ChunkCoordIntPair(pos.x >> 4, pos.z >> 4)) - ) { - send = true - packet.writeByteArray(stream.getBytes) - packet.writeByte(255) // terminator - } - if (send) { - packet.writeInt(Int.MaxValue) // terminator - packet.sendToPlayer(p) - } - case _ => - } - } - updateMap.foreach(_._2.clear()) - for (p <- players if newWatchers.containsKey(p.getEntityId)) { - for (c <- newWatchers(p.getEntityId)) { - val chunk = p.worldObj.getChunkFromChunkCoords(c.chunkXPos, c.chunkZPos) - val pkt = getDescPacket( - chunk, - chunk.chunkTileEntityMap - .asInstanceOf[JMap[_, TileEntity]] - .values - .iterator - ) - if (pkt != null) pkt.sendToPlayer(p) - chunkWatchers.addBinding(p.getEntityId, c) - } - } - newWatchers.clear() - } - - def onChunkWatch(p: EntityPlayer, c: ChunkCoordIntPair) { - newWatchers.getOrElseUpdate(p.getEntityId, new LinkedList).add(c) - } - - def onChunkUnWatch(p: EntityPlayer, c: ChunkCoordIntPair) { - newWatchers.get(p.getEntityId) match { - case Some(chunks) => chunks.remove(c) - case _ => - } - chunkWatchers.removeBinding(p.getEntityId, c) - } - - def getDescPacket(chunk: Chunk, it: Iterator[TileEntity]): PacketCustom = { - val s = new MCByteStream(new ByteArrayOutputStream) - - var num = 0 - while (it.hasNext) { - val tile = it.next - if (tile.isInstanceOf[TileMultipart]) { - s.writeByte(tile.xCoord & 0xf) - s.writeInt(tile.yCoord) - s.writeByte(tile.zCoord & 0xf) - tile.asInstanceOf[TileMultipart].writeDesc(s) - num += 1 - } - } - if (num != 0) { - return new PacketCustom(channel, 2) - .compress() - .writeInt(chunk.xPosition) - .writeInt(chunk.zPosition) - .writeShort(num) - .writeByteArray(s.getBytes) - } - return null - } -}