Rewrite monitor networking (#453)

This moves monitor networking into its own packet, rather than serialising
using NBT. This allows us to be more flexible with how monitors are
serialised.

We now compress terminal data using gzip. This reduces the packet size
of a max-sized-monitor from ~25kb to as little as 100b.

On my test set of images (what I would consider to be the extreme end of
the "reasonable" case), we have packets from 1.4kb bytes up to 12kb,
with a mean of 6kb. Even in the worst case, this is a 2x reduction in
packet size.

While this is a fantastic win for the common case, it is not abuse-proof.
One can create a terminal with high entropy (and so uncompressible). This
will still be close to the original packet size.

In order to prevent any other abuse, we also limit the amount of monitor
data a client can possibly receive to 1MB (configurable).
This commit is contained in:
Jonathan Coates 2020-05-20 08:44:44 +01:00 committed by GitHub
parent 161a5b4707
commit d50a08a549
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 507 additions and 72 deletions

View File

@ -138,6 +138,7 @@ public class ComputerCraft
public static int modem_highAltitudeRangeDuringStorm = 384;
public static int maxNotesPerTick = 8;
public static MonitorRenderer monitorRenderer = MonitorRenderer.BEST;
public static long monitorBandwidth = 1_000_000;
public static boolean turtlesNeedFuel = true;
public static int turtleFuelLimit = 20000;
@ -537,7 +538,7 @@ public void close() throws IOException
}
catch( IOException e )
{
if( zipFile != null ) IoUtil.closeQuietly( zipFile );
IoUtil.closeQuietly( zipFile );
}
}
}

View File

