mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-22 09:27:39 +00:00
Compare commits
14 Commits
v1.14.4-1.
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1da86d6f75 | ||
![]() |
53a23f8d3d | ||
![]() |
550f63b1cb | ||
![]() |
416e87852e | ||
![]() |
7940687df2 | ||
![]() |
37a447e745 | ||
![]() |
9e2232d240 | ||
![]() |
2a8efb3fd5 | ||
![]() |
c0f3ca81fb | ||
![]() |
014bf55cd4 | ||
![]() |
085ae2e74a | ||
![]() |
4ff33f165d | ||
![]() |
d929c02d2a | ||
![]() |
d50a08a549 |
53
.github/workflows/main-ci.yml
vendored
53
.github/workflows/main-ci.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Cache gradle dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: CC-Tweaked
|
||||
path: build/libs
|
||||
|
||||
- name: Upload Coverage
|
||||
run: bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
lint-lua:
|
||||
name: Lint Lua
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Lint Lua code
|
||||
run: |
|
||||
test -d bin || mkdir bin
|
||||
test -f bin/illuaminate || wget -q -Obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate
|
||||
chmod +x bin/illuaminate
|
||||
bin/illuaminate lint
|
||||
|
||||
- name: Check whitespace
|
||||
run: python3 tools/check-lines.py
|
16
.github/workflows/make-doc.sh
vendored
16
.github/workflows/make-doc.sh
vendored
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
DEST="${GITHUB_REF#refs/*/}"
|
||||
echo "Uploading docs to https://tweaked.cc/$DEST"
|
||||
|
||||
# Setup ssh key
|
||||
mkdir -p "$HOME/.ssh/"
|
||||
echo "$SSH_KEY" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
|
||||
# And upload
|
||||
rsync -avc -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no -p $SSH_PORT" \
|
||||
"$GITHUB_WORKSPACE/doc/" \
|
||||
"$SSH_USER@$SSH_HOST:/var/www/tweaked.cc/$DEST"
|
29
.github/workflows/make-doc.yml
vendored
29
.github/workflows/make-doc.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Build documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
|
||||
jobs:
|
||||
make_doc:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
test -d bin || mkdir bin
|
||||
test -f bin/illuaminate || wget -q -Obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate
|
||||
chmod +x bin/illuaminate
|
||||
bin/illuaminate doc-gen
|
||||
|
||||
- name: Upload documentation
|
||||
run: .github/workflows/make-doc.sh 2> /dev/null
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_PORT: ${{ secrets.SSH_PORT }}
|
15
build.gradle
15
build.gradle
@@ -52,14 +52,6 @@ repositories {
|
||||
name "Charset"
|
||||
artifactPattern "https://asie.pl/files/mods/Charset/LibOnly/[module]-[revision](-[classifier]).[ext]"
|
||||
}
|
||||
maven {
|
||||
name "Amadornes"
|
||||
url "https://maven.amadornes.com/"
|
||||
}
|
||||
maven {
|
||||
name "CraftTweaker"
|
||||
url "https://maven.blamejared.com/"
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -78,7 +70,7 @@ dependencies {
|
||||
|
||||
runtime "mezz.jei:jei_1.12.2:4.15.0.269"
|
||||
|
||||
shade 'org.squiddev:Cobalt:0.5.1-SNAPSHOT'
|
||||
shade 'org.squiddev:Cobalt:0.5.5'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
|
||||
@@ -144,6 +136,9 @@ task proguard(type: ProGuardTask, dependsOn: jar) {
|
||||
// We want to avoid as much obfuscation as possible. We're only doing this to shrink code size.
|
||||
dontobfuscate; dontoptimize; keepattributes; keepparameternames
|
||||
|
||||
// Tell ProGuard to shut up
|
||||
dontwarn 'org.checkerframework.**'
|
||||
|
||||
// Proguard will remove directories by default, but that breaks JarMount.
|
||||
keepdirectories 'assets/computercraft/lua**'
|
||||
|
||||
@@ -265,7 +260,7 @@ license {
|
||||
mapping("java", "SLASHSTAR_STYLE")
|
||||
strictCheck true
|
||||
|
||||
ext.year = Calendar.getInstance().get(Calendar.YEAR)
|
||||
ext.year = 2020
|
||||
}
|
||||
|
||||
[licenseMain, licenseFormatMain].forEach {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Mod properties
|
||||
mod_version=1.88.0
|
||||
mod_version=1.89.2
|
||||
|
||||
# Minecraft properties
|
||||
mc_version=1.12.2
|
||||
|
@@ -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 class ComputerCraft
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
if( zipFile != null ) IoUtil.closeQuietly( zipFile );
|
||||
IoUtil.closeQuietly( zipFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ import dan200.computercraft.shared.util.Palette;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
@@ -27,12 +26,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;
|
||||
@@ -45,16 +40,6 @@ class MonitorTextureBufferShader
|
||||
|
||||
static void setupUniform( int width, int height, Palette palette, boolean greyscale )
|
||||
{
|
||||
MATRIX_BUFFER.rewind();
|
||||
GL11.glGetFloat( GL11.GL_MODELVIEW_MATRIX, MATRIX_BUFFER );
|
||||
MATRIX_BUFFER.rewind();
|
||||
OpenGlHelper.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER );
|
||||
|
||||
MATRIX_BUFFER.rewind();
|
||||
GL11.glGetFloat( GL11.GL_PROJECTION_MATRIX, MATRIX_BUFFER );
|
||||
MATRIX_BUFFER.rewind();
|
||||
OpenGlHelper.glUniformMatrix4( uniformP, false, MATRIX_BUFFER );
|
||||
|
||||
OpenGlHelper.glUniform1i( uniformWidth, width );
|
||||
OpenGlHelper.glUniform1i( uniformHeight, height );
|
||||
|
||||
@@ -123,9 +108,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" );
|
||||
|
@@ -35,6 +35,7 @@ import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_
|
||||
public class TileEntityMonitorRenderer extends TileEntitySpecialRenderer<TileMonitor>
|
||||
{
|
||||
private static final float MARGIN = (float) (TileMonitor.RENDER_MARGIN * 1.1);
|
||||
private static ByteBuffer tboContents;
|
||||
|
||||
@Override
|
||||
public void render( @Nonnull TileMonitor tileEntity, double posX, double posY, double posZ, float f, int i, float f2 )
|
||||
@@ -162,7 +163,14 @@ public class TileEntityMonitorRenderer extends TileEntitySpecialRenderer<TileMon
|
||||
|
||||
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.clear();
|
||||
for( int y = 0; y < height; y++ )
|
||||
{
|
||||
TextBuffer text = terminal.getLine( y ), textColour = terminal.getTextColourLine( y ), background = terminal.getBackgroundColourLine( y );
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -106,7 +106,7 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
|
||||
|
||||
protected static <T extends Closeable> T closeCloseable( T closeable )
|
||||
{
|
||||
if( closeable != null ) IoUtil.closeQuietly( closeable );
|
||||
IoUtil.closeQuietly( closeable );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@@ -221,7 +221,7 @@ public class Websocket extends Resource<Websocket>
|
||||
|
||||
WeakReference<WebsocketHandle> websocketHandleRef = websocketHandle;
|
||||
WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
||||
if( websocketHandle != null ) IoUtil.closeQuietly( websocketHandle );
|
||||
IoUtil.closeQuietly( websocketHandle );
|
||||
this.websocketHandle = null;
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,7 @@ import javax.annotation.Nullable;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
@@ -49,11 +50,11 @@ import static dan200.computercraft.core.computer.TimeoutState.TIMEOUT;
|
||||
public final class ComputerThread
|
||||
{
|
||||
/**
|
||||
* How often the computer thread monitor should run, in milliseconds.
|
||||
* How often the computer thread monitor should run.
|
||||
*
|
||||
* @see Monitor
|
||||
*/
|
||||
private static final int MONITOR_WAKEUP = 100;
|
||||
private static final long MONITOR_WAKEUP = TimeUnit.MILLISECONDS.toNanos( 100 );
|
||||
|
||||
/**
|
||||
* The target latency between executing two tasks on a single machine.
|
||||
@@ -76,6 +77,13 @@ public final class ComputerThread
|
||||
*/
|
||||
private static final long LATENCY_MAX_TASKS = DEFAULT_LATENCY / DEFAULT_MIN_PERIOD;
|
||||
|
||||
/**
|
||||
* Time difference between reporting crashed threads.
|
||||
*
|
||||
* @see TaskRunner#reportTimeout(ComputerExecutor, long)
|
||||
*/
|
||||
private static final long REPORT_DEBOUNCE = TimeUnit.SECONDS.toNanos( 1 );
|
||||
|
||||
/**
|
||||
* Lock used for modifications to the array of current threads.
|
||||
*/
|
||||
@@ -102,6 +110,8 @@ public final class ComputerThread
|
||||
private static final ReentrantLock computerLock = new ReentrantLock();
|
||||
|
||||
private static final Condition hasWork = computerLock.newCondition();
|
||||
private static final AtomicInteger idleWorkers = new AtomicInteger( 0 );
|
||||
private static final Condition monitorWakeup = computerLock.newCondition();
|
||||
|
||||
/**
|
||||
* Active queues to execute.
|
||||
@@ -135,7 +145,7 @@ public final class ComputerThread
|
||||
|
||||
if( runners == null )
|
||||
{
|
||||
// TODO: Change the runners length on config reloads
|
||||
// TODO: Update this on config reloads. Or possibly on world restarts?
|
||||
runners = new TaskRunner[ComputerCraft.computer_threads];
|
||||
|
||||
// latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for
|
||||
@@ -227,9 +237,14 @@ public final class ComputerThread
|
||||
|
||||
executor.virtualRuntime = Math.max( newRuntime, executor.virtualRuntime );
|
||||
|
||||
boolean wasBusy = isBusy();
|
||||
// Add to the queue, and signal the workers.
|
||||
computerQueue.add( executor );
|
||||
hasWork.signal();
|
||||
|
||||
// If we've transitioned into a busy state, notify the monitor. This will cause it to sleep for scaledPeriod
|
||||
// instead of the longer wakeup duration.
|
||||
if( !wasBusy && isBusy() ) monitorWakeup.signal();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -346,6 +361,17 @@ public final class ComputerThread
|
||||
return !computerQueue.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have more work queued than we have capacity for. Effectively a more fine-grained version of
|
||||
* {@link #hasPendingWork()}.
|
||||
*
|
||||
* @return If the computer threads are busy.
|
||||
*/
|
||||
private static boolean isBusy()
|
||||
{
|
||||
return computerQueue.size() > idleWorkers.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes all currently active {@link TaskRunner}s and terminates their tasks once they have exceeded the hard
|
||||
* abort limit.
|
||||
@@ -357,76 +383,93 @@ public final class ComputerThread
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
while( true )
|
||||
{
|
||||
while( true )
|
||||
computerLock.lock();
|
||||
try
|
||||
{
|
||||
Thread.sleep( MONITOR_WAKEUP );
|
||||
// If we've got more work than we have capacity for it, then we'll need to pause a task soon, so
|
||||
// sleep for a single pause duration. Otherwise we only need to wake up to set the soft/hard abort
|
||||
// flags, which are far less granular.
|
||||
monitorWakeup.awaitNanos( isBusy() ? scaledPeriod() : MONITOR_WAKEUP );
|
||||
}
|
||||
catch( InterruptedException e )
|
||||
{
|
||||
ComputerCraft.log.error( "Monitor thread interrupted. Computers may behave very badly!", e );
|
||||
break;
|
||||
}
|
||||
finally
|
||||
{
|
||||
computerLock.unlock();
|
||||
}
|
||||
|
||||
TaskRunner[] currentRunners = ComputerThread.runners;
|
||||
if( currentRunners != null )
|
||||
checkRunners();
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkRunners()
|
||||
{
|
||||
TaskRunner[] currentRunners = ComputerThread.runners;
|
||||
if( currentRunners == null ) return;
|
||||
|
||||
for( int i = 0; i < currentRunners.length; i++ )
|
||||
{
|
||||
TaskRunner runner = currentRunners[i];
|
||||
// If we've no runner, skip.
|
||||
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
|
||||
{
|
||||
if( !running ) continue;
|
||||
|
||||
// Mark the old runner as dead and start a new one.
|
||||
ComputerCraft.log.warn( "Previous runner ({}) has crashed, restarting!",
|
||||
runner != null && runner.owner != null ? runner.owner.getName() : runner );
|
||||
if( runner != null ) runner.running = false;
|
||||
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
|
||||
// If the runner has no work, skip
|
||||
ComputerExecutor executor = runner.currentExecutor.get();
|
||||
if( executor == null ) continue;
|
||||
|
||||
// Refresh the timeout state. Will set the pause/soft timeout flags as appropriate.
|
||||
executor.timeout.refresh();
|
||||
|
||||
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
|
||||
// then we can let the Lua machine do its work.
|
||||
long afterStart = executor.timeout.nanoCumulative();
|
||||
long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT;
|
||||
if( afterHardAbort < 0 ) continue;
|
||||
|
||||
// Set the hard abort flag.
|
||||
executor.timeout.hardAbort();
|
||||
executor.abort();
|
||||
|
||||
if( afterHardAbort >= ABORT_TIMEOUT * 2 )
|
||||
{
|
||||
// If we've hard aborted and interrupted, and we're still not dead, then mark the runner
|
||||
// as dead, finish off the task, and spawn a new runner.
|
||||
runner.reportTimeout( executor, afterStart );
|
||||
runner.running = false;
|
||||
runner.owner.interrupt();
|
||||
|
||||
ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null );
|
||||
if( thisExecutor != null ) afterWork( runner, executor );
|
||||
|
||||
synchronized( threadLock )
|
||||
{
|
||||
for( int i = 0; i < currentRunners.length; i++ )
|
||||
if( running && runners.length > i && runners[i] == runner )
|
||||
{
|
||||
TaskRunner runner = currentRunners[i];
|
||||
// If we've no runner, skip.
|
||||
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
|
||||
{
|
||||
if( !running ) continue;
|
||||
|
||||
// Mark the old runner as dead and start a new one.
|
||||
ComputerCraft.log.warn( "Previous runner ({}) has crashed, restarting!",
|
||||
runner != null && runner.owner != null ? runner.owner.getName() : runner );
|
||||
if( runner != null ) runner.running = false;
|
||||
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
|
||||
// If the runner has no work, skip
|
||||
ComputerExecutor executor = runner.currentExecutor.get();
|
||||
if( executor == null ) continue;
|
||||
|
||||
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
|
||||
// then we can let the Lua machine do its work.
|
||||
long afterStart = executor.timeout.nanoCumulative();
|
||||
long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT;
|
||||
if( afterHardAbort < 0 ) continue;
|
||||
|
||||
// Set the hard abort flag.
|
||||
executor.timeout.hardAbort();
|
||||
executor.abort();
|
||||
|
||||
if( afterHardAbort >= ABORT_TIMEOUT )
|
||||
{
|
||||
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
||||
// the task.
|
||||
timeoutTask( executor, runner.owner, afterStart );
|
||||
runner.owner.interrupt();
|
||||
}
|
||||
else if( afterHardAbort >= ABORT_TIMEOUT * 2 )
|
||||
{
|
||||
// If we've hard aborted and interrupted, and we're still not dead, then mark the runner
|
||||
// as dead, finish off the task, and spawn a new runner.
|
||||
timeoutTask( executor, runner.owner, afterStart );
|
||||
runner.running = false;
|
||||
runner.owner.interrupt();
|
||||
|
||||
ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null );
|
||||
if( thisExecutor != null ) afterWork( runner, executor );
|
||||
|
||||
synchronized( threadLock )
|
||||
{
|
||||
if( running && runners.length > i && runners[i] == runner )
|
||||
{
|
||||
runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( InterruptedException ignored )
|
||||
{
|
||||
else if( afterHardAbort >= ABORT_TIMEOUT )
|
||||
{
|
||||
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
||||
// the task.
|
||||
runner.reportTimeout( executor, afterStart );
|
||||
runner.owner.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -441,6 +484,7 @@ public final class ComputerThread
|
||||
private static final class TaskRunner implements Runnable
|
||||
{
|
||||
Thread owner;
|
||||
long lastReport = Long.MIN_VALUE;
|
||||
volatile boolean running = true;
|
||||
|
||||
final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>();
|
||||
@@ -460,6 +504,7 @@ public final class ComputerThread
|
||||
computerLock.lockInterruptibly();
|
||||
try
|
||||
{
|
||||
idleWorkers.incrementAndGet();
|
||||
while( computerQueue.isEmpty() ) hasWork.await();
|
||||
executor = computerQueue.pollFirst();
|
||||
assert executor != null : "hasWork should ensure we never receive null work";
|
||||
@@ -467,6 +512,7 @@ public final class ComputerThread
|
||||
finally
|
||||
{
|
||||
computerLock.unlock();
|
||||
idleWorkers.decrementAndGet();
|
||||
}
|
||||
}
|
||||
catch( InterruptedException ignored )
|
||||
@@ -514,27 +560,32 @@ public final class ComputerThread
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void timeoutTask( ComputerExecutor executor, Thread thread, long time )
|
||||
{
|
||||
if( !ComputerCraft.logPeripheralErrors ) return;
|
||||
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append( "Terminating computer #" ).append( executor.getComputer().getID() )
|
||||
.append( " due to timeout (running for " ).append( time * 1e-9 )
|
||||
.append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " )
|
||||
.append( thread.getName() )
|
||||
.append( " is currently " )
|
||||
.append( thread.getState() );
|
||||
Object blocking = LockSupport.getBlocker( thread );
|
||||
if( blocking != null ) builder.append( "\n on " ).append( blocking );
|
||||
|
||||
for( StackTraceElement element : thread.getStackTrace() )
|
||||
private void reportTimeout( ComputerExecutor executor, long time )
|
||||
{
|
||||
builder.append( "\n at " ).append( element );
|
||||
}
|
||||
if( !ComputerCraft.logPeripheralErrors ) return;
|
||||
|
||||
ComputerCraft.log.warn( builder.toString() );
|
||||
// Attempt to debounce stack trace reporting, limiting ourselves to one every second.
|
||||
long now = System.nanoTime();
|
||||
if( lastReport != Long.MIN_VALUE && now - lastReport - REPORT_DEBOUNCE <= 0 ) return;
|
||||
lastReport = now;
|
||||
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append( "Terminating computer #" ).append( executor.getComputer().getID() )
|
||||
.append( " due to timeout (running for " ).append( time * 1e-9 )
|
||||
.append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " )
|
||||
.append( owner.getName() )
|
||||
.append( " is currently " )
|
||||
.append( owner.getState() );
|
||||
Object blocking = LockSupport.getBlocker( owner );
|
||||
if( blocking != null ) builder.append( "\n on " ).append( blocking );
|
||||
|
||||
for( StackTraceElement element : owner.getStackTrace() )
|
||||
{
|
||||
builder.append( "\n at " ).append( element );
|
||||
}
|
||||
|
||||
ComputerCraft.log.warn( builder.toString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -86,7 +86,7 @@ public final class TimeoutState
|
||||
/**
|
||||
* Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags.
|
||||
*/
|
||||
public void refresh()
|
||||
public synchronized void refresh()
|
||||
{
|
||||
// Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we
|
||||
// need to handle overflow.
|
||||
@@ -153,7 +153,7 @@ public final class TimeoutState
|
||||
*
|
||||
* @see #nanoCumulative()
|
||||
*/
|
||||
void pauseTimer()
|
||||
synchronized void pauseTimer()
|
||||
{
|
||||
// We set the cumulative time to difference between current time and "nominal start time".
|
||||
cumulativeElapsed = System.nanoTime() - cumulativeStart;
|
||||
@@ -163,7 +163,7 @@ public final class TimeoutState
|
||||
/**
|
||||
* Resets the cumulative time and resets the abort flags.
|
||||
*/
|
||||
void stopTimer()
|
||||
synchronized void stopTimer()
|
||||
{
|
||||
cumulativeElapsed = 0;
|
||||
paused = softAbort = hardAbort = false;
|
||||
|
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -445,24 +445,9 @@ public class CobaltLuaMachine implements ILuaMachine
|
||||
// We check our current pause/abort state every 128 instructions.
|
||||
if( (count = (count + 1) & 127) == 0 )
|
||||
{
|
||||
// If we've been hard aborted or closed then abort.
|
||||
if( timeout.isHardAborted() || m_state == null ) throw HardAbortError.INSTANCE;
|
||||
|
||||
timeout.refresh();
|
||||
if( timeout.isPaused() )
|
||||
{
|
||||
// Preserve the current state
|
||||
isPaused = true;
|
||||
oldInHook = ds.inhook;
|
||||
oldFlags = di.flags;
|
||||
|
||||
// Suspend the state. This will probably throw, but we need to handle the case where it won't.
|
||||
di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED;
|
||||
LuaThread.suspend( ds.getLuaState() );
|
||||
resetPaused( ds, di );
|
||||
}
|
||||
|
||||
handleSoftAbort();
|
||||
if( timeout.isPaused() ) handlePause( ds, di );
|
||||
if( timeout.isSoftAborted() ) handleSoftAbort();
|
||||
}
|
||||
|
||||
super.onInstruction( ds, di, pc );
|
||||
@@ -471,13 +456,10 @@ public class CobaltLuaMachine implements ILuaMachine
|
||||
@Override
|
||||
public void poll() throws LuaError
|
||||
{
|
||||
// If we've been hard aborted or closed then abort.
|
||||
LuaState state = m_state;
|
||||
if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE;
|
||||
|
||||
timeout.refresh();
|
||||
if( timeout.isPaused() ) LuaThread.suspendBlocking( state );
|
||||
handleSoftAbort();
|
||||
if( timeout.isSoftAborted() ) handleSoftAbort();
|
||||
}
|
||||
|
||||
private void resetPaused( DebugState ds, DebugFrame di )
|
||||
@@ -491,11 +473,24 @@ public class CobaltLuaMachine implements ILuaMachine
|
||||
private void handleSoftAbort() throws LuaError
|
||||
{
|
||||
// If we already thrown our soft abort error then don't do it again.
|
||||
if( !timeout.isSoftAborted() || thrownSoftAbort ) return;
|
||||
if( thrownSoftAbort ) return;
|
||||
|
||||
thrownSoftAbort = true;
|
||||
throw new LuaError( TimeoutState.ABORT_MESSAGE );
|
||||
}
|
||||
|
||||
private void handlePause( DebugState ds, DebugFrame di ) throws LuaError, UnwindThrowable
|
||||
{
|
||||
// Preserve the current state
|
||||
isPaused = true;
|
||||
oldInHook = ds.inhook;
|
||||
oldFlags = di.flags;
|
||||
|
||||
// Suspend the state. This will probably throw, but we need to handle the case where it won't.
|
||||
di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED;
|
||||
LuaThread.suspend( ds.getLuaState() );
|
||||
resetPaused( ds, di );
|
||||
}
|
||||
}
|
||||
|
||||
private class CobaltLuaContext implements ILuaContext
|
||||
|
@@ -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 final class Config
|
||||
"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 final class Config
|
||||
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();
|
||||
|
@@ -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 class ClientTerminal implements ITerminal
|
||||
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
|
||||
{
|
||||
|
@@ -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 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( 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 );
|
||||
}
|
||||
}
|
||||
|
@@ -155,9 +155,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput
|
||||
|
||||
protected IMessage createTerminalPacket()
|
||||
{
|
||||
NBTTagCompound tagCompound = new NBTTagCompound();
|
||||
writeDescription( tagCompound );
|
||||
return new ComputerTerminalClientMessage( getInstanceID(), tagCompound );
|
||||
return new ComputerTerminalClientMessage( getInstanceID(), write() );
|
||||
}
|
||||
|
||||
public void broadcastState( boolean force )
|
||||
|
@@ -45,6 +45,7 @@ public final class NetworkHandler
|
||||
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 final class NetworkHandler
|
||||
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.
|
||||
|
@@ -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 @@ import javax.annotation.Nonnull;
|
||||
|
||||
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 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 = NBTUtil.readCompoundTag( buf );
|
||||
state = new TerminalState( buf );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle( MessageContext context )
|
||||
{
|
||||
getComputer().readDescription( tag );
|
||||
getComputer().read( state );
|
||||
}
|
||||
}
|
||||
|
@@ -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 );
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,6 +24,7 @@ import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.util.EnumHand;
|
||||
import net.minecraft.util.math.AxisAlignedBB;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.util.text.TextComponentTranslation;
|
||||
@@ -444,4 +445,11 @@ public class TileCable extends TileGeneric implements IPeripheralTile
|
||||
IBlockState state = getBlockState();
|
||||
return BlockCable.getPeripheralType( state );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public AxisAlignedBB getRenderBoundingBox()
|
||||
{
|
||||
return Block.FULL_BLOCK_AABB.offset( getPos() );
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 class TileMonitor extends TileGeneric implements ITilePeripheral, IPeriph
|
||||
}
|
||||
}
|
||||
|
||||
if( m_serverMonitor.pollTerminalChanged() ) updateBlock();
|
||||
if( m_serverMonitor.pollTerminalChanged() ) MonitorWatcher.enqueue( this );
|
||||
}
|
||||
|
||||
// IPeripheralTile implementation
|
||||
@@ -239,11 +245,6 @@ public class TileMonitor extends TileGeneric implements ITilePeripheral, IPeriph
|
||||
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 @@ public class TileMonitor extends TileGeneric implements ITilePeripheral, IPeriph
|
||||
|
||||
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 @@ public class TileMonitor extends TileGeneric implements ITilePeripheral, IPeriph
|
||||
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()
|
||||
|
@@ -13,11 +13,11 @@ import net.minecraft.util.math.AxisAlignedBB;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.event.entity.EntityJoinWorldEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingDropsEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.eventhandler.EventPriority;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
@@ -31,16 +31,16 @@ public final class DropConsumer
|
||||
|
||||
private static Function<ItemStack, ItemStack> dropConsumer;
|
||||
private static List<ItemStack> remainingDrops;
|
||||
private static WeakReference<World> dropWorld;
|
||||
private static World dropWorld;
|
||||
private static AxisAlignedBB dropBounds;
|
||||
private static WeakReference<Entity> dropEntity;
|
||||
private static Entity dropEntity;
|
||||
|
||||
public static void set( Entity entity, Function<ItemStack, ItemStack> consumer )
|
||||
{
|
||||
dropConsumer = consumer;
|
||||
remainingDrops = new ArrayList<>();
|
||||
dropEntity = new WeakReference<>( entity );
|
||||
dropWorld = new WeakReference<>( entity.world );
|
||||
dropEntity = entity;
|
||||
dropWorld = entity.world;
|
||||
dropBounds = new AxisAlignedBB( entity.getPosition() ).grow( 2, 2, 2 );
|
||||
|
||||
entity.captureDrops = true;
|
||||
@@ -51,26 +51,12 @@ public final class DropConsumer
|
||||
dropConsumer = consumer;
|
||||
remainingDrops = new ArrayList<>( 2 );
|
||||
dropEntity = null;
|
||||
dropWorld = new WeakReference<>( world );
|
||||
dropWorld = world;
|
||||
dropBounds = new AxisAlignedBB( pos ).grow( 2, 2, 2 );
|
||||
}
|
||||
|
||||
public static List<ItemStack> clear()
|
||||
{
|
||||
if( dropEntity != null )
|
||||
{
|
||||
Entity entity = dropEntity.get();
|
||||
if( entity != null )
|
||||
{
|
||||
entity.captureDrops = false;
|
||||
if( entity.capturedDrops != null )
|
||||
{
|
||||
for( EntityItem entityItem : entity.capturedDrops ) handleDrops( entityItem.getItem() );
|
||||
entity.capturedDrops.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<ItemStack> remainingStacks = remainingDrops;
|
||||
|
||||
dropConsumer = null;
|
||||
@@ -92,11 +78,20 @@ public final class DropConsumer
|
||||
public static void onEntitySpawn( EntityJoinWorldEvent event )
|
||||
{
|
||||
// Capture any nearby item spawns
|
||||
if( dropWorld != null && dropWorld.get() == event.getWorld() && event.getEntity() instanceof EntityItem
|
||||
if( dropWorld == event.getWorld() && event.getEntity() instanceof EntityItem
|
||||
&& dropBounds.contains( event.getEntity().getPositionVector() ) )
|
||||
{
|
||||
handleDrops( ((EntityItem) event.getEntity()).getItem() );
|
||||
event.setCanceled( true );
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onLivingDrops( LivingDropsEvent drops )
|
||||
{
|
||||
if( dropEntity == null || drops.getEntity() != dropEntity ) return;
|
||||
|
||||
for( EntityItem drop : drops.getDrops() ) handleDrops( drop.getItem() );
|
||||
drops.setCanceled( true );
|
||||
}
|
||||
}
|
||||
|
@@ -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 )
|
||||
{
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -1,3 +1,20 @@
|
||||
# New features in CC: Tweaked 1.89.2
|
||||
|
||||
* Fix dupe bug when killing an entity with a turtle.
|
||||
|
||||
# New features in CC: Tweaked 1.89.1
|
||||
|
||||
* Fix crashes when rendering monitors of varying sizes.
|
||||
|
||||
# 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)
|
||||
|
||||
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.
|
||||
|
@@ -1,17 +1,5 @@
|
||||
New features in CC: Tweaked 1.88.0
|
||||
New features in CC: Tweaked 1.89.2
|
||||
|
||||
* 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.
|
||||
|
||||
And several bug fixes:
|
||||
* Fix io.lines not accepting arguments.
|
||||
* Fix settings.load using an unknown global (MCJack123).
|
||||
* Fix dupe bug when killing an entity with a turtle.
|
||||
|
||||
Type "help changelog" to see the full version history.
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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()
|
||||
|
Reference in New Issue
Block a user