diff --git a/gradle.properties b/gradle.properties index f2791becc..85f36de68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Mod properties -mod_version=1.88.0 +mod_version=1.89.0 # Minecraft properties (update mods.toml when changing) mc_version=1.14.4 diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 46e1f2b64..f0d7ddbae 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -91,6 +91,7 @@ public final 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; diff --git a/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java b/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java index ade44e02b..c8db179b1 100644 --- a/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java +++ b/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java @@ -13,7 +13,6 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.client.gui.FixedWidthFontRenderer; import dan200.computercraft.shared.util.Palette; import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL20; @@ -25,12 +24,8 @@ class MonitorTextureBufferShader { static final int TEXTURE_INDEX = GL13.GL_TEXTURE3; - private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer( 16 ); private static final FloatBuffer PALETTE_BUFFER = BufferUtils.createFloatBuffer( 16 * 3 ); - private static int uniformMv; - private static int uniformP; - private static int uniformFont; private static int uniformWidth; private static int uniformHeight; @@ -43,16 +38,6 @@ class MonitorTextureBufferShader static void setupUniform( int width, int height, Palette palette, boolean greyscale ) { - MATRIX_BUFFER.rewind(); - GL11.glGetFloatv( GL11.GL_MODELVIEW_MATRIX, MATRIX_BUFFER ); - MATRIX_BUFFER.rewind(); - GLX.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER ); - - MATRIX_BUFFER.rewind(); - GL11.glGetFloatv( GL11.GL_PROJECTION_MATRIX, MATRIX_BUFFER ); - MATRIX_BUFFER.rewind(); - GLX.glUniformMatrix4( uniformP, false, MATRIX_BUFFER ); - GLX.glUniform1i( uniformWidth, width ); GLX.glUniform1i( uniformHeight, height ); @@ -121,9 +106,6 @@ class MonitorTextureBufferShader if( !ok ) return false; - uniformMv = getUniformLocation( program, "u_mv" ); - uniformP = getUniformLocation( program, "u_p" ); - uniformFont = getUniformLocation( program, "u_font" ); uniformWidth = getUniformLocation( program, "u_width" ); uniformHeight = getUniformLocation( program, "u_height" ); diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java index db98ccd54..1e2d9fedc 100644 --- a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java +++ b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java @@ -39,6 +39,7 @@ import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_ public class TileEntityMonitorRenderer extends TileEntityRenderer { private static final float MARGIN = (float) (TileMonitor.RENDER_MARGIN * 1.1); + private static ByteBuffer tboContents; @Override public void render( @Nonnull TileMonitor monitor, double posX, double posY, double posZ, float f, int i ) @@ -161,7 +162,14 @@ public class TileEntityMonitorRenderer extends TileEntityRenderer if( redraw ) { - ByteBuffer monitorBuffer = GLAllocation.createDirectByteBuffer( width * height * 3 ); + int size = width * height * 3; + if( tboContents == null || tboContents.capacity() < size ) + { + tboContents = GLAllocation.createDirectByteBuffer( size ); + } + + ByteBuffer monitorBuffer = tboContents; + monitorBuffer.position( 0 ); for( int y = 0; y < height; y++ ) { TextBuffer text = terminal.getLine( y ), textColour = terminal.getTextColourLine( y ), background = terminal.getBackgroundColourLine( y ); diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java index 2ebad4202..a26cd7364 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -37,12 +37,8 @@ public abstract class HandleGeneric implements ILuaObject { m_open = false; - Closeable closeable = m_closable; - if( closeable != null ) - { - IoUtil.closeQuietly( closeable ); - m_closable = null; - } + IoUtil.closeQuietly( m_closable ); + m_closable = null; } /** diff --git a/src/main/java/dan200/computercraft/core/apis/http/Resource.java b/src/main/java/dan200/computercraft/core/apis/http/Resource.java index 6587fb19d..469a57511 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/Resource.java +++ b/src/main/java/dan200/computercraft/core/apis/http/Resource.java @@ -106,7 +106,7 @@ public abstract class Resource> implements Closeable protected static T closeCloseable( T closeable ) { - if( closeable != null ) IoUtil.closeQuietly( closeable ); + IoUtil.closeQuietly( closeable ); return null; } diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index c1d91a078..75dd42e04 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -221,7 +221,7 @@ public class Websocket extends Resource WeakReference websocketHandleRef = websocketHandle; WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get(); - if( websocketHandle != null ) IoUtil.closeQuietly( websocketHandle ); + IoUtil.closeQuietly( websocketHandle ); this.websocketHandle = null; } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index 0403f98ac..644515e1c 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -366,8 +366,7 @@ public class FileSystem 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 ) ); } } } diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index 192ca6b07..ab5ee3308 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -67,6 +67,7 @@ public final class Config private static final ConfigValue modemRangeDuringStorm; private static final ConfigValue modemHighAltitudeRangeDuringStorm; private static final ConfigValue maxNotesPerTick; + private static final ConfigValue monitorBandwidth; private static final ConfigValue turtlesNeedFuel; private static final ConfigValue turtleFuelLimit; @@ -230,6 +231,16 @@ public final class Config .comment( "Maximum amount of notes a speaker can play at once" ) .defineInRange( "max_notes_per_tick", ComputerCraft.maxNotesPerTick, 1, Integer.MAX_VALUE ); + monitorBandwidth = builder + .comment( "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." ) + .defineInRange( "monitor_bandwidth", (int) ComputerCraft.monitorBandwidth, 0, Integer.MAX_VALUE ); + builder.pop(); } @@ -317,6 +328,7 @@ public final class Config ComputerCraft.modem_highAltitudeRange = modemHighAltitudeRange.get(); ComputerCraft.modem_rangeDuringStorm = modemRangeDuringStorm.get(); ComputerCraft.modem_highAltitudeRangeDuringStorm = modemHighAltitudeRangeDuringStorm.get(); + ComputerCraft.monitorBandwidth = monitorBandwidth.get(); // Turtles ComputerCraft.turtlesNeedFuel = turtlesNeedFuel.get(); diff --git a/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java b/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java index 2ce762435..08fb599fe 100644 --- a/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java +++ b/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java @@ -6,9 +6,7 @@ package dan200.computercraft.shared.common; import dan200.computercraft.core.terminal.Terminal; -import io.netty.buffer.Unpooled; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.network.PacketBuffer; +import dan200.computercraft.shared.network.client.TerminalState; public class ClientTerminal implements ITerminal { @@ -48,14 +46,13 @@ public class ClientTerminal implements ITerminal return m_colour; } - public void readDescription( CompoundNBT nbt ) + public void read( TerminalState state ) { - m_colour = nbt.getBoolean( "colour" ); - if( nbt.contains( "terminal" ) ) + m_colour = state.colour; + if( state.hasTerminal() ) { - CompoundNBT terminal = nbt.getCompound( "terminal" ); - resizeTerminal( terminal.getInt( "term_width" ), terminal.getInt( "term_height" ) ); - m_terminal.read( new PacketBuffer( Unpooled.wrappedBuffer( terminal.getByteArray( "term_contents" ) ) ) ); + resizeTerminal( state.width, state.height ); + state.apply( m_terminal ); } else { diff --git a/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java b/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java index 2b7d153a6..a2385b684 100644 --- a/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java +++ b/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java @@ -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.CompoundNBT; -import net.minecraft.network.PacketBuffer; +import dan200.computercraft.shared.network.client.TerminalState; import java.util.concurrent.atomic.AtomicBoolean; @@ -73,8 +69,6 @@ public class ServerTerminal implements ITerminal return m_terminalChangedLastFrame; } - // ITerminal implementation - @Override public Terminal getTerminal() { @@ -87,29 +81,8 @@ public class ServerTerminal implements ITerminal return m_colour; } - public void writeDescription( CompoundNBT nbt ) + public TerminalState write() { - nbt.putBoolean( "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() ); - } - - CompoundNBT terminal = new CompoundNBT(); - terminal.putInt( "term_width", m_terminal.getWidth() ); - terminal.putInt( "term_height", m_terminal.getHeight() ); - terminal.putByteArray( "term_contents", buffer.array() ); - nbt.put( "terminal", terminal ); - } + return new TerminalState( m_colour, m_terminal ); } } diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index a8e9c46cd..51a0a7698 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -154,9 +154,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput protected NetworkMessage createTerminalPacket() { - CompoundNBT tagCompound = new CompoundNBT(); - writeDescription( tagCompound ); - return new ComputerTerminalClientMessage( getInstanceID(), tagCompound ); + return new ComputerTerminalClientMessage( getInstanceID(), write() ); } public void broadcastState( boolean force ) diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java index 938f56812..c28cd3375 100644 --- a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java +++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java @@ -14,9 +14,11 @@ import net.minecraft.network.PacketBuffer; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; import net.minecraftforge.fml.network.NetworkDirection; import net.minecraftforge.fml.network.NetworkEvent; import net.minecraftforge.fml.network.NetworkRegistry; +import net.minecraftforge.fml.network.PacketDistributor; import net.minecraftforge.fml.network.simple.SimpleChannel; import net.minecraftforge.fml.server.ServerLifecycleHooks; @@ -52,6 +54,7 @@ public final class NetworkHandler registerMainThread( 12, ComputerDeletedClientMessage::new ); registerMainThread( 13, ComputerTerminalClientMessage::new ); registerMainThread( 14, PlayRecordClientMessage.class, PlayRecordClientMessage::new ); + registerMainThread( 15, MonitorClientMessage.class, MonitorClientMessage::new ); } public static void sendToPlayer( PlayerEntity player, NetworkMessage packet ) @@ -74,15 +77,13 @@ public final class NetworkHandler public static void sendToAllAround( NetworkMessage packet, World world, Vec3d pos, double range ) { - for( ServerPlayerEntity player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers() ) - { - if( player.getEntityWorld() != world ) continue; + PacketDistributor.TargetPoint target = new PacketDistributor.TargetPoint( pos.x, pos.y, pos.z, range, world.getDimension().getType() ); + network.send( PacketDistributor.NEAR.with( () -> target ), packet ); + } - double x = pos.x - player.posX; - double y = pos.y - player.posY; - double z = pos.z - player.posZ; - if( x * x + y * y + z * z < range * range ) sendToPlayer( player, packet ); - } + public static void sendToAllTracking( NetworkMessage packet, Chunk chunk ) + { + network.send( PacketDistributor.TRACKING_CHUNK.with( () -> chunk ), packet ); } /** diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java index f775851e6..de7bf3e85 100644 --- a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java +++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java @@ -5,7 +5,6 @@ */ package dan200.computercraft.shared.network.client; -import net.minecraft.nbt.CompoundNBT; import net.minecraft.network.PacketBuffer; import net.minecraftforge.fml.network.NetworkEvent; @@ -13,12 +12,12 @@ import javax.annotation.Nonnull; public class ComputerTerminalClientMessage extends ComputerClientMessage { - private CompoundNBT tag; + private TerminalState state; - public ComputerTerminalClientMessage( int instanceId, CompoundNBT tag ) + public ComputerTerminalClientMessage( int instanceId, TerminalState state ) { super( instanceId ); - this.tag = tag; + this.state = state; } public ComputerTerminalClientMessage() @@ -29,19 +28,19 @@ public class ComputerTerminalClientMessage extends ComputerClientMessage 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 = buf.readCompoundTag(); + state = new TerminalState( buf ); } @Override public void handle( NetworkEvent.Context context ) { - getComputer().readDescription( tag ); + getComputer().read( state ); } } diff --git a/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java new file mode 100644 index 000000000..55d4f7406 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java @@ -0,0 +1,55 @@ +/* + * 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.player.ClientPlayerEntity; +import net.minecraft.network.PacketBuffer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; + +public class MonitorClientMessage implements NetworkMessage +{ + private final BlockPos pos; + private final TerminalState state; + + public MonitorClientMessage( BlockPos pos, TerminalState state ) + { + this.pos = pos; + this.state = state; + } + + public MonitorClientMessage( @Nonnull PacketBuffer buf ) + { + pos = buf.readBlockPos(); + state = new TerminalState( buf ); + } + + @Override + public void toBytes( @Nonnull PacketBuffer buf ) + { + buf.writeBlockPos( pos ); + state.write( buf ); + } + + @Override + public void handle( NetworkEvent.Context context ) + { + ClientPlayerEntity player = Minecraft.getInstance().player; + if( player == null || player.world == null ) return; + + TileEntity te = player.world.getTileEntity( pos ); + if( !(te instanceof TileMonitor) ) return; + + ((TileMonitor) te).read( state ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java b/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java new file mode 100644 index 000000000..a720bc818 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java @@ -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; + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java new file mode 100644 index 000000000..8ea81ceb4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java @@ -0,0 +1,104 @@ +/* + * 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.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.world.ChunkWatchEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.ArrayDeque; +import java.util.Queue; + +@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID ) +public final class MonitorWatcher +{ + private static final Queue 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 ) + { + ChunkPos chunkPos = event.getPos(); + Chunk chunk = event.getWorld().getChunk( chunkPos.x, chunkPos.z ); + 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(); + if( !(world instanceof ServerWorld) ) continue; + + Chunk chunk = world.getChunkAt( pos ); + if( !((ServerWorld) world).getChunkProvider().chunkManager.getTrackingPlayers( chunk.getPos(), false ).findAny().isPresent() ) + { + continue; + } + + TerminalState state = tile.cached = monitor.write(); + NetworkHandler.sendToAllTracking( new MonitorClientMessage( pos, state ), chunk ); + + limit -= state.size(); + } + } + + private static ServerMonitor getMonitor( TileMonitor monitor ) + { + return !monitor.isRemoved() && monitor.getXIndex() == 0 && monitor.getYIndex() == 0 ? monitor.getCachedServerMonitor() : null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java index 447cb52e9..d50881eba 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java @@ -12,6 +12,7 @@ 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.util.NamedTileEntityType; import dan200.computercraft.shared.util.TickScheduler; import net.minecraft.entity.player.PlayerEntity; @@ -63,6 +64,10 @@ public class TileMonitor extends TileGeneric implements IPeripheralTile 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; @@ -170,7 +175,7 @@ public class TileMonitor extends TileGeneric implements IPeripheralTile } } - if( m_serverMonitor.pollTerminalChanged() ) updateBlock(); + if( m_serverMonitor.pollTerminalChanged() ) MonitorWatcher.enqueue( this ); } // IPeripheralTile implementation @@ -249,16 +254,10 @@ public class TileMonitor extends TileGeneric implements IPeripheralTile protected void writeDescription( @Nonnull CompoundNBT nbt ) { super.writeDescription( nbt ); - nbt.putInt( NBT_X, m_xIndex ); nbt.putInt( NBT_Y, m_yIndex ); nbt.putInt( NBT_WIDTH, m_width ); nbt.putInt( NBT_HEIGHT, m_height ); - - if( m_xIndex == 0 && m_yIndex == 0 && m_serverMonitor != null ) - { - m_serverMonitor.writeDescription( nbt ); - } } @Override @@ -286,9 +285,8 @@ public class TileMonitor extends TileGeneric implements IPeripheralTile 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( advanced, this ); - m_clientMonitor.readDescription( nbt ); } if( oldXIndex != m_xIndex || oldYIndex != m_yIndex || @@ -299,6 +297,20 @@ public class TileMonitor extends TileGeneric implements IPeripheralTile } } + 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( advanced, this ); + m_clientMonitor.read( state ); + } + + // Sizing and placement stuff + private void updateBlockState() { getWorld().setBlockState( getPos(), getBlockState() diff --git a/src/main/java/dan200/computercraft/shared/util/IoUtil.java b/src/main/java/dan200/computercraft/shared/util/IoUtil.java index a2afde286..910a716e2 100644 --- a/src/main/java/dan200/computercraft/shared/util/IoUtil.java +++ b/src/main/java/dan200/computercraft/shared/util/IoUtil.java @@ -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 ) { diff --git a/src/main/resources/assets/computercraft/shaders/monitor.frag b/src/main/resources/assets/computercraft/shaders/monitor.frag index 3e1796932..b0b7b49ed 100644 --- a/src/main/resources/assets/computercraft/shaders/monitor.frag +++ b/src/main/resources/assets/computercraft/shaders/monitor.frag @@ -35,6 +35,6 @@ void main() { int bg = int(texelFetch(u_tbo, index + 2).r * 255.0); vec2 pos = (term_pos - corner) * vec2(FONT_WIDTH, FONT_HEIGHT); - vec4 img = texture2D(u_font, (texture_corner(character) + pos) / 256.0); + vec4 img = texture(u_font, (texture_corner(character) + pos) / 256.0); colour = vec4(mix(u_palette[bg], img.rgb * u_palette[fg], img.a * mult), 1.0); } diff --git a/src/main/resources/assets/computercraft/shaders/monitor.vert b/src/main/resources/assets/computercraft/shaders/monitor.vert index 564f063e2..b758cce8d 100644 --- a/src/main/resources/assets/computercraft/shaders/monitor.vert +++ b/src/main/resources/assets/computercraft/shaders/monitor.vert @@ -1,13 +1,10 @@ -#version 140 - -uniform mat4 u_mv; -uniform mat4 u_p; +#version 130 in vec3 v_pos; out vec2 f_pos; void main() { - gl_Position = u_p * u_mv * vec4(v_pos.x, v_pos.y, 0, 1); + gl_Position = gl_ModelViewProjectionMatrix * vec4(v_pos.x, v_pos.y, 0, 1); f_pos = v_pos.xy; } diff --git a/src/main/resources/data/computercraft/lua/rom/apis/settings.lua b/src/main/resources/data/computercraft/lua/rom/apis/settings.lua index 92d211a24..f86044128 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/settings.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/settings.lua @@ -205,7 +205,7 @@ function load(sPath) end for k, v in pairs(tFile) do - local ty_v = type(k) + local ty_v = type(v) if type(k) == "string" and (ty_v == "string" or ty_v == "number" or ty_v == "boolean" or ty_v == "table") then local opt = details[k] if not opt or not opt.type or ty_v == opt.type then diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index 5616c8bc0..4ec707c88 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -1,3 +1,13 @@ +# New features in CC: Tweaked 1.89.0 + +* Compress monitor data, reducing network traffic by a significant amount. +* Allow limiting the bandwidth monitor updates use. +* Several optimisations to monitor rendering (@Lignum). +* Expose block and item tags to turtle.inspect and turtle.getItemDetail. + +And several bug fixes: +* Fix settings.load failing on defined settings. + # New features in CC: Tweaked 1.88.0 * Computers and turtles now preserve their ID when broken. diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index d99423023..e5591fe97 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -1,18 +1,11 @@ -New features in CC: Tweaked 1.88.0 +New features in CC: Tweaked 1.89.0 -* Computers and turtles now preserve their ID when broken. -* Add `peripheral.getName` - returns the name of a wrapped peripheral. -* Reduce network overhead of monitors and terminals. -* Add a TBO backend for monitors, with a significant performance boost. -* The Lua REPL warns when declaring locals (lupus590, exerro) -* Add config to allow using command computers in survival. -* Add fs.isDriveRoot - checks if a path is the root of a drive. -* `cc.pretty` can now display a function's arguments and where it was defined. The Lua REPL will show arguments by default. -* Move the shell's `require`/`package` implementation to a separate `cc.require` module. +* Compress monitor data, reducing network traffic by a significant amount. +* Allow limiting the bandwidth monitor updates use. +* Several optimisations to monitor rendering (@Lignum). +* Expose block and item tags to turtle.inspect and turtle.getItemDetail. And several bug fixes: -* Fix io.lines not accepting arguments. -* Fix settings.load using an unknown global (MCJack123). -* Prevent computers scanning peripherals twice. +* Fix settings.load failing on defined settings. Type "help changelog" to see the full version history. diff --git a/src/test/java/dan200/computercraft/shared/network/client/TerminalStateTest.java b/src/test/java/dan200/computercraft/shared/network/client/TerminalStateTest.java new file mode 100644 index 000000000..44427c361 --- /dev/null +++ b/src/test/java/dan200/computercraft/shared/network/client/TerminalStateTest.java @@ -0,0 +1,85 @@ +/* + * 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.core.terminal.TextBuffer; +import io.netty.buffer.Unpooled; +import net.minecraft.network.PacketBuffer; +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 +{ + @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; + } +} diff --git a/src/test/resources/test-rom/spec/apis/settings_spec.lua b/src/test/resources/test-rom/spec/apis/settings_spec.lua index 40af033d1..1551a822a 100644 --- a/src/test/resources/test-rom/spec/apis/settings_spec.lua +++ b/src/test/resources/test-rom/spec/apis/settings_spec.lua @@ -153,11 +153,50 @@ describe("The settings library", function() expect.error(settings.load, 1):eq("bad argument #1 (expected string, got number)") end) + local function setup_with(contents) + settings.clear() + local h = fs.open("/test-files/.settings", "w") + h.write(contents) + h.close() + + return settings.load("/test-files/.settings") + end + + local function setup(contents) + return setup_with(textutils.serialize(contents)) + end + it("defaults to .settings", function() local s = stub(fs, "open") settings.load() expect(s):called_with(".settings", "r") end) + + it("loads undefined settings", function() + expect(setup { ["test"] = 1 }):eq(true) + expect(settings.get("test")):eq(1) + end) + + it("loads defined settings", function() + settings.define("test.defined", { type = "number" }) + expect(setup { ["test.defined"] = 1 }):eq(true) + expect(settings.get("test.defined")):eq(1) + end) + + it("skips defined settings with incorrect types", function() + settings.define("test.defined", { type = "number" }) + expect(setup { ["test.defined"] = "abc" }):eq(true) + expect(settings.get("test.defined")):eq(nil) + end) + + it("skips unserializable values", function() + expect(setup_with "{ test = function() end }"):eq(true) + expect(settings.get("test")):eq(nil) + end) + + it("skips non-table files", function() + expect(setup "not a table"):eq(false) + end) end) describe("settings.save", function()