@ -37,12 +37,8 @@ protected final void close()
{
m_open = false;
Closeable closeable = m_closable;
if( closeable != null )
{
IoUtil.closeQuietly( closeable );
m_closable = null;
}
IoUtil.closeQuietly( m_closable );
m_closable = null;
}
/**

View File

@ -106,7 +106,7 @@ public boolean queue( Consumer<T> task )
protected static <T extends Closeable> T closeCloseable( T closeable )
{
if( closeable != null ) IoUtil.closeQuietly( closeable );
IoUtil.closeQuietly( closeable );
return null;
}

View File

@ -221,7 +221,7 @@ protected void dispose()
WeakReference<WebsocketHandle> websocketHandleRef = websocketHandle;
WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
if( websocketHandle != null ) IoUtil.closeQuietly( websocketHandle );
IoUtil.closeQuietly( websocketHandle );
this.websocketHandle = null;
}

View File

@ -366,8 +366,7 @@ private void cleanup()
Reference<?> ref;
while( (ref = m_openFileQueue.poll()) != null )
{
Closeable file = m_openFiles.remove( ref );
if( file != null ) IoUtil.closeQuietly( file );
IoUtil.closeQuietly( m_openFiles.remove( ref ) );
}
}
}

View File

@ -72,6 +72,7 @@ public final class Config
private static Property modemHighAltitudeRangeDuringStorm;
private static Property maxNotesPerTick;
private static Property monitorRenderer;
private static Property monitorBandwidth;
private static Property turtlesNeedFuel;
private static Property turtleFuelLimit;
@ -276,10 +277,21 @@ public static void load( File configFile )
"monitors have performance issues, you may wish to experiment with alternative renderers." );
monitorRenderer.setValidValues( MonitorRenderer.NAMES );
monitorBandwidth = config.get( CATEGORY_PERIPHERAL, "monitor_bandwidth", (int) ComputerCraft.monitorBandwidth );
monitorBandwidth.setComment( "The limit to how much monitor data can be sent *per tick*. Note:\n" +
" - Bandwidth is measured before compression, so the data sent to the client is smaller.\n" +
" - This ignores the number of players a packet is sent to. Updating a monitor for one player consumes " +
"the same bandwidth limit as sending to 20.\n" +
" - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40 monitors to be updated " +
"in a single tick. \n" +
"Set to 0 to disable." );
monitorBandwidth.setValidValues( MonitorRenderer.NAMES );
monitorBandwidth.setMinValue( 0 );
setOrder(
CATEGORY_PERIPHERAL,
commandBlockEnabled, modemRange, modemHighAltitudeRange, modemRangeDuringStorm, modemHighAltitudeRangeDuringStorm, maxNotesPerTick,
monitorRenderer
monitorRenderer, monitorBandwidth
);
}
@ -474,6 +486,7 @@ public static void sync()
ComputerCraft.modem_rangeDuringStorm = Math.min( modemRangeDuringStorm.getInt(), MODEM_MAX_RANGE );
ComputerCraft.modem_highAltitudeRangeDuringStorm = Math.min( modemHighAltitudeRangeDuringStorm.getInt(), MODEM_MAX_RANGE );
ComputerCraft.monitorRenderer = MonitorRenderer.ofString( monitorRenderer.getString() );
ComputerCraft.monitorBandwidth = Math.max( 0, monitorBandwidth.getLong() );
// Turtles
ComputerCraft.turtlesNeedFuel = turtlesNeedFuel.getBoolean();

View File

@ -6,9 +6,7 @@
package dan200.computercraft.shared.common;
import dan200.computercraft.core.terminal.Terminal;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.PacketBuffer;
import dan200.computercraft.shared.network.client.TerminalState;
public class ClientTerminal implements ITerminal
{
@ -48,14 +46,13 @@ public boolean isColour()
return m_colour;
}
public void readDescription( NBTTagCompound nbt )
public void read( TerminalState state )
{
m_colour = nbt.getBoolean( "colour" );
if( nbt.hasKey( "terminal" ) )
m_colour = state.colour;
if( state.hasTerminal() )
{
NBTTagCompound terminal = nbt.getCompoundTag( "terminal" );
resizeTerminal( terminal.getInteger( "term_width" ), terminal.getInteger( "term_height" ) );
m_terminal.read( new PacketBuffer( Unpooled.wrappedBuffer( terminal.getByteArray( "term_contents" ) ) ) );
resizeTerminal( state.width, state.height );
state.apply( m_terminal );
}
else
{

View File

@ -5,12 +5,8 @@
*/
package dan200.computercraft.shared.common;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.terminal.Terminal;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.PacketBuffer;
import dan200.computercraft.shared.network.client.TerminalState;
import java.util.concurrent.atomic.AtomicBoolean;
@ -73,8 +69,6 @@ public boolean hasTerminalChanged()
return m_terminalChangedLastFrame;
}
// ITerminal implementation
@Override
public Terminal getTerminal()
{
@ -87,29 +81,8 @@ public boolean isColour()
return m_colour;
}
public void writeDescription( NBTTagCompound nbt )
public TerminalState write()
{
nbt.setBoolean( "colour", m_colour );
if( m_terminal != null )
{
// We have a 10 byte header (2 integer positions, then blinking and current colours), followed by the
// contents and palette.
// Yes, this serialisation code is terrible, but we need to serialise to NBT in order to work with monitors
// (or rather tile entity serialisation).
final int length = 10 + (2 * m_terminal.getWidth() * m_terminal.getHeight()) + (16 * 3);
ByteBuf buffer = Unpooled.buffer( length );
m_terminal.write( new PacketBuffer( buffer ) );
if( buffer.writableBytes() != 0 )
{
ComputerCraft.log.warn( "Should have written {} bytes, but have {} ({} remaining).", length, buffer.writerIndex(), buffer.writableBytes() );
}
NBTTagCompound terminal = new NBTTagCompound();
terminal.setInteger( "term_width", m_terminal.getWidth() );
terminal.setInteger( "term_height", m_terminal.getHeight() );
terminal.setByteArray( "term_contents", buffer.array() );
nbt.setTag( "terminal", terminal );
}
return new TerminalState( m_colour, m_terminal );
}
}

View File

