Skip to content

Instantly share code, notes, and snippets.

@aadnk
Last active December 12, 2015 07:09
Show Gist options
  • Save aadnk/4734454 to your computer and use it in GitHub Desktop.
Save aadnk/4734454 to your computer and use it in GitHub Desktop.
Demonstration of how to modify the biome of each chunk.
package com.comphenix.example;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.Deflater;
import org.bukkit.Chunk;
import org.bukkit.World.Environment;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.StructureModifier;
import com.google.common.collect.MapMaker;
public class RandomBiomes extends JavaPlugin implements Listener {
private static final int BYTES_PER_NIBBLE_PART = 2048;
private static final int CHUNK_SEGMENTS = 16;
private static final int NIBBLES_REQUIRED = 4;
private static final int BIOME_ARRAY_LENGTH = 256;
// Look this up in net.minecraft.server.Biome
private static final byte BIOME_HELL = 8;
// Used to pass around detailed information about chunks
private static class ChunkInfo {
public int chunkX;
public int chunkZ;
public int chunkMask;
public int extraMask;
public int chunkSectionNumber;
public int extraSectionNumber;
public boolean hasContinous;
public byte[] data;
public Player player;
public int startIndex;
public int size;
}
private Set<Object> changed = Collections.newSetFromMap(new MapMaker().weakKeys().<Object, Boolean>makeMap());
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player) {
Player player = (Player) sender;
Chunk chunk = player.getLocation().getChunk();
player.getWorld().refreshChunk(chunk.getX(), chunk.getZ());
}
return true;
}
public void onEnable() {
getServer().getPluginManager().registerEvents(this, this);
// Transform packets
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.MAP_CHUNK, PacketType.Play.Server.MAP_CHUNK_BULK) {
@Override
public void onPacketSending(PacketEvent event) {
final PacketType type = event.getPacketType();
if (type == PacketType.Play.Server.MAP_CHUNK) {
translateMapChunk(event.getPacket(), event.getPlayer());
} else if (type == PacketType.Play.Server.MAP_CHUNK_BULK) {
translateMapChunkBulk(event.getPacket(), event.getPlayer());
}
}
});
// Finalize packets
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, ListenerPriority.MONITOR, PacketType.Play.Server.MAP_CHUNK) {
@Override
public void onPacketSending(PacketEvent event) {
finalizeMapChunk(event.getPacket());
}
});
}
private void finalizeMapChunk(PacketContainer packet) {
// Ensure the packet has changed
if (changed.remove(packet.getHandle())) {
StructureModifier<Integer> ints = packet.getIntegers();
StructureModifier<byte[]> byteArray = packet.getByteArrays();
Deflater localDeflater = new Deflater(-1);
byte[] output = byteArray.read(0);
byte[] data = byteArray.read(1);
// Occurs if the packet has already been compressed
if (data == null) {
return;
}
try {
localDeflater.setInput(data, 0, data.length);
localDeflater.finish();
ints.write(4, localDeflater.deflate(output));
// Indicate that the packet has been compressed
byteArray.write(1, null);
} finally {
localDeflater.end();
}
}
}
public void translateMapChunk(PacketContainer packet, Player player) throws FieldAccessException {
StructureModifier<Integer> ints = packet.getIntegers();
StructureModifier<byte[]> byteArray = packet.getByteArrays();
// Create an info objects
ChunkInfo info = new ChunkInfo();
info.player = player;
info.chunkX = ints.read(0); // packet.a;
info.chunkZ = ints.read(1); // packet.b;
info.chunkMask = ints.read(2); // packet.c;
info.extraMask = ints.read(3); // packet.d;
info.hasContinous = getOrDefault(packet.getBooleans().readSafely(0), true);
info.data = byteArray.read(1); // packet.inflatedBuffer;
info.startIndex = 0;
if (info.data != null) {
if (translateChunkInfo(info, info.data)) {
changed.add(packet.getHandle());
}
}
}
// Mimic the ?? operator in C#
private <T> T getOrDefault(T value, T defaultIfNull) {
return value != null ? value : defaultIfNull;
}
public void translateMapChunkBulk(PacketContainer packet, Player player) throws FieldAccessException {
StructureModifier<int[]> intArrays = packet.getIntegerArrays();
StructureModifier<byte[]> byteArrays = packet.getSpecificModifier(byte[].class);
int[] x = intArrays.read(0); // getPrivateField(packet, "c");
int[] z = intArrays.read(1); // getPrivateField(packet, "d");
ChunkInfo[] infos = new ChunkInfo[x.length];
int dataStartIndex = 0;
int[] chunkMask = intArrays.read(2); // packet.a;
int[] extraMask = intArrays.read(3); // packet.b;
for (int chunkNum = 0; chunkNum < infos.length; chunkNum++) {
// Create an info objects
ChunkInfo info = new ChunkInfo();
infos[chunkNum] = info;
info.player = player;
info.chunkX = x[chunkNum];
info.chunkZ = z[chunkNum];
info.chunkMask = chunkMask[chunkNum];
info.extraMask = extraMask[chunkNum];
info.hasContinous = true; // Always TRUE here
info.data = byteArrays.read(1); //packet.buildBuffer;
// Check for Spigot
if (info.data == null || info.data.length == 0) {
info.data = packet.getSpecificModifier(byte[][].class).read(0)[chunkNum];
} else {
info.startIndex = dataStartIndex;
}
translateChunkInfo(info, info.data);
dataStartIndex += info.size;
}
}
private boolean translateChunkInfo(ChunkInfo info, byte[] returnData) {
// Compute chunk number
for (int i = 0; i < CHUNK_SEGMENTS; i++) {
if ((info.chunkMask & (1 << i)) > 0) {
info.chunkSectionNumber++;
}
if ((info.extraMask & (1 << i)) > 0) {
info.extraSectionNumber++;
}
}
// There's no sun/moon in the end or in the nether, so Minecraft doesn't sent any skylight information
// This optimization was added in 1.4.6. Note that ideally you should get this from the "f" (skylight) field.
int skylightCount = info.player.getWorld().getEnvironment() == Environment.NORMAL ? 1 : 0;
// To calculate the size of each chunk, we need to take into account the number of segments (out of 16)
// that have been sent. Each segment sent is encoded in the chunkMask bit field, where every binary 1
// indicates that a segment is present and every 0 indicates that it's not.
// The total size of a chunk is the number of blocks sent (depends on the number of sections) multiplied by the
// amount of bytes per block. This last figure can be calculated by adding together all the data parts:
// For any block:
// * Block ID - 8 bits per block (byte)
// * Block metadata - 4 bits per block (nibble)
// * Block light array - 4 bits per block
// If 'worldProvider.skylight' is TRUE
// * Sky light array - 4 bits per block
// If the segment has extra data:
// * Add array - 4 bits per block
// Biome array - only if the entire chunk (has continous) is sent:
// * Biome array - 256 bytes
//
// A section has 16 * 16 * 16 = 4096 blocks.
info.size = BYTES_PER_NIBBLE_PART * (
(NIBBLES_REQUIRED + skylightCount) * info.chunkSectionNumber +
info.extraSectionNumber) +
(info.hasContinous ? BIOME_ARRAY_LENGTH : 0);
if (info.hasContinous) {
int biomeStart = info.startIndex + info.size - BIOME_ARRAY_LENGTH;
byte value = (byte) (Math.abs(info.chunkX + info.chunkZ) % 22);
for (int i = 0; i < BIOME_ARRAY_LENGTH; i++) {
info.data[biomeStart + i] = value;
}
// If has changed
return true;
}
return false;
}
}
@fhntv24
Copy link

fhntv24 commented Aug 13, 2015

Use PacketWrapper.
Also in 1.8 world.updateChunk is buggy i think.

@Skyost
Copy link

Skyost commented Aug 13, 2015

Yeah, thanks. I've updated it, as you can see here.
Edit : And yeah, world.updateChunk(...) is buggy so I've made this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment