Skip to content

Commit

Permalink
Send compact chunks on initial world load for better performance (#86)
Browse files Browse the repository at this point in the history
* Add chunk loading functions based on ThinkMap

* Write block types to a buffer and gzip it

* Send compressed initial chunk update (b, command with deflate).

Depends on satoshinm/NetCraft#173

* Change compact chunk order to ZYX, about 10% smaller

See #86 (comment)

* Cleanup logging

* Clarify block data update variable name

* Fix block lighting updates
  • Loading branch information
satoshinm authored Jun 3, 2017
1 parent 7769caf commit 6f3a71e
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 34 deletions.
142 changes: 108 additions & 34 deletions src/main/java/io/github/satoshinm/WebSandboxMC/bridge/BlockBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.github.satoshinm.WebSandboxMC.Settings;
import io.github.satoshinm.WebSandboxMC.ws.WebSocketServerThread;
import io.netty.buffer.*;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import org.bukkit.*;
Expand All @@ -11,11 +12,12 @@
import org.bukkit.block.Sign;
import org.bukkit.material.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

/**
* Bridges blocks in the world, translates between coordinate systems
Expand Down Expand Up @@ -116,25 +118,87 @@ public BlockBridge(WebSocketServerThread webSocketServerThread, Settings setting
this.textureURL = settings.textureURL;
}

private final ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;

// Send the client the initial section of the world when they join
public void sendWorld(final Channel channel) {
if (textureURL != null) {
webSocketServerThread.sendLine(channel, "t," + textureURL);
}

// Send a multi-block update message announcement that a binary chunk is coming
/*
int startx = -radius;
int starty = y_offset;
int startz = -radius;
int endx = radius - 1;
int endy = radius * 2 - 1 + y_offset;
int endz = radius - 1;
*/
int startx = 0;
int starty = y_offset;
int startz = 0;
int endx = radius * 2 - 1;
int endy = radius * 2 - 1 + y_offset;
int endz = radius * 2 - 1;

webSocketServerThread.sendLine(channel, "b," + startx + "," + starty + "," + startz + "," + endx + "," + endy + "," + endz);

ByteBuf data = allocator.buffer( (radius*2) * (radius*2) * (radius*2) * 2);

boolean thereIsAWorld = false;
// TODO: bulk block update compressed, for efficiency (this is very efficient, but surprisingly works!)
LinkedList<String> blockDataUpdates = new LinkedList<String>();
int offset = 0;
// Gather block data for multiblock update compression
for (int i = -radius; i < radius; ++i) {
for (int j = -radius; j < radius; ++j) {
for (int k = -radius; k < radius; ++k) {
Block block = world.getBlockAt(i + x_center, j + y_center, k + z_center);
//int type = toWebBlockType(block.getType(), block.getData());
Block block = world.getBlockAt(j + x_center, i + y_center, k + z_center);

//webSocketServerThread.sendLine(channel, "B,0,0," + (i + radius) + "," + (j + radius + y_offset) + "," + (k + radius) + "," + type);
thereIsAWorld |= setBlockUpdate(block.getLocation(), block.getType(), block.getState());
Material material = block.getType();
BlockState blockState = block.getState();

int type = toWebBlockType(material, blockState);
data.setShortLE(offset, (short) type);
offset += 2;

// Gather block data updates
String blockDataCommand = getDataBlockUpdateCommand(block.getLocation(), material, blockState);

if (type != 0) thereIsAWorld = true;
if (blockDataCommand != null) blockDataUpdates.add(blockDataCommand);
}
}
}
data.writerIndex(data.capacity());

// Send compressed block types
try {
// Compress with DeflateOutputStream, note _not_ GZIPOutputStream since that adds
// gzip headers (see https://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib)
// which miniz does not support (https://github.com/richgel999/miniz/blob/ec028ffe66e2da67eed208de3db66fcf72b24dac/miniz.h#L33)
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DeflaterOutputStream gzipOutputStream = new DeflaterOutputStream(byteArrayOutputStream);
byte[] bytes = new byte[data.readableBytes()];
data.readBytes(bytes);
gzipOutputStream.write(bytes);
gzipOutputStream.close();

byte[] gzipBytes = byteArrayOutputStream.toByteArray();
ByteBuf gzipBytesBuffer = Unpooled.wrappedBuffer(gzipBytes);
webSocketServerThread.sendBinary(channel, gzipBytesBuffer);
} catch (IOException ex) {
webSocketServerThread.log(Level.WARNING, "Failed to compress chunk data to send to web client: "+ex);
throw new RuntimeException(ex);
} finally {
data.release();
}

// then block data and refresh
for (String blockDataCommand : blockDataUpdates) {
webSocketServerThread.sendLine(channel, blockDataCommand);
}

webSocketServerThread.sendLine(channel,"K,0,0,1");
webSocketServerThread.sendLine(channel, "R,0,0");

Expand Down Expand Up @@ -269,46 +333,52 @@ public void notifyBlockUpdate(Location location, Material material, BlockState b
webSocketServerThread.broadcastLine("R,0,0");
}

private boolean setBlockUpdate(Location location, Material material, BlockState blockState) {
// Get the command string to send block data besides the type, if needed (signs, lighting)
private String getDataBlockUpdateCommand(Location location, Material material, BlockState blockState) {
if (material == null || material == Material.AIR) return null;

int light_level = toWebLighting(material, blockState);
if (light_level != 0) {
int x = toWebLocationBlockX(location);
int y = toWebLocationBlockY(location);
int z = toWebLocationBlockZ(location);
return "L,0,0,"+x+","+y+","+z+"," + light_level;
}

if (material == Material.WALL_SIGN || material == Material.SIGN_POST) {
Block block = location.getWorld().getBlockAt(location);
if (blockState instanceof Sign) {
Sign sign = (Sign) blockState;

return getNotifySignChange(block.getLocation(), block.getType(), block.getState(), sign.getLines());
}
}

return null;
}

private void setBlockUpdate(Location location, Material material, BlockState blockState) {
// Send to all web clients to let them know it changed using the "B," command
int type = toWebBlockType(material, blockState);
boolean substantial;

if (type == -1) {
if (warnMissing) {
webSocketServerThread.log(Level.WARNING, "Block type missing from blocks_to_web: " + material + " at " + location);
}
type = blocksToWebMissing;
substantial = false;
} else if (type == 0) {
substantial = false;
} else {
substantial = true;
}

int x = toWebLocationBlockX(location);
int y = toWebLocationBlockY(location);
int z = toWebLocationBlockZ(location);

webSocketServerThread.broadcastLine("B,0,0,"+x+","+y+","+z+","+type);

int light_level = toWebLighting(material, blockState);
if (light_level != 0) {
webSocketServerThread.broadcastLine("L,0,0,"+x+","+y+","+z+"," + light_level);
}

if (material == Material.WALL_SIGN || material == Material.SIGN_POST) {
Block block = location.getWorld().getBlockAt(location);
if (blockState instanceof Sign) {
Sign sign = (Sign) blockState;

notifySignChange(block.getLocation(), block.getType(), block.getState(), sign.getLines());
}
String blockDataCommand = this.getDataBlockUpdateCommand(location, material, blockState);
if (blockDataCommand != null) {
webSocketServerThread.broadcastLine(blockDataCommand);
}

webSocketServerThread.log(Level.FINEST, "notified block update: ("+x+","+y+","+z+") to "+type);

return substantial; // was something "real" set? (not air, not missing)
}

private int toWebLighting(Material material, BlockState blockState) {
Expand Down Expand Up @@ -784,7 +854,7 @@ private void toBukkitBlockType(int type, BlockState blockState) {
}
}

public void notifySignChange(Location location, Material material, BlockState blockState, String[] lines) {
public String getNotifySignChange(Location location, Material material, BlockState blockState, String[] lines) {
int x = toWebLocationBlockX(location);
int y = toWebLocationBlockY(location);
int z = toWebLocationBlockZ(location);
Expand Down Expand Up @@ -865,8 +935,12 @@ public void notifySignChange(Location location, Material material, BlockState bl
text = text.replaceAll("\n", " ");
}

webSocketServerThread.broadcastLine("S,0,0,"+x+","+y+","+z+","+face+","+text);
webSocketServerThread.broadcastLine("R,0,0");
return "S,0,0,"+x+","+y+","+z+","+face+","+text;
}

public void notifySignChange(Location location, Material material, BlockState blockState, String[] lines) {
webSocketServerThread.broadcastLine(this.getNotifySignChange(location, material, blockState, lines));
webSocketServerThread.broadcastLine("R,0,0"); // TODO: refresh correct chunk
}

public void clientNewSign(ChannelHandlerContext ctx, int x, int y, int z, int face, String text) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.github.satoshinm.WebSandboxMC.bridge.WebPlayerBridge;
import io.github.satoshinm.WebSandboxMC.bridge.PlayersBridge;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
Expand Down Expand Up @@ -135,6 +136,10 @@ public void sendLine(Channel channel, String message) {
channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer((message + "\n").getBytes())));
}

public void sendBinary(Channel channel, ByteBuf data) {
channel.writeAndFlush(new BinaryWebSocketFrame(data));
}

public void broadcastLine(String message) {
allUsersGroup.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer((message + "\n").getBytes())));
}
Expand Down

0 comments on commit 6f3a71e

Please sign in to comment.