@ -155,9 +155,7 @@ private IMessage createComputerPacket()
protected IMessage createTerminalPacket()
{
NBTTagCompound tagCompound = new NBTTagCompound();
writeDescription( tagCompound );
return new ComputerTerminalClientMessage( getInstanceID(), tagCompound );
return new ComputerTerminalClientMessage( getInstanceID(), write() );
}
public void broadcastState( boolean force )

View File

@ -45,6 +45,7 @@ public static void setup()
registerMainThread( 12, Side.CLIENT, ComputerDeletedClientMessage::new );
registerMainThread( 13, Side.CLIENT, ComputerTerminalClientMessage::new );
registerMainThread( 14, Side.CLIENT, PlayRecordClientMessage::new );
registerMainThread( 15, Side.CLIENT, MonitorClientMessage::new );
}
public static void sendToPlayer( EntityPlayer player, IMessage packet )
@ -67,6 +68,11 @@ public static void sendToAllAround( IMessage packet, NetworkRegistry.TargetPoint
network.sendToAllAround( packet, point );
}
public static void sendToAllTracking( IMessage packet, NetworkRegistry.TargetPoint point )
{
network.sendToAllTracking( packet, point );
}
/**
* /**
* Register packet, and a thread-unsafe handler for it.

View File

@ -5,8 +5,6 @@
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;
@ -14,12 +12,12 @@
public class ComputerTerminalClientMessage extends ComputerClientMessage
{
private NBTTagCompound tag;
private TerminalState state;
public ComputerTerminalClientMessage( int instanceId, NBTTagCompound tag )
public ComputerTerminalClientMessage( int instanceId, TerminalState state )
{
super( instanceId );
this.tag = tag;
this.state = state;
}
public ComputerTerminalClientMessage()
@ -30,19 +28,19 @@ public ComputerTerminalClientMessage()
public void toBytes( @Nonnull PacketBuffer buf )
{
super.toBytes( buf );
buf.writeCompoundTag( tag ); // TODO: Do we need to compress this?
state.write( buf );
}
@Override
public void fromBytes( @Nonnull PacketBuffer buf )
{
super.fromBytes( buf );
tag = NBTUtil.readCompoundTag( buf );
state = new TerminalState( buf );
}
@Override
public void handle( MessageContext context )
{
getComputer().readDescription( tag );
getComputer().read( state );
}
}

View File

@ -0,0 +1,60 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.network.PacketBuffer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;
import javax.annotation.Nonnull;
public class MonitorClientMessage implements NetworkMessage
{
private BlockPos pos;
private TerminalState state;
public MonitorClientMessage( BlockPos pos, TerminalState state )
{
this.pos = pos;
this.state = state;
}
public MonitorClientMessage()
{
}
@Override
public void toBytes( @Nonnull PacketBuffer buf )
{
buf.writeBlockPos( pos );
state.write( buf );
}
@Override
public void fromBytes( @Nonnull PacketBuffer buf )
{
pos = buf.readBlockPos();
state = new TerminalState( buf );
}
@Override
public void handle( MessageContext context )
{
EntityPlayerSP player = Minecraft.getMinecraft().player;
if( player == null || player.world == null ) return;
TileEntity te = player.world.getTileEntity( pos );
if( !(te instanceof TileMonitor) ) return;
((TileMonitor) te).read( state );
}
}

View File

@ -0,0 +1,183 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.util.IoUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import net.minecraft.network.PacketBuffer;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* A snapshot of a terminal's state.
*
* This is somewhat memory inefficient (we build a buffer, only to write it elsewhere), however it means we get a
* complete and accurate description of a terminal, which avoids a lot of complexities with resizing terminals, dirty
* states, etc...
*/
public class TerminalState
{
public final boolean colour;
public final int width;
public final int height;
private final boolean compress;
@Nullable
private final ByteBuf buffer;
private ByteBuf compressed;
public TerminalState( boolean colour, @Nullable Terminal terminal )
{
this( colour, terminal, true );
}
public TerminalState( boolean colour, @Nullable Terminal terminal, boolean compress )
{
this.colour = colour;
this.compress = compress;
if( terminal == null )
{
this.width = this.height = 0;
this.buffer = null;
}
else
{
this.width = terminal.getWidth();
this.height = terminal.getHeight();
ByteBuf buf = this.buffer = Unpooled.buffer();
terminal.write( new PacketBuffer( buf ) );
}
}
public TerminalState( PacketBuffer buf )
{
this.colour = buf.readBoolean();
this.compress = buf.readBoolean();
if( buf.readBoolean() )
{
this.width = buf.readVarInt();
this.height = buf.readVarInt();
int length = buf.readVarInt();
this.buffer = readCompressed( buf, length, compress );
}
else
{
this.width = this.height = 0;
this.buffer = null;
}
}
public void write( PacketBuffer buf )
{
buf.writeBoolean( colour );
buf.writeBoolean( compress );
buf.writeBoolean( buffer != null );
if( buffer != null )
{
buf.writeVarInt( width );
buf.writeVarInt( height );
ByteBuf sendBuffer = getCompressed();
buf.writeVarInt( sendBuffer.readableBytes() );
buf.writeBytes( sendBuffer, sendBuffer.readerIndex(), sendBuffer.readableBytes() );
}
}
public boolean hasTerminal()
{
return buffer != null;
}
public int size()
{
return buffer == null ? 0 : buffer.readableBytes();
}
public void apply( Terminal terminal )
{
if( buffer == null ) throw new NullPointerException( "buffer" );
terminal.read( new PacketBuffer( buffer ) );
}
private ByteBuf getCompressed()
{
if( buffer == null ) throw new NullPointerException( "buffer" );
if( !compress ) return buffer;
if( compressed != null ) return compressed;
ByteBuf compressed = Unpooled.directBuffer();
OutputStream stream = null;
try
{
stream = new GZIPOutputStream( new ByteBufOutputStream( compressed ) );
stream.write( buffer.array(), buffer.arrayOffset(), buffer.readableBytes() );
}
catch( IOException e )
{
throw new UncheckedIOException( e );
}
finally
{
IoUtil.closeQuietly( stream );
}
return this.compressed = compressed;
}
private static ByteBuf readCompressed( ByteBuf buf, int length, boolean compress )
{
if( compress )
{
ByteBuf buffer = Unpooled.buffer();
InputStream stream = null;
try
{
stream = new GZIPInputStream( new ByteBufInputStream( buf, length ) );
byte[] swap = new byte[8192];
while( true )
{
int bytes = stream.read( swap );
if( bytes == -1 ) break;
buffer.writeBytes( swap, 0, bytes );
}
}
catch( IOException e )
{
throw new UncheckedIOException( e );
}
finally
{
IoUtil.closeQuietly( stream );
}
return buffer;
}
else
{
ByteBuf buffer = Unpooled.buffer( length );
buf.readBytes( buffer, length );
return buffer;
}
}
}

