refactor: port multipart packet handlers

This commit is contained in:
Algent committed 2026-09-01 00:50:14 +02:00
1 parent bed000cacf
commit fdfd7763dd
10 files changed
+538 -257

No files matched your search

+9
View File
@@ -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.
+58
View File
@@ -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.
+18 -14
View File
@@ -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'"' '/<testsuite /{t+=$4;f+=$8;e+=$10} END{print "tests="t" failures="f" e
grep -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' run/server/junit-out/TEST-*.xml
```
Current baseline: **191 plain-JVM tests and 98 Java 8 Forge dedicated-server tests passing.** The ignored local server
Current baseline: **202 plain-JVM tests and 98 Java 8 Forge dedicated-server tests passing.** The ignored local server
EULA is accepted in this checkout.
### ABI diff against the reference
@@ -249,8 +249,8 @@ All eight load-bearing `$class` helpers from the inventory, both registries, and
`TRedstoneTile`, `TTileChangeTile`, `TFluidHandlerTile`, `TIInventoryTile`/`JInventoryTile`, `TileMultipartClient`, and
`TRandomDisplayTickTile` Java-trait ports, plus `MultipartCompatiblity`/`MCPCCompatModule`, `MultipartMod`,
`MultipartEventHandler`, `MicroblockMod`, `MicroblockEventHandler`, and the complete `MicroblockPH`/
`MicroblockCPH`/`MicroblockSPH` packet-handler unit, plus `MultipartSaveLoad`, `MissingMicroMaterial`,
`DefaultContent`, and `GrassMicroMaterial`/`TopMicroMaterial`.
`MicroblockCPH`/`MicroblockSPH` and `MultipartPH`/`MultipartCPH`/`MultipartSPH` packet-handler units, plus
`MultipartSaveLoad`, `MissingMicroMaterial`, `DefaultContent`, and `GrassMicroMaterial`/`TopMicroMaterial`.
The complete `MultipartProxy` and `MicroblockProxy` server/client hierarchies and static facades are also Java.
Plus the six marker interfaces: `TSlottedPart`, `IRandomDisplayTick`, `INeighborTileChange`, `TRandomUpdateTick`,
@@ -266,7 +266,7 @@ across the port. It is also where the two shim constraints in the gotchas list w
`rayTraceAll`'s index production is now characterized, closing the gap the read-path cleanup left: the index written
into `ExtendedMOP.data` is what `reduceMOP` hands back to every click, activate, harvest and pick block.
125 Java files, 26 Scala files, ~4,519 Scala lines left (non-blank; that is the metric this figure has always used).
130 Java files, 25 Scala files, ~4,295 Scala lines left (non-blank; that is the metric this figure has always used).
## What is left, and in what order
@@ -415,19 +415,23 @@ field-only side annotation; Forge strips the two lifecycle overrides and facade
falls back to the server implementation. Actual item-renderer registration, client packets and Angelica integration
remain on the manual checklist.
**Medium.** The remaining multipart packet handler, `ItemMicroPart`,
`MicroblockPlacement`, `PlacementGrids`, `BlockMicroMaterial`, `ConfigContent`,
and the remaining material/render units.
The ForgeMultipart packet-handler unit is now Java. Its shared base, both static facades, both companion singletons,
nested byte stream, `MultipartMod$` channel descriptor and all three mangled state accessors retain their exact public
or reflective shapes. Eleven plain-JVM tests freeze the ABI, ordered registry disconnect, desync disconnect, control
packet, coordinate stream and both update terminators; existing Forge tests retain unload cleanup, deferred watcher
promotion and exact chunk-description framing. Direct loops replace fourteen unreferenced Scala closure/anonymous
classes. Real client description/update application remains on the manual checklist.
**Medium.** `ItemMicroPart`, `MicroblockPlacement`, `PlacementGrids`, `BlockMicroMaterial`, `ConfigContent`, and the
remaining material/render units.
**Phase 5 is complete.** There are no Scala files left in `multipart/scalatraits/`. The client pair required the first
narrow generator relaxation: Java-trait parent linearization, explicit field-accessor recognition, and exclusion of
transient runtime caches from generated copying.
Take `multipart/handler/packethandlers.scala` next. At 224 nonblank lines it is the last Scala file in either handler
package and owns central registry, tile-description, tile-update and chunk-watch traffic. Freeze all facade/companion
interfaces, the `MultipartMod$.MODULE$` channel descriptor, private prefixed watcher/update-map accessors already used
by tests, disconnect behavior, packet framing/terminators, unload cleanup and watch/tick ordering. Keep real client
decode/render integration on the manual checklist, and keep the characterization and port commits separate.
Take `microblock/ItemMicroPart.scala` next. It is the smallest remaining self-contained medium-risk gameplay unit and
owns item placement, creative subitems, names/icons and microblock recipe lookup. Freeze its class hierarchy, public
surface, placement dispatch, shape/material damage packing and client-only icon/subitem behavior before porting it.
**Phase 6/7, last.** `Microblock` and the microblock shape hierarchy, `MicroblockGenerator`, `MultipartGenerator`, and
all of `multipart/asm/`. The ASM subsystem should be last; freeze generated-class fixtures before touching it.
+2
View File
@@ -23,6 +23,7 @@ release that includes those ports.
| Change a mixed multipart tile containing static and dynamically rendered parts | Static parts remain in the block render, dynamic parts remain in the TESR pass, and neither disappears after a part update | `TileMultipartClient` render caches |
| Watch a multipart torch long enough to emit display particles | Only parts implementing `IRandomDisplayTick` emit particles, with no duplicate or missing callbacks | `TRandomDisplayTickTile.randomDisplayTick` |
| Start a client, join a server, place/update a multipart and use the multipart control key | Block and generated-tile rendering work, client packets arrive, and control-key state changes are sent once without duplicate registrations | `MultipartProxy_clientImpl.postInit` / `onTileClassBuilt` |
| Move into view of a chunk containing several multipart tiles, then add, update and remove parts | The initial chunk description reconstructs every tile and compressed updates apply without missing, duplicated or ghost parts | `MultipartCPH` / `MultipartSPH` |
| Start a client with 3D saws enabled, inspect all three saw tiers and the microblock item, then repeat with Angelica installed | Each renderer is registered once, saw models use the correct tier, microblock icons load, client packets register, and the Angelica hook initializes without errors | `MicroblockProxy_clientImpl.init` / `postInit` |
## Placement and interaction
@@ -50,6 +51,7 @@ release that includes those ports.
| Inspect a mixed multipart tile with Waila | Providers receive every saved part ID and show the expected part data | reflected block identity and tile NBT `parts`/`id` layout |
| Load a world saved before the port | All microblocks and covers keep their material and shape | `MicroMaterialRegistry` id map |
| Join a server whose material set differs from the client | The client is disconnected with the missing material list, not a crash | `readIDMap` |
| Join a server whose multipart type set differs from the client | The client is disconnected with the ordered missing-part list, not a crash | `MultipartCPH.handlePartRegistration` |
## Not yet covered anywhere
@@ -0,0 +1,88 @@
package codechicken.multipart.handler;
import java.util.List;
import net.minecraft.client.Minecraft;
import net.minecraft.network.play.INetHandlerPlayClient;
import net.minecraft.network.play.server.S40PacketDisconnect;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.world.ChunkCoordIntPair;
import net.minecraft.world.World;
import codechicken.lib.packet.PacketCustom;
import codechicken.lib.packet.PacketCustom.IClientPacketHandler;
import codechicken.lib.vec.BlockCoord;
import codechicken.multipart.MultiPartRegistry;
import codechicken.multipart.TileMultipart;
import scala.MatchError;
public final class MultipartCPH$ extends MultipartPH implements IClientPacketHandler {
public static final MultipartCPH$ MODULE$ = new MultipartCPH$();
private MultipartCPH$() {}
@Override
public void handlePacket(PacketCustom packet, Minecraft minecraft, INetHandlerPlayClient netHandler) {
try {
int type = packet.getType();
switch (type) {
case 1:
handlePartRegistration(packet, netHandler);
return;
case 2:
handleCompressedTileDesc(packet, minecraft.theWorld);
return;
case 3:
handleCompressedTileData(packet, minecraft.theWorld);
return;
default:
throw new MatchError(type);
}
} catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().startsWith("DC: ")) {
netHandler
.handleDisconnect(new S40PacketDisconnect(new ChatComponentText(e.getMessage().substring(4))));
return;
}
throw e;
}
}
public void handlePartRegistration(PacketCustom packet, INetHandlerPlayClient netHandler) {
List<String> 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();
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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<World, Map<BlockCoord, MultipartSPH.MCByteStream>> codechicken$multipart$handler$MultipartSPH$$updateMap = new HashMap<>();
private final HashMap<Object, Set<ChunkCoordIntPair>> codechicken$multipart$handler$MultipartSPH$$chunkWatchers = new HashMap<>();
private final Map<Object, LinkedList<ChunkCoordIntPair>> codechicken$multipart$handler$MultipartSPH$$newWatchers = new HashMap<>();
private MultipartSPH$() {}
public Map<World, Map<BlockCoord, MultipartSPH.MCByteStream>> codechicken$multipart$handler$MultipartSPH$$updateMap() {
return codechicken$multipart$handler$MultipartSPH$$updateMap;
}
public HashMap<Object, Set<ChunkCoordIntPair>> codechicken$multipart$handler$MultipartSPH$$chunkWatchers() {
return codechicken$multipart$handler$MultipartSPH$$chunkWatchers;
}
public Map<Object, LinkedList<ChunkCoordIntPair>> 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<Map<BlockCoord, MultipartSPH.MCByteStream>> worldOption = codechicken$multipart$handler$MultipartSPH$$updateMap
.get(world);
Map<BlockCoord, MultipartSPH.MCByteStream> 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<MultipartSPH.MCByteStream> 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<EntityPlayerMP> players) {
PacketScheduler.sendScheduled();
scala.collection.Iterator<EntityPlayerMP> playerIterator = players.iterator();
while (playerIterator.hasNext()) {
EntityPlayerMP player = playerIterator.next();
Object playerId = player.getEntityId();
Option<Set<ChunkCoordIntPair>> chunkOption = codechicken$multipart$handler$MultipartSPH$$chunkWatchers
.get(playerId);
if (chunkOption.isEmpty()) {
continue;
}
Option<Map<BlockCoord, MultipartSPH.MCByteStream>> updatesOption = codechicken$multipart$handler$MultipartSPH$$updateMap
.get(player.worldObj);
if (updatesOption.isEmpty() || updatesOption.get().isEmpty()) {
continue;
}
Set<ChunkCoordIntPair> chunks = chunkOption.get();
PacketCustom packet = new PacketCustom(channel(), 3).compress();
boolean send = false;
scala.collection.Iterator<Tuple2<BlockCoord, MultipartSPH.MCByteStream>> updateIterator = updatesOption
.get().iterator();
while (updateIterator.hasNext()) {
Tuple2<BlockCoord, MultipartSPH.MCByteStream> 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<Tuple2<World, Map<BlockCoord, MultipartSPH.MCByteStream>>> 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<LinkedList<ChunkCoordIntPair>> 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<TileEntity> tiles = ((java.util.Map<Object, TileEntity>) 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<LinkedList<ChunkCoordIntPair>> option = codechicken$multipart$handler$MultipartSPH$$newWatchers
.get(playerId);
LinkedList<ChunkCoordIntPair> 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<LinkedList<ChunkCoordIntPair>> pending = codechicken$multipart$handler$MultipartSPH$$newWatchers
.get(playerId);
if (!pending.isEmpty()) {
pending.get().remove(chunk);
}
Option<Set<ChunkCoordIntPair>> active = codechicken$multipart$handler$MultipartSPH$$chunkWatchers.get(playerId);
if (!active.isEmpty()) {
Set<ChunkCoordIntPair> chunks = active.get();
chunks.remove(chunk);
if (chunks.isEmpty()) {
codechicken$multipart$handler$MultipartSPH$$chunkWatchers.remove(playerId);
}
}
}
public PacketCustom getDescPacket(Chunk chunk, Iterator<TileEntity> 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<Set<ChunkCoordIntPair>> option = codechicken$multipart$handler$MultipartSPH$$chunkWatchers.get(playerId);
Set<ChunkCoordIntPair> chunks;
if (option.isEmpty()) {
chunks = new HashSet<>();
codechicken$multipart$handler$MultipartSPH$$chunkWatchers.put(playerId, chunks);
} else {
chunks = option.get();
}
chunks.add(chunk);
}
}
@@ -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<EntityPlayerMP> 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<TileEntity> tiles) {
return MultipartSPH$.MODULE$.getDescPacket(chunk, tiles);
}
}
@@ -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
}
}