mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-13 19:50:31 +00:00
Handle file transfers inside CraftOS (#1190)
- Add a new file_transfer event. This has the signature "file_transfer", TransferredFiles. TransferredFiles has a single method getFiles(), which returns a list of all transferred files. - Add a new "import" program which waits for a file_transfer event and writes files to the current directory. - If a file_transfer event is not handled (i.e. its getFiles() method is not called) within 5 seconds on the client, we display a toast informing the user on how to upload a file.
This commit is contained in:
parent
1f910ee2ba
commit
97387556fe
41
doc/events/file_transfer.md
Normal file
41
doc/events/file_transfer.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
module: [kind=event] file_transfer
|
||||
---
|
||||
|
||||
The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer.
|
||||
|
||||
This event contains a single argument, that in turn has a single method @{TransferredFiles.getFiles|getFiles}. This
|
||||
returns the list of files that are being transferred. Each file is a @{fs.BinaryReadHandle|binary file handle} with an
|
||||
additional @{TransferredFile.getName|getName} method.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name
|
||||
2. @{TransferredFiles}: The list of transferred files.
|
||||
|
||||
## Example
|
||||
Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file.
|
||||
|
||||
```lua
|
||||
local _, files = os.pullEvent("file_transfer")
|
||||
for _, file in ipairs(files.getFiles()) do
|
||||
-- Seek to the end of the file to get its size, then go back to the beginning.
|
||||
local size = file.seek("end")
|
||||
file.seek("set", 0)
|
||||
|
||||
print(file.getName() .. " " .. file.getSize())
|
||||
end
|
||||
```
|
||||
|
||||
## Example
|
||||
Save each transferred file to the computer's storage.
|
||||
|
||||
```lua
|
||||
local _, files = os.pullEvent("file_transfer")
|
||||
for _, file in ipairs(files.getFiles()) do
|
||||
local handle = fs.open(file.getName(), "wb")
|
||||
handle.write(file.readAll())
|
||||
|
||||
handle.close()
|
||||
file.close()
|
||||
end
|
||||
```
|
@ -18,12 +18,12 @@ jqwik = "1.7.0"
|
||||
junit = "5.9.1"
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = "1.5.1"
|
||||
cctJavadoc = "1.5.2"
|
||||
checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I can't remember what it is.
|
||||
curseForgeGradle = "1.0.11"
|
||||
forgeGradle = "5.1.+"
|
||||
githubRelease = "2.2.12"
|
||||
illuaminate = "0.1.0-3-g0f40379"
|
||||
illuaminate = "0.1.0-7-g2a5a89c"
|
||||
librarian = "1.+"
|
||||
minotaur = "2.+"
|
||||
mixinGradle = "0.7.+"
|
||||
|
@ -82,6 +82,8 @@ public final class ComputerCraft
|
||||
public static int monitorWidth = 8;
|
||||
public static int monitorHeight = 6;
|
||||
|
||||
public static int uploadNagDelay = 5;
|
||||
|
||||
public static final class TurtleUpgrades
|
||||
{
|
||||
public static TurtleModem wirelessModemNormal;
|
||||
|
@ -17,30 +17,35 @@ import dan200.computercraft.shared.computer.inventory.ContainerComputerBase;
|
||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.network.NetworkHandler;
|
||||
import dan200.computercraft.shared.network.server.ContinueUploadMessage;
|
||||
import dan200.computercraft.shared.network.server.UploadFileMessage;
|
||||
import net.minecraft.client.gui.screen.inventory.ContainerScreen;
|
||||
import net.minecraft.entity.player.PlayerInventory;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.Util;
|
||||
import net.minecraft.util.text.ITextComponent;
|
||||
import net.minecraft.util.text.StringTextComponent;
|
||||
import net.minecraft.util.text.TextFormatting;
|
||||
import net.minecraft.util.text.TranslationTextComponent;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class ComputerScreenBase<T extends ContainerComputerBase> extends ContainerScreen<T>
|
||||
{
|
||||
private static final ITextComponent OK = new TranslationTextComponent( "gui.ok" );
|
||||
private static final ITextComponent CANCEL = new TranslationTextComponent( "gui.cancel" );
|
||||
private static final ITextComponent OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite_button" );
|
||||
private static final ITextComponent NO_RESPONSE_TITLE = new TranslationTextComponent( "gui.computercraft.upload.no_response" );
|
||||
private static final ITextComponent NO_RESPONSE_MSG = new TranslationTextComponent( "gui.computercraft.upload.no_response.msg",
|
||||
new StringTextComponent( "import" ).withStyle( TextFormatting.DARK_GRAY ) );
|
||||
|
||||
protected WidgetTerminal terminal;
|
||||
protected Terminal terminalData;
|
||||
@ -49,11 +54,15 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
||||
|
||||
protected final int sidebarYOffset;
|
||||
|
||||
private long uploadNagDeadline = Long.MAX_VALUE;
|
||||
private final ItemStack displayStack;
|
||||
|
||||
public ComputerScreenBase( T container, PlayerInventory player, ITextComponent title, int sidebarYOffset )
|
||||
{
|
||||
super( container, player, title );
|
||||
terminalData = container.getTerminal();
|
||||
family = container.getFamily();
|
||||
displayStack = container.getDisplayStack();
|
||||
input = new ClientInputHandler( menu );
|
||||
this.sidebarYOffset = sidebarYOffset;
|
||||
}
|
||||
@ -83,6 +92,13 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
||||
{
|
||||
super.tick();
|
||||
terminal.update();
|
||||
|
||||
if( uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline )
|
||||
{
|
||||
new ItemToast( minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN )
|
||||
.showOrReplace( minecraft.getToasts() );
|
||||
uploadNagDeadline = Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -194,41 +210,29 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
|
||||
if( toUpload.size() > 0 ) UploadFileMessage.send( menu, toUpload, NetworkHandler::sendToServer );
|
||||
}
|
||||
|
||||
public void uploadResult( UploadResult result, ITextComponent message )
|
||||
public void uploadResult( UploadResult result, @Nullable ITextComponent message )
|
||||
{
|
||||
switch( result )
|
||||
{
|
||||
case SUCCESS:
|
||||
alert( UploadResult.SUCCESS_TITLE, message );
|
||||
case QUEUED:
|
||||
{
|
||||
if( ComputerCraft.uploadNagDelay > 0 )
|
||||
{
|
||||
uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos( ComputerCraft.uploadNagDelay );
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONSUMED:
|
||||
{
|
||||
uploadNagDeadline = Long.MAX_VALUE;
|
||||
break;
|
||||
}
|
||||
case ERROR:
|
||||
alert( UploadResult.FAILED_TITLE, message );
|
||||
break;
|
||||
case CONFIRM_OVERWRITE:
|
||||
OptionScreen.show(
|
||||
minecraft, UploadResult.UPLOAD_OVERWRITE, message,
|
||||
Arrays.asList(
|
||||
OptionScreen.newButton( CANCEL, b -> cancelUpload() ),
|
||||
OptionScreen.newButton( OVERWRITE, b -> continueUpload() )
|
||||
),
|
||||
this::cancelUpload
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void continueUpload()
|
||||
{
|
||||
if( minecraft.screen instanceof OptionScreen ) ((OptionScreen) minecraft.screen).disable();
|
||||
NetworkHandler.sendToServer( new ContinueUploadMessage( menu, true ) );
|
||||
}
|
||||
|
||||
private void cancelUpload()
|
||||
{
|
||||
minecraft.setScreen( this );
|
||||
NetworkHandler.sendToServer( new ContinueUploadMessage( menu, false ) );
|
||||
}
|
||||
|
||||
private void alert( ITextComponent title, ITextComponent message )
|
||||
{
|
||||
OptionScreen.show( minecraft, title, message,
|
||||
|
150
src/main/java/dan200/computercraft/client/gui/ItemToast.java
Normal file
150
src/main/java/dan200/computercraft/client/gui/ItemToast.java
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.matrix.MatrixStack;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.toasts.IToast;
|
||||
import net.minecraft.client.gui.toasts.ToastGui;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.IReorderingProcessor;
|
||||
import net.minecraft.util.text.ITextComponent;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link IToast} implementation which displays an arbitrary message along with an optional {@link ItemStack}.
|
||||
*/
|
||||
public class ItemToast implements IToast
|
||||
{
|
||||
public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object();
|
||||
|
||||
private static final long DISPLAY_TIME = 7000L;
|
||||
private static final int MAX_LINE_SIZE = 200;
|
||||
|
||||
private static final int IMAGE_SIZE = 16;
|
||||
private static final int LINE_SPACING = 10;
|
||||
private static final int MARGIN = 8;
|
||||
|
||||
private final ItemStack stack;
|
||||
private final ITextComponent title;
|
||||
private final List<IReorderingProcessor> message;
|
||||
private final Object token;
|
||||
private final int width;
|
||||
|
||||
private boolean isNew = true;
|
||||
private long firstDisplay;
|
||||
|
||||
public ItemToast( Minecraft minecraft, ItemStack stack, ITextComponent title, ITextComponent message, Object token )
|
||||
{
|
||||
this.stack = stack;
|
||||
this.title = title;
|
||||
this.token = token;
|
||||
|
||||
FontRenderer font = minecraft.font;
|
||||
this.message = font.split( message, MAX_LINE_SIZE );
|
||||
width = Math.max( MAX_LINE_SIZE, this.message.stream().mapToInt( font::width ).max().orElse( MAX_LINE_SIZE ) ) + MARGIN * 3 + IMAGE_SIZE;
|
||||
}
|
||||
|
||||
public void showOrReplace( ToastGui toasts )
|
||||
{
|
||||
ItemToast existing = toasts.getToast( ItemToast.class, getToken() );
|
||||
if( existing != null )
|
||||
{
|
||||
existing.isNew = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
toasts.addToast( this );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int width()
|
||||
{
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int height()
|
||||
{
|
||||
return MARGIN * 2 + LINE_SPACING + message.size() * LINE_SPACING;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Object getToken()
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Visibility render( @Nonnull MatrixStack transform, @Nonnull ToastGui component, long time )
|
||||
{
|
||||
if( isNew )
|
||||
{
|
||||
|
||||
firstDisplay = time;
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
component.getMinecraft().getTextureManager().bind( TEXTURE );
|
||||
RenderSystem.color3f( 1.0F, 1.0F, 1.0F );
|
||||
|
||||
if( width == 160 && message.size() <= 1 )
|
||||
{
|
||||
component.blit( transform, 0, 0, 0, 64, width, height() );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
int height = height();
|
||||
|
||||
int bottom = Math.min( 4, height - 28 );
|
||||
renderBackgroundRow( transform, component, width, 0, 0, 28 );
|
||||
|
||||
for( int i = 28; i < height - bottom; i += 10 )
|
||||
{
|
||||
renderBackgroundRow( transform, component, width, 16, i, Math.min( 16, height - i - bottom ) );
|
||||
}
|
||||
|
||||
renderBackgroundRow( transform, component, width, 32 - bottom, height - bottom, bottom );
|
||||
}
|
||||
|
||||
int textX = MARGIN;
|
||||
if( !stack.isEmpty() )
|
||||
{
|
||||
textX += MARGIN + IMAGE_SIZE;
|
||||
component.getMinecraft().getItemRenderer().renderAndDecorateFakeItem( stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE );
|
||||
}
|
||||
|
||||
component.getMinecraft().font.draw( transform, title, textX, MARGIN, 0xff500050 );
|
||||
for( int i = 0; i < message.size(); ++i )
|
||||
{
|
||||
component.getMinecraft().font.draw( transform, message.get( i ), textX, (float) (LINE_SPACING + (i + 1) * LINE_SPACING), 0xff000000 );
|
||||
}
|
||||
|
||||
return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
|
||||
}
|
||||
|
||||
private static void renderBackgroundRow( MatrixStack transform, ToastGui component, int x, int u, int y, int height )
|
||||
{
|
||||
int leftOffset = 5;
|
||||
int rightOffset = Math.min( 60, x - leftOffset );
|
||||
|
||||
component.blit( transform, 0, y, 0, 32 + u, leftOffset, height );
|
||||
for( int k = leftOffset; k < x - rightOffset; k += 64 )
|
||||
{
|
||||
component.blit( transform, k, y, 32, 32 + u, Math.min( 64, x - k - rightOffset ), height );
|
||||
}
|
||||
|
||||
component.blit( transform, x - rightOffset, y, 160 - rightOffset, 32 + u, rightOffset, height );
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A seekable, readable byte channel which is backed by a {@link ByteBuffer}.
|
||||
*/
|
||||
public class ByteBufferChannel implements SeekableByteChannel
|
||||
{
|
||||
private boolean closed = false;
|
||||
private int position = 0;
|
||||
|
||||
private final ByteBuffer backing;
|
||||
|
||||
public ByteBufferChannel( ByteBuffer backing )
|
||||
{
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read( ByteBuffer destination ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
Objects.requireNonNull( destination, "destination" );
|
||||
|
||||
if( position >= backing.limit() ) return -1;
|
||||
|
||||
int remaining = Math.min( backing.limit() - position, destination.remaining() );
|
||||
|
||||
// TODO: Switch to Java 17 methods on 1.18.x
|
||||
ByteBuffer slice = backing.slice();
|
||||
slice.position( position );
|
||||
slice.limit( position + remaining );
|
||||
destination.put( slice );
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write( ByteBuffer src ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
|
||||
{
|
||||
throw new IllegalArgumentException( "Position out of bounds" );
|
||||
}
|
||||
position = (int) newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
return backing.limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate( long size ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen()
|
||||
{
|
||||
return !closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
}
|
||||
}
|
@ -293,6 +293,7 @@ public final class ResourceMount implements IMount
|
||||
try
|
||||
{
|
||||
for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager );
|
||||
CONTENTS_CACHE.invalidateAll();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -17,7 +17,6 @@ import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.AddressRuleConfig;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
import net.minecraftforge.common.ForgeConfigSpec.Builder;
|
||||
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.ModLoadingContext;
|
||||
@ -86,6 +85,7 @@ public final class Config
|
||||
|
||||
private static final ConfigValue<MonitorRenderer> monitorRenderer;
|
||||
private static final ConfigValue<Integer> monitorDistance;
|
||||
private static final ConfigValue<Integer> uploadNagDelay;
|
||||
|
||||
private static final ForgeConfigSpec serverSpec;
|
||||
private static final ForgeConfigSpec clientSpec;
|
||||
@ -94,7 +94,7 @@ public final class Config
|
||||
|
||||
static
|
||||
{
|
||||
Builder builder = new Builder();
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
|
||||
{ // General computers
|
||||
computerSpaceLimit = builder
|
||||
@ -282,13 +282,17 @@ public final class Config
|
||||
|
||||
serverSpec = builder.build();
|
||||
|
||||
Builder clientBuilder = new Builder();
|
||||
ForgeConfigSpec.Builder clientBuilder = new ForgeConfigSpec.Builder();
|
||||
monitorRenderer = clientBuilder
|
||||
.comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers." )
|
||||
.defineEnum( "monitor_renderer", MonitorRenderer.BEST );
|
||||
monitorDistance = clientBuilder
|
||||
.comment( "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors." )
|
||||
.defineInRange( "monitor_distance", 64, 16, 1024 );
|
||||
uploadNagDelay = clientBuilder
|
||||
.comment( "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable." )
|
||||
.defineInRange( "upload_nag_delay", ComputerCraft.uploadNagDelay, 0, 60 );
|
||||
|
||||
clientSpec = clientBuilder.build();
|
||||
}
|
||||
|
||||
@ -357,6 +361,7 @@ public final class Config
|
||||
// Client
|
||||
ComputerCraft.monitorRenderer = monitorRenderer.get();
|
||||
ComputerCraft.monitorDistanceSq = monitorDistance.get() * monitorDistance.get();
|
||||
ComputerCraft.uploadNagDelay = uploadNagDelay.get();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
@ -28,6 +28,7 @@ import net.minecraft.entity.player.PlayerInventory;
|
||||
import net.minecraft.entity.player.ServerPlayerEntity;
|
||||
import net.minecraft.inventory.container.Container;
|
||||
import net.minecraft.inventory.container.INamedContainerProvider;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.network.play.server.SPlayerPositionLookPacket;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.text.IFormattableTextComponent;
|
||||
@ -225,7 +226,7 @@ public final class CommandComputerCraft
|
||||
.executes( context -> {
|
||||
ServerPlayerEntity player = context.getSource().getPlayerOrException();
|
||||
ServerComputer computer = getComputerArgument( context, "computer" );
|
||||
new ComputerContainerData( computer ).open( player, new INamedContainerProvider()
|
||||
new ComputerContainerData( computer, ItemStack.EMPTY ).open( player, new INamedContainerProvider()
|
||||
{
|
||||
@Nonnull
|
||||
@Override
|
||||
|
@ -146,7 +146,11 @@ public abstract class TileComputerBase extends TileGeneric implements IComputerT
|
||||
{
|
||||
ServerComputer computer = createServerComputer();
|
||||
computer.turnOn();
|
||||
new ComputerContainerData( computer ).open( player, this );
|
||||
|
||||
ItemStack stack = getBlockState().getBlock() instanceof BlockComputerBase<?>
|
||||
? ((BlockComputerBase<?>) getBlockState().getBlock()).getItem( this )
|
||||
: ItemStack.EMPTY;
|
||||
new ComputerContainerData( computer, stack ).open( player, this );
|
||||
}
|
||||
return ActionResultType.SUCCESS;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputState;
|
||||
import dan200.computercraft.shared.network.client.TerminalState;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
@ -16,6 +17,7 @@ import dan200.computercraft.shared.util.SingleIntArray;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.inventory.container.Container;
|
||||
import net.minecraft.inventory.container.ContainerType;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.IIntArray;
|
||||
import net.minecraft.util.IntArray;
|
||||
|
||||
@ -30,10 +32,12 @@ public abstract class ContainerComputerBase extends Container implements Compute
|
||||
private final IIntArray data;
|
||||
|
||||
private final @Nullable ServerComputer computer;
|
||||
private final @Nullable ServerInputState input;
|
||||
private final @Nullable ServerInputState<ContainerComputerBase> input;
|
||||
|
||||
private final @Nullable Terminal terminal;
|
||||
|
||||
private final ItemStack displayStack;
|
||||
|
||||
public ContainerComputerBase(
|
||||
ContainerType<? extends ContainerComputerBase> type, int id, Predicate<PlayerEntity> canUse,
|
||||
ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData
|
||||
@ -46,8 +50,9 @@ public abstract class ContainerComputerBase extends Container implements Compute
|
||||
addDataSlots( data );
|
||||
|
||||
this.computer = computer;
|
||||
input = computer == null ? null : new ServerInputState( this );
|
||||
input = computer == null ? null : new ServerInputState<>( this );
|
||||
terminal = containerData == null ? null : containerData.terminal().create();
|
||||
displayStack = containerData == null ? null : containerData.displayStack();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -75,7 +80,7 @@ public abstract class ContainerComputerBase extends Container implements Compute
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerInputState getInput()
|
||||
public ServerInputHandler getInput()
|
||||
{
|
||||
if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" );
|
||||
return input;
|
||||
@ -106,4 +111,15 @@ public abstract class ContainerComputerBase extends Container implements Compute
|
||||
super.removed( player );
|
||||
if( input != null ) input.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stack associated with this container.
|
||||
*
|
||||
* @return The current stack.
|
||||
*/
|
||||
@Nonnull
|
||||
public ItemStack getDisplayStack()
|
||||
{
|
||||
return displayStack;
|
||||
}
|
||||
}
|
||||
|
@ -46,12 +46,4 @@ public interface ServerInputHandler extends InputHandler
|
||||
* @param uploadId The unique ID of this upload.
|
||||
*/
|
||||
void finishUpload( ServerPlayerEntity uploader, UUID uploadId );
|
||||
|
||||
/**
|
||||
* Continue an upload.
|
||||
*
|
||||
* @param uploader The player uploading files.
|
||||
* @param overwrite Whether the files should be overwritten or not.
|
||||
*/
|
||||
void confirmUpload( ServerPlayerEntity uploader, boolean overwrite );
|
||||
}
|
||||
|
@ -6,13 +6,8 @@
|
||||
package dan200.computercraft.shared.computer.menu;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import dan200.computercraft.core.filesystem.FileSystemWrapper;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.computer.upload.*;
|
||||
import dan200.computercraft.shared.network.NetworkHandler;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
|
||||
@ -20,27 +15,24 @@ import it.unimi.dsi.fastutil.ints.IntIterator;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import net.minecraft.entity.player.ServerPlayerEntity;
|
||||
import net.minecraft.inventory.container.Container;
|
||||
import net.minecraft.util.text.TranslationTextComponent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The default concrete implementation of {@link ServerInputHandler}.
|
||||
* <p>
|
||||
* This keeps track of the current key and mouse state, and releases them when the container is closed.
|
||||
*
|
||||
* @param <T> The type of container this server input belongs to.
|
||||
*/
|
||||
public class ServerInputState implements ServerInputHandler
|
||||
public class ServerInputState<T extends Container & ComputerMenu> implements ServerInputHandler
|
||||
{
|
||||
private static final String LIST_PREFIX = "\n \u2022 ";
|
||||
|
||||
private final ComputerMenu owner;
|
||||
private final T owner;
|
||||
private final IntSet keysDown = new IntOpenHashSet( 4 );
|
||||
|
||||
private int lastMouseX;
|
||||
@ -50,7 +42,7 @@ public class ServerInputState implements ServerInputHandler
|
||||
private @Nullable UUID toUploadId;
|
||||
private @Nullable List<FileUpload> toUpload;
|
||||
|
||||
public ServerInputState( ComputerMenu owner )
|
||||
public ServerInputState( T owner )
|
||||
{
|
||||
this.owner = owner;
|
||||
}
|
||||
@ -160,91 +152,31 @@ public class ServerInputState implements ServerInputHandler
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkMessage message = finishUpload( false );
|
||||
NetworkMessage message = finishUpload( uploader );
|
||||
NetworkHandler.sendToPlayer( uploader, message );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirmUpload( ServerPlayerEntity uploader, boolean overwrite )
|
||||
{
|
||||
if( toUploadId == null || toUpload == null || toUpload.isEmpty() )
|
||||
{
|
||||
ComputerCraft.log.warn( "Invalid finishUpload call, skipping." );
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkMessage message = finishUpload( true );
|
||||
NetworkHandler.sendToPlayer( uploader, message );
|
||||
}
|
||||
|
||||
private UploadResultMessage finishUpload( boolean forceOverwrite )
|
||||
private UploadResultMessage finishUpload( ServerPlayerEntity player )
|
||||
{
|
||||
ServerComputer computer = owner.getComputer();
|
||||
if( toUpload == null ) return UploadResultMessage.COMPUTER_OFF;
|
||||
|
||||
FileSystem fs = computer.getComputer().getAPIEnvironment().getFileSystem();
|
||||
if( toUpload == null )
|
||||
{
|
||||
return UploadResultMessage.error( owner, UploadResult.COMPUTER_OFF_MSG );
|
||||
}
|
||||
|
||||
for( FileUpload upload : toUpload )
|
||||
{
|
||||
if( !upload.checksumMatches() )
|
||||
{
|
||||
ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() );
|
||||
return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) );
|
||||
return UploadResultMessage.error( owner, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) );
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
List<String> overwrite = new ArrayList<>();
|
||||
List<FileUpload> files = toUpload;
|
||||
toUpload = null;
|
||||
for( FileUpload upload : files )
|
||||
{
|
||||
if( !fs.exists( upload.getName() ) ) continue;
|
||||
if( fs.isDir( upload.getName() ) )
|
||||
{
|
||||
return new UploadResultMessage(
|
||||
UploadResult.ERROR,
|
||||
new TranslationTextComponent( "gui.computercraft.upload.failed.overwrite_dir", upload.getName() )
|
||||
);
|
||||
}
|
||||
|
||||
overwrite.add( upload.getName() );
|
||||
}
|
||||
|
||||
if( !overwrite.isEmpty() && !forceOverwrite )
|
||||
{
|
||||
StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" );
|
||||
for( String value : overwrite ) joiner.add( value );
|
||||
toUpload = files;
|
||||
return new UploadResultMessage(
|
||||
UploadResult.CONFIRM_OVERWRITE,
|
||||
new TranslationTextComponent( "gui.computercraft.upload.overwrite.detail", joiner.toString() )
|
||||
);
|
||||
}
|
||||
|
||||
long availableSpace = fs.getFreeSpace( "/" );
|
||||
long neededSpace = 0;
|
||||
for( FileUpload upload : files ) neededSpace += Math.max( 512, upload.getBytes().remaining() );
|
||||
if( neededSpace > availableSpace ) return UploadResultMessage.OUT_OF_SPACE;
|
||||
|
||||
for( FileUpload file : files )
|
||||
{
|
||||
try( FileSystemWrapper<WritableByteChannel> channel = fs.openForWrite( file.getName(), false, Function.identity() ) )
|
||||
{
|
||||
channel.get().write( file.getBytes() );
|
||||
}
|
||||
}
|
||||
|
||||
return new UploadResultMessage(
|
||||
UploadResult.SUCCESS, new TranslationTextComponent( "gui.computercraft.upload.success.msg", files.size() )
|
||||
);
|
||||
}
|
||||
catch( FileSystemException | IOException e )
|
||||
{
|
||||
ComputerCraft.log.error( "Error uploading files", e );
|
||||
return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) );
|
||||
}
|
||||
computer.queueEvent( "file_transfer", new Object[] {
|
||||
new TransferredFiles( player, owner, toUpload.stream().map( x -> new TransferredFile( x.getName(), x.getBytes() ) ).collect( Collectors.toList() ) ),
|
||||
} );
|
||||
return UploadResultMessage.queued( owner );
|
||||
}
|
||||
|
||||
public void close()
|
||||
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.computer.upload;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
||||
import dan200.computercraft.core.asm.ObjectSource;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A binary file handle that has been transferred to this computer.
|
||||
* <p>
|
||||
* This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard
|
||||
* {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file.
|
||||
*
|
||||
* @cc.module [kind=event] file_transfer.TransferredFile
|
||||
* @see BinaryReadableHandle
|
||||
*/
|
||||
public class TransferredFile implements ObjectSource
|
||||
{
|
||||
private final String name;
|
||||
private final BinaryReadableHandle handle;
|
||||
|
||||
public TransferredFile( String name, ByteBuffer contents )
|
||||
{
|
||||
this.name = name;
|
||||
handle = BinaryReadableHandle.of( new ByteBufferChannel( contents ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of this file being transferred.
|
||||
*
|
||||
* @return The file's name.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Object> getExtra()
|
||||
{
|
||||
return Collections.singleton( handle );
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.computer.upload;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.shared.network.NetworkHandler;
|
||||
import dan200.computercraft.shared.network.client.UploadResultMessage;
|
||||
import net.minecraft.entity.player.ServerPlayerEntity;
|
||||
import net.minecraft.inventory.container.Container;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A list of files that have been transferred to this computer.
|
||||
*
|
||||
* @cc.module [kind=event] file_transfer.TransferredFiles
|
||||
*/
|
||||
public class TransferredFiles
|
||||
{
|
||||
private final ServerPlayerEntity player;
|
||||
private final Container container;
|
||||
private final AtomicBoolean consumed = new AtomicBoolean( false );
|
||||
|
||||
private final List<TransferredFile> files;
|
||||
|
||||
public TransferredFiles( ServerPlayerEntity player, Container container, List<TransferredFile> files )
|
||||
{
|
||||
this.player = player;
|
||||
this.container = container;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the files that are being transferred to this computer.
|
||||
*
|
||||
* @return The list of files.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final List<TransferredFile> getFiles()
|
||||
{
|
||||
consumed();
|
||||
return files;
|
||||
}
|
||||
|
||||
private void consumed()
|
||||
{
|
||||
if( consumed.getAndSet( true ) ) return;
|
||||
|
||||
if( player.isAlive() && player.containerMenu == container )
|
||||
{
|
||||
NetworkHandler.sendToPlayer( player, UploadResultMessage.consumed( container ) );
|
||||
}
|
||||
}
|
||||
}
|
@ -10,16 +10,13 @@ import net.minecraft.util.text.TranslationTextComponent;
|
||||
|
||||
public enum UploadResult
|
||||
{
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
CONFIRM_OVERWRITE;
|
||||
QUEUED,
|
||||
CONSUMED,
|
||||
ERROR;
|
||||
|
||||
public static final ITextComponent SUCCESS_TITLE = new TranslationTextComponent( "gui.computercraft.upload.success" );
|
||||
|
||||
public static final ITextComponent FAILED_TITLE = new TranslationTextComponent( "gui.computercraft.upload.failed" );
|
||||
public static final ITextComponent COMPUTER_OFF_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.computer_off" );
|
||||
public static final ITextComponent OUT_OF_SPACE_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.out_of_space" );
|
||||
public static final ITextComponent TOO_MUCH_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.too_much" );
|
||||
|
||||
public static final ITextComponent UPLOAD_OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite" );
|
||||
}
|
||||
|
@ -50,7 +50,6 @@ public final class NetworkHandler
|
||||
registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new );
|
||||
registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new );
|
||||
registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new );
|
||||
registerMainThread( 5, NetworkDirection.PLAY_TO_SERVER, ContinueUploadMessage.class, ContinueUploadMessage::new );
|
||||
|
||||
// Client messages
|
||||
registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage.class, ChatTableClientMessage::new );
|
||||
|
@ -11,37 +11,55 @@ import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.inventory.container.Container;
|
||||
import net.minecraft.network.PacketBuffer;
|
||||
import net.minecraft.util.text.ITextComponent;
|
||||
import net.minecraftforge.fml.network.NetworkEvent;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class UploadResultMessage implements NetworkMessage
|
||||
{
|
||||
public static final UploadResultMessage COMPUTER_OFF = new UploadResultMessage( UploadResult.ERROR, UploadResult.COMPUTER_OFF_MSG );
|
||||
public static final UploadResultMessage OUT_OF_SPACE = new UploadResultMessage( UploadResult.ERROR, UploadResult.OUT_OF_SPACE_MSG );
|
||||
|
||||
private final int containerId;
|
||||
private final UploadResult result;
|
||||
private final ITextComponent message;
|
||||
private final ITextComponent errorMessage;
|
||||
|
||||
public UploadResultMessage( UploadResult result, ITextComponent message )
|
||||
private UploadResultMessage( Container container, UploadResult result, @Nullable ITextComponent errorMessage )
|
||||
{
|
||||
containerId = container.containerId;
|
||||
this.result = result;
|
||||
this.message = message;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public static UploadResultMessage queued( Container container )
|
||||
{
|
||||
return new UploadResultMessage( container, UploadResult.QUEUED, null );
|
||||
}
|
||||
|
||||
public static UploadResultMessage consumed( Container container )
|
||||
{
|
||||
return new UploadResultMessage( container, UploadResult.CONSUMED, null );
|
||||
}
|
||||
|
||||
public static UploadResultMessage error( Container container, ITextComponent errorMessage )
|
||||
{
|
||||
return new UploadResultMessage( container, UploadResult.ERROR, errorMessage );
|
||||
}
|
||||
|
||||
public UploadResultMessage( @Nonnull PacketBuffer buf )
|
||||
{
|
||||
containerId = buf.readVarInt();
|
||||
result = buf.readEnum( UploadResult.class );
|
||||
message = buf.readComponent();
|
||||
errorMessage = result == UploadResult.ERROR ? buf.readComponent() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBytes( @Nonnull PacketBuffer buf )
|
||||
{
|
||||
buf.writeVarInt( containerId );
|
||||
buf.writeEnum( result );
|
||||
buf.writeComponent( message );
|
||||
if( result == UploadResult.ERROR ) buf.writeComponent( errorMessage );
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -50,9 +68,9 @@ public class UploadResultMessage implements NetworkMessage
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
|
||||
Screen screen = OptionScreen.unwrap( minecraft.screen );
|
||||
if( screen instanceof ComputerScreenBase<?> )
|
||||
if( screen instanceof ComputerScreenBase<?> && ((ComputerScreenBase<?>) screen).getMenu().containerId == containerId )
|
||||
{
|
||||
((ComputerScreenBase<?>) screen).uploadResult( result, message );
|
||||
((ComputerScreenBase<?>) screen).uploadResult( result, errorMessage );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,23 +8,29 @@ package dan200.computercraft.shared.network.container;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.network.client.TerminalState;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.network.PacketBuffer;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class ComputerContainerData implements ContainerData
|
||||
{
|
||||
private final ComputerFamily family;
|
||||
private final TerminalState terminal;
|
||||
private final ItemStack displayStack;
|
||||
|
||||
public ComputerContainerData( ServerComputer computer )
|
||||
public ComputerContainerData( ServerComputer computer, @Nonnull ItemStack displayStack )
|
||||
{
|
||||
family = computer.getFamily();
|
||||
terminal = computer.getTerminalState();
|
||||
this.displayStack = displayStack;
|
||||
}
|
||||
|
||||
public ComputerContainerData( PacketBuffer buf )
|
||||
{
|
||||
family = buf.readEnum( ComputerFamily.class );
|
||||
terminal = new TerminalState( buf );
|
||||
displayStack = buf.readItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -32,6 +38,7 @@ public class ComputerContainerData implements ContainerData
|
||||
{
|
||||
buf.writeEnum( family );
|
||||
terminal.write( buf );
|
||||
buf.writeItemStack( displayStack, true );
|
||||
}
|
||||
|
||||
public ComputerFamily family()
|
||||
@ -43,4 +50,15 @@ public class ComputerContainerData implements ContainerData
|
||||
{
|
||||
return terminal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a stack associated with this menu. This may be displayed on the client.
|
||||
*
|
||||
* @return The stack associated with this menu.
|
||||
*/
|
||||
@Nonnull
|
||||
public ItemStack displayStack()
|
||||
{
|
||||
return displayStack;
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.network.server;
|
||||
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import net.minecraft.entity.player.ServerPlayerEntity;
|
||||
import net.minecraft.inventory.container.Container;
|
||||
import net.minecraft.network.PacketBuffer;
|
||||
import net.minecraftforge.fml.network.NetworkEvent;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class ContinueUploadMessage extends ComputerServerMessage
|
||||
{
|
||||
private final boolean overwrite;
|
||||
|
||||
public ContinueUploadMessage( Container menu, boolean overwrite )
|
||||
{
|
||||
super( menu );
|
||||
this.overwrite = overwrite;
|
||||
}
|
||||
|
||||
public ContinueUploadMessage( @Nonnull PacketBuffer buf )
|
||||
{
|
||||
super( buf );
|
||||
overwrite = buf.readBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBytes( @Nonnull PacketBuffer buf )
|
||||
{
|
||||
super.toBytes( buf );
|
||||
buf.writeBoolean( overwrite );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle( NetworkEvent.Context context, @Nonnull ComputerMenu container )
|
||||
{
|
||||
ServerPlayerEntity player = context.getSender();
|
||||
if( player != null ) container.getInput().confirmUpload( player, overwrite );
|
||||
}
|
||||
}
|
@ -168,7 +168,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
|
||||
if( !stop )
|
||||
{
|
||||
boolean isTypingOnly = hand == Hand.OFF_HAND;
|
||||
new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
|
||||
new ComputerContainerData( computer, stack ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
|
||||
}
|
||||
}
|
||||
return new ActionResult<>( ActionResultType.SUCCESS, stack );
|
||||
|
@ -119,16 +119,13 @@
|
||||
"gui.computercraft.upload.success": "Upload Succeeded",
|
||||
"gui.computercraft.upload.success.msg": "%d files uploaded.",
|
||||
"gui.computercraft.upload.failed": "Upload Failed",
|
||||
"gui.computercraft.upload.failed.out_of_space": "Not enough space on the computer for these files.",
|
||||
"gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.",
|
||||
"gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
|
||||
"gui.computercraft.upload.failed.name_too_long": "File names are too long to be uploaded.",
|
||||
"gui.computercraft.upload.failed.too_many_files": "Cannot upload this many files.",
|
||||
"gui.computercraft.upload.failed.overwrite_dir": "Cannot upload %s, as there is already a directory with the same name.",
|
||||
"gui.computercraft.upload.failed.generic": "Uploading files failed (%s)",
|
||||
"gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.",
|
||||
"gui.computercraft.upload.overwrite": "Files would be overwritten",
|
||||
"gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s",
|
||||
"gui.computercraft.upload.overwrite_button": "Overwrite",
|
||||
"gui.computercraft.upload.no_response": "Transferring Files",
|
||||
"gui.computercraft.upload.no_response.msg": "Your computer has not used your transferred files. You may need to run the %s program and try again.",
|
||||
"gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close."
|
||||
}
|
||||
|
@ -211,6 +211,8 @@ local function tabulateCommon(bPaged, ...)
|
||||
end
|
||||
print()
|
||||
end
|
||||
|
||||
local previous_colour = term.getTextColour()
|
||||
for _, t in ipairs(tAll) do
|
||||
if type(t) == "table" then
|
||||
if #t > 0 then
|
||||
@ -220,6 +222,7 @@ local function tabulateCommon(bPaged, ...)
|
||||
term.setTextColor(t)
|
||||
end
|
||||
end
|
||||
term.setTextColor(previous_colour)
|
||||
end
|
||||
|
||||
--[[- Prints tables in a structured form.
|
||||
|
@ -0,0 +1,70 @@
|
||||
-- Internal module for handling file uploads. This has NO stability guarantees,
|
||||
-- and so SHOULD NOT be relyed on in user code.
|
||||
|
||||
local completion = require "cc.completion"
|
||||
|
||||
return function(files)
|
||||
local overwrite = {}
|
||||
for _, file in pairs(files) do
|
||||
local filename = file.getName()
|
||||
local path = shell.resolve(filename)
|
||||
if fs.exists(path) then
|
||||
if fs.isDir(path) then
|
||||
return nil, filename .. " is already a directory."
|
||||
end
|
||||
|
||||
overwrite[#overwrite + 1] = filename
|
||||
end
|
||||
end
|
||||
|
||||
if #overwrite > 0 then
|
||||
table.sort(overwrite)
|
||||
printError("The following files will be overwritten:")
|
||||
textutils.pagedTabulate(colours.cyan, overwrite)
|
||||
|
||||
while true do
|
||||
io.write("Overwrite? (yes/no) ")
|
||||
local input = read(nil, nil, function(t)
|
||||
return completion.choice(t, { "yes", "no" })
|
||||
end)
|
||||
if not input then return end
|
||||
|
||||
input = input:lower()
|
||||
if input == "" or input == "yes" or input == "y" then
|
||||
break
|
||||
elseif input == "no" or input == "n" then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, file in pairs(files) do
|
||||
local filename = file.getName()
|
||||
print("Transferring " .. filename)
|
||||
|
||||
local path = shell.resolve(filename)
|
||||
local handle, err = fs.open(path, "wb")
|
||||
if not handle then return nil, err end
|
||||
|
||||
-- Write the file without loading it all into memory. This uses the same buffer size
|
||||
-- as BinaryReadHandle. It would be really nice to have a way to do this without
|
||||
-- multiple copies.
|
||||
while true do
|
||||
local chunk = file.read(8192)
|
||||
if not chunk then break end
|
||||
|
||||
local ok, err = pcall(handle.write, chunk)
|
||||
if not ok then
|
||||
handle.close()
|
||||
|
||||
-- Probably an out-of-space issue, just bail.
|
||||
if err:sub(1, 7) == "pcall: " then err = err:sub(8) end
|
||||
return nil, "Failed to write file (" .. err .. "). File may be corrupted"
|
||||
end
|
||||
end
|
||||
|
||||
handle.close()
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
@ -331,9 +331,8 @@ while #tProcesses > 0 do
|
||||
resizeWindows()
|
||||
redrawMenu()
|
||||
|
||||
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then
|
||||
-- Keyboard event
|
||||
-- Passthrough to current process
|
||||
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then
|
||||
-- Basic input, just passthrough to current process
|
||||
resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n))
|
||||
if cullProcess(nCurrentProcess) then
|
||||
setMenuVisible(#tProcesses >= 2)
|
||||
|
@ -0,0 +1,24 @@
|
||||
require "cc.completion"
|
||||
|
||||
print("Drop files to transfer them to this computer")
|
||||
|
||||
local files
|
||||
while true do
|
||||
local event, arg = os.pullEvent()
|
||||
if event == "file_transfer" then
|
||||
files = arg.getFiles()
|
||||
break
|
||||
elseif event == "key" and arg == keys.q then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if #files == 0 then
|
||||
printError("No files to transfer")
|
||||
return
|
||||
end
|
||||
|
||||
package.path = package.path .. "/rom/modules/internal/?.lua"
|
||||
|
||||
local ok, err = require("cc.import")(files)
|
||||
if not ok and err then printError(err) end
|
@ -10,7 +10,6 @@
|
||||
--
|
||||
-- @module[module] shell
|
||||
|
||||
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||
local make_package = dofile("rom/modules/main/cc/require.lua").make
|
||||
|
||||
local multishell = multishell
|
||||
@ -35,6 +34,14 @@ local function createShellEnv(dir)
|
||||
return env
|
||||
end
|
||||
|
||||
-- Set up a dummy require based on the current shell, for loading some of our internal dependencies.
|
||||
local require
|
||||
do
|
||||
local env = setmetatable(createShellEnv("/rom/modules/internal"), { __index = _ENV })
|
||||
require = env.require
|
||||
end
|
||||
local expect = require("cc.expect").expect
|
||||
|
||||
-- Colours
|
||||
local promptColour, textColour, bgColour
|
||||
if term.isColour() then
|
||||
@ -591,6 +598,13 @@ if #tArgs > 0 then
|
||||
shell.run(...)
|
||||
|
||||
else
|
||||
local function show_prompt()
|
||||
term.setBackgroundColor(bgColour)
|
||||
term.setTextColour(promptColour)
|
||||
write(shell.dir() .. "> ")
|
||||
term.setTextColour(textColour)
|
||||
end
|
||||
|
||||
-- "shell"
|
||||
-- Print the header
|
||||
term.setBackgroundColor(bgColour)
|
||||
@ -607,21 +621,49 @@ else
|
||||
local tCommandHistory = {}
|
||||
while not bExit do
|
||||
term.redirect(parentTerm)
|
||||
term.setBackgroundColor(bgColour)
|
||||
term.setTextColour(promptColour)
|
||||
write(shell.dir() .. "> ")
|
||||
term.setTextColour(textColour)
|
||||
show_prompt()
|
||||
|
||||
|
||||
local sLine
|
||||
if settings.get("shell.autocomplete") then
|
||||
sLine = read(nil, tCommandHistory, shell.complete)
|
||||
else
|
||||
sLine = read(nil, tCommandHistory)
|
||||
local complete
|
||||
if settings.get("shell.autocomplete") then complete = shell.complete end
|
||||
|
||||
local ok, result
|
||||
local co = coroutine.create(read)
|
||||
assert(coroutine.resume(co, nil, tCommandHistory, complete))
|
||||
|
||||
while coroutine.status(co) ~= "dead" do
|
||||
local event = table.pack(os.pullEvent())
|
||||
if event[1] == "file_transfer" then
|
||||
-- Abandon the current prompt
|
||||
local _, h = term.getSize()
|
||||
local _, y = term.getCursorPos()
|
||||
if y == h then
|
||||
term.scroll(1)
|
||||
term.setCursorPos(1, y)
|
||||
else
|
||||
term.setCursorPos(1, y + 1)
|
||||
end
|
||||
term.setCursorBlink(false)
|
||||
|
||||
-- Run the import script with the provided files
|
||||
local ok, err = require("cc.import")(event[2].getFiles())
|
||||
if not ok and err then printError(err) end
|
||||
|
||||
-- And attempt to restore the prompt.
|
||||
show_prompt()
|
||||
term.setCursorBlink(true)
|
||||
event = { "term_resize", n = 1 } -- Nasty hack to force read() to redraw.
|
||||
end
|
||||
|
||||
if result == nil or event[1] == result or event[1] == "terminate" then
|
||||
ok, result = coroutine.resume(co, table.unpack(event, 1, event.n))
|
||||
if not ok then error(result, 0) end
|
||||
end
|
||||
end
|
||||
if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then
|
||||
table.insert(tCommandHistory, sLine)
|
||||
|
||||
if result:match("%S") and tCommandHistory[#tCommandHistory] ~= result then
|
||||
table.insert(tCommandHistory, result)
|
||||
end
|
||||
shell.run(sLine)
|
||||
shell.run(result)
|
||||
end
|
||||
end
|
||||
|
@ -6,13 +6,12 @@
|
||||
package dan200.computercraft.support;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
|
||||
public class CustomMatchers
|
||||
{
|
||||
/**
|
||||
@ -27,6 +26,7 @@ public class CustomMatchers
|
||||
*/
|
||||
public static <T> Matcher<Iterable<? extends T>> containsWith( List<T> items, Function<T, Matcher<? super T>> matcher )
|
||||
{
|
||||
return contains( items.stream().map( matcher ).collect( Collectors.toList() ) );
|
||||
// The explicit type argument should be redundant, but it appears some Java compilers require it.
|
||||
return Matchers.<T>contains( items.stream().map( matcher ).collect( Collectors.toList() ) );
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
local with_window = require "test_helpers".with_window
|
||||
|
||||
describe("The shell", function()
|
||||
describe("require", function()
|
||||
it("validates arguments", function()
|
||||
@ -101,4 +103,48 @@ describe("The shell", function()
|
||||
expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("file uploads", function()
|
||||
local function create_file(name, contents)
|
||||
local did_read = false
|
||||
return {
|
||||
getName = function() return name end,
|
||||
read = function()
|
||||
if did_read then return end
|
||||
did_read = true
|
||||
return contents
|
||||
end,
|
||||
close = function() end,
|
||||
}
|
||||
end
|
||||
local function create_files(files) return { getFiles = function() return files end } end
|
||||
|
||||
it("suspends the read prompt", function()
|
||||
fs.delete("tmp.txt")
|
||||
|
||||
local win = with_window(32, 5, function()
|
||||
local queue = {
|
||||
{ "shell" },
|
||||
{ "paste", "xyz" },
|
||||
{ "file_transfer", create_files { create_file("transfer.txt", "empty file") } },
|
||||
}
|
||||
local co = coroutine.create(shell.run)
|
||||
for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end
|
||||
end)
|
||||
|
||||
expect(win.getCursorBlink()):eq(true)
|
||||
|
||||
local lines = {}
|
||||
for i = 1, 5 do lines[i] = win.getLine(i):gsub(" +$", "") end
|
||||
expect(lines):same {
|
||||
"CraftOS 1.8",
|
||||
"> xyz",
|
||||
"Transferring transfer.txt",
|
||||
"> xyz",
|
||||
"",
|
||||
}
|
||||
|
||||
expect({ win.getCursorPos() }):same { 6, 4 }
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
Loading…
Reference in New Issue
Block a user