View File

@ -0,0 +1,102 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.peripheral.monitor;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.MonitorClientMessage;
import dan200.computercraft.shared.network.client.TerminalState;
import net.minecraft.server.management.PlayerChunkMapEntry;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.common.DimensionManager;
import net.minecraftforge.event.world.ChunkWatchEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import java.util.ArrayDeque;
import java.util.Queue;
@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID )
public final class MonitorWatcher
{
private static final Queue<TileMonitor> watching = new ArrayDeque<>();
private MonitorWatcher()
{
}
static void enqueue( TileMonitor monitor )
{
if( monitor.enqueued ) return;
monitor.enqueued = true;
monitor.cached = null;
watching.add( monitor );
}
@SubscribeEvent
public static void onWatch( ChunkWatchEvent.Watch event )
{
Chunk chunk = event.getChunkInstance();
if( chunk == null ) return;
for( TileEntity te : chunk.getTileEntityMap().values() )
{
// Find all origin monitors who are not already on the queue.
if( !(te instanceof TileMonitor) ) continue;
TileMonitor monitor = (TileMonitor) te;
ServerMonitor serverMonitor = getMonitor( monitor );
if( serverMonitor == null || monitor.enqueued ) continue;
// We use the cached terminal state if available - this is guaranteed to
TerminalState state = monitor.cached;
if( state == null ) state = monitor.cached = serverMonitor.write();
NetworkHandler.sendToPlayer( event.getPlayer(), new MonitorClientMessage( monitor.getPos(), state ) );
}
}
@SubscribeEvent
public static void onTick( TickEvent.ServerTickEvent event )
{
if( event.phase != TickEvent.Phase.END ) return;
long limit = ComputerCraft.monitorBandwidth;
boolean obeyLimit = limit > 0;
TileMonitor tile;
while( (!obeyLimit || limit > 0) && (tile = watching.poll()) != null )
{
tile.enqueued = false;
ServerMonitor monitor = getMonitor( tile );
if( monitor == null ) continue;
BlockPos pos = tile.getPos();
World world = tile.getWorld();
WorldServer serverWorld = world instanceof WorldServer ? (WorldServer) world : DimensionManager.getWorld( world.provider.getDimension() );
PlayerChunkMapEntry entry = serverWorld.getPlayerChunkMap().getEntry( pos.getX() >> 4, pos.getZ() >> 4 );
if( entry == null || entry.getWatchingPlayers().isEmpty() ) continue;
NetworkRegistry.TargetPoint point = new NetworkRegistry.TargetPoint( world.provider.getDimension(), pos.getX(), pos.getY(), pos.getZ(), 0 );
TerminalState state = tile.cached = monitor.write();
NetworkHandler.sendToAllTracking( new MonitorClientMessage( pos, state ), point );
limit -= state.size();
}
}
private static ServerMonitor getMonitor( TileMonitor monitor )
{
return !monitor.isInvalid() && monitor.getXIndex() == 0 && monitor.getYIndex() == 0 ? monitor.getCachedServerMonitor() : null;
}
}

