Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compact chunks #86

Merged
merged 7 commits into from
Jun 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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