Last active
December 12, 2015 07:09
-
-
Save aadnk/4734454 to your computer and use it in GitHub Desktop.
Demonstration of how to modify the biome of each chunk.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use PacketWrapper.
Also in 1.8 world.updateChunk is buggy i think.