View File

@ -5,12 +5,14 @@
*/
package dan200.computercraft.shared.peripheral.monitor;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.IPeripheralTile;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.common.ServerTerminal;
import dan200.computercraft.shared.common.TileGeneric;
import dan200.computercraft.shared.network.client.TerminalState;
import dan200.computercraft.shared.peripheral.PeripheralType;
import dan200.computercraft.shared.peripheral.common.BlockPeripheral;
import dan200.computercraft.shared.peripheral.common.ITilePeripheral;
@ -46,6 +48,10 @@ public class TileMonitor extends TileGeneric implements ITilePeripheral, IPeriph
private boolean m_destroyed = false;
private boolean visiting = false;
// MonitorWatcher state.
boolean enqueued;
TerminalState cached;
private int m_width = 1;
private int m_height = 1;
private int m_xIndex = 0;
@ -148,7 +154,7 @@ public void updateTick()
}
}
if( m_serverMonitor.pollTerminalChanged() ) updateBlock();
if( m_serverMonitor.pollTerminalChanged() ) MonitorWatcher.enqueue( this );
}
// IPeripheralTile implementation
@ -239,11 +245,6 @@ protected void writeDescription( @Nonnull NBTTagCompound nbt )
nbt.setInteger( "width", m_width );
nbt.setInteger( "height", m_height );
nbt.setInteger( "monitorDir", m_dir );
if( m_xIndex == 0 && m_yIndex == 0 && m_serverMonitor != null )
{
m_serverMonitor.writeDescription( nbt );
}
}
@Override
@ -273,9 +274,8 @@ protected final void readDescription( @Nonnull NBTTagCompound nbt )
if( m_xIndex == 0 && m_yIndex == 0 )
{
// If we're the origin terminal then read the description
// If we're the origin terminal then create it.
if( m_clientMonitor == null ) m_clientMonitor = new ClientMonitor( m_advanced, this );
m_clientMonitor.readDescription( nbt );
}
if( oldXIndex != m_xIndex || oldYIndex != m_yIndex ||
@ -286,6 +286,19 @@ protected final void readDescription( @Nonnull NBTTagCompound nbt )
updateBlock();
}
}
public final void read( TerminalState state )
{
if( m_xIndex != 0 || m_yIndex != 0 )
{
ComputerCraft.log.warn( "Receiving monitor state for non-origin terminal at {}", getPos() );
return;
}
if( m_clientMonitor == null ) m_clientMonitor = new ClientMonitor( m_advanced, this );
m_clientMonitor.read( state );
}
// Sizing and placement stuff
public EnumFacing getDirection()

View File

@ -5,6 +5,7 @@
*/
package dan200.computercraft.shared.util;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.IOException;
@ -12,11 +13,11 @@ public final class IoUtil
{
private IoUtil() {}
public static void closeQuietly( Closeable closeable )
public static void closeQuietly( @Nullable Closeable closeable )
{
try
{
closeable.close();
if( closeable != null ) closeable.close();
}
catch( IOException ignored )
{

View File

@ -191,6 +191,7 @@ gui.computercraft:config.peripheral.monitor_renderer.best=Best
gui.computercraft:config.peripheral.monitor_renderer.tbo=Texture Buffers
gui.computercraft:config.peripheral.monitor_renderer.vbo=Vertex Buffers
gui.computercraft:config.peripheral.monitor_renderer.display_list=Display Lists
gui.computercraft:config.peripheral.monitor_bandwidth=Monitor bandwidth
gui.computercraft:config.turtle=Turtles
gui.computercraft:config.turtle.need_fuel=Enable fuel

View File

@ -0,0 +1,94 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import io.netty.buffer.Unpooled;
import net.minecraft.network.PacketBuffer;
import org.apache.logging.log4j.LogManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.RepeatedTest;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests {@link TerminalState} round tripping works as expected.
*/
public class TerminalStateTest
{
@BeforeAll
public static void before()
{
ComputerCraft.log = LogManager.getLogger( ComputerCraft.MOD_ID );
}
@RepeatedTest( 5 )
public void testCompressed()
{
Terminal terminal = randomTerminal();
PacketBuffer buffer = new PacketBuffer( Unpooled.directBuffer() );
new TerminalState( true, terminal, true ).write( buffer );
checkEqual( terminal, read( buffer ) );
assertEquals( 0, buffer.readableBytes() );
}
@RepeatedTest( 5 )
public void testUncompressed()
{
Terminal terminal = randomTerminal();
PacketBuffer buffer = new PacketBuffer( Unpooled.directBuffer() );
new TerminalState( true, terminal, false ).write( buffer );
checkEqual( terminal, read( buffer ) );
assertEquals( 0, buffer.readableBytes() );
}
private static Terminal randomTerminal()
{
Random random = new Random();
Terminal terminal = new Terminal( 10, 5 );
for( int y = 0; y < terminal.getHeight(); y++ )
{
TextBuffer buffer = terminal.getLine( y );
for( int x = 0; x < buffer.length(); x++ ) buffer.setChar( x, (char) (random.nextInt( 26 ) + 65) );
}
return terminal;
}
private static void checkEqual( Terminal expected, Terminal actual )
{
assertNotNull( expected, "Expected cannot be null" );
assertNotNull( actual, "Actual cannot be null" );
assertEquals( expected.getHeight(), actual.getHeight(), "Heights must match" );
assertEquals( expected.getWidth(), actual.getWidth(), "Widths must match" );
for( int y = 0; y < expected.getHeight(); y++ )
{
assertEquals( expected.getLine( y ).toString(), actual.getLine( y ).toString() );
}
}
private static Terminal read( PacketBuffer buffer )
{
TerminalState state = new TerminalState( buffer );
assertTrue( state.colour );
if( !state.hasTerminal() ) return null;
Terminal other = new Terminal( state.width, state.height );
state.apply( other );
return other;
}
}