Added card that can be used to import and export individual files from and to clients.

This commit is contained in:
Florian Nücke
2021-05-17 01:07:25 +02:00
parent bcaec00b49
commit 9cf2468bd2
29 changed files with 1180 additions and 25 deletions

View File

@@ -0,0 +1,19 @@
package li.cil.oc2.api.capabilities;
import net.minecraft.entity.player.PlayerEntity;
/**
* This interface provides access to a list of {@link PlayerEntity}s that are currently
* using a terminal or similar provided by the owner of this capability.
* <p>
* For example, for computers and robots this is the list of players that currently have
* the terminal UI opened.
*/
public interface TerminalUserProvider {
/**
* The list of players currently interacting with a terminal.
*
* @return the list of terminal users.
*/
Iterable<PlayerEntity> getTerminalUsers();
}

View File

@@ -28,7 +28,6 @@ public final class ComputerTerminalScreen extends ContainerScreen<ComputerTermin
///////////////////////////////////////////////////////////////////
@Override
protected void drawGuiContainerBackgroundLayer(final MatrixStack matrixStack, final float partialTicks, final int mouseX, final int mouseY) {
terminalWidget.renderBackground(matrixStack, mouseX, mouseY);

View File

@@ -0,0 +1,375 @@
package li.cil.oc2.client.gui;
import com.mojang.blaze3d.matrix.MatrixStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.client.gui.widget.button.Button;
import net.minecraft.client.gui.widget.list.ExtendedList;
import net.minecraft.util.text.Color;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.util.text.TranslationTextComponent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class FileChooserScreen extends Screen {
private static final Logger LOGGER = LogManager.getLogger();
///////////////////////////////////////////////////////////////////
private static final int MARGIN = 30;
private static final int WIDGET_SPACING = 8;
private static final int TEXT_FIELD_HEIGHT = 20;
private static final int BUTTON_HEIGHT = 20;
private static final int LIST_ENTRY_HEIGHT = 12;
private static final TranslationTextComponent OPEN_TITLE_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.title.load");
private static final TranslationTextComponent SAVE_TITLE_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.title.save");
private static final TranslationTextComponent FILE_NAME_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.text_field.filename");
private static final TranslationTextComponent LOAD_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.confirm_button.load");
private static final TranslationTextComponent SAVE_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.confirm_button.save");
private static final TranslationTextComponent OVERWRITE_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.confirm_button.overwrite");
private static final TranslationTextComponent CANCEL_TEXT = new TranslationTextComponent("gui.oc2.file_chooser.cancel_button");
///////////////////////////////////////////////////////////////////
private static Path directory = Paths.get("").toAbsolutePath();
///////////////////////////////////////////////////////////////////
private final FileChooserCallback callback;
private final boolean isLoad;
private final Screen previousScreen;
private FileList fileList;
private TextFieldWidget fileNameTextField;
private Button okButton;
private boolean isComplete;
///////////////////////////////////////////////////////////////////
@FunctionalInterface
public
interface FileChooserCallback {
void onFileSelected(Path path);
default void onCanceled() {
}
}
///////////////////////////////////////////////////////////////////
public static void openFileChooserForSave(final String name, final FileChooserCallback callback) {
final Screen currentScreen = Minecraft.getInstance().currentScreen;
if (currentScreen instanceof FileChooserScreen) {
currentScreen.closeScreen();
}
final FileChooserScreen screen = new FileChooserScreen(callback, false);
Minecraft.getInstance().displayGuiScreen(screen);
screen.fileNameTextField.setText(name);
}
public static void openFileChooserForLoad(final FileChooserCallback callback) {
final Screen currentScreen = Minecraft.getInstance().currentScreen;
if (currentScreen instanceof FileChooserScreen) {
currentScreen.closeScreen();
}
final FileChooserScreen screen = new FileChooserScreen(callback, true);
Minecraft.getInstance().displayGuiScreen(screen);
}
///////////////////////////////////////////////////////////////////
public FileChooserScreen(final FileChooserCallback callback, final boolean isLoad) {
super(isLoad ? OPEN_TITLE_TEXT : SAVE_TITLE_TEXT);
this.callback = callback;
this.isLoad = isLoad;
this.previousScreen = Minecraft.getInstance().currentScreen;
}
///////////////////////////////////////////////////////////////////
@Override
public void onClose() {
if (!isComplete) {
callback.onCanceled();
}
if (previousScreen != null) {
minecraft.enqueue(() -> minecraft.displayGuiScreen(previousScreen));
}
}
@Override
public void render(final MatrixStack matrixStack, final int mouseX, final int mouseY, final float partialTicks) {
super.renderBackground(matrixStack);
fileList.render(matrixStack, mouseX, mouseY, partialTicks);
fileNameTextField.render(matrixStack, mouseX, mouseY, partialTicks);
super.render(matrixStack, mouseX, mouseY, partialTicks);
}
@Override
public boolean isPauseScreen() {
return false;
}
///////////////////////////////////////////////////////////////////
@Override
protected void init() {
super.init();
minecraft.keyboardListener.enableRepeatEvents(true);
final int widgetsWidth = width - MARGIN * 2;
final int listHeight = height - MARGIN - WIDGET_SPACING - TEXT_FIELD_HEIGHT - WIDGET_SPACING - BUTTON_HEIGHT - MARGIN;
fileList = new FileList(MARGIN, listHeight, LIST_ENTRY_HEIGHT);
addListener(fileList);
final int fileNameTop = MARGIN + listHeight + WIDGET_SPACING;
fileNameTextField = new TextFieldWidget(font, MARGIN, fileNameTop, widgetsWidth, TEXT_FIELD_HEIGHT, FILE_NAME_TEXT);
fileNameTextField.setResponder(s -> {
fileList.setSelected(null);
updateButtons();
});
fileNameTextField.setMaxStringLength(1024);
addListener(fileNameTextField);
final int buttonTop = fileNameTop + TEXT_FIELD_HEIGHT + WIDGET_SPACING;
final int buttonCount = 2;
final int buttonWidth = widgetsWidth / buttonCount - (buttonCount - 1) * WIDGET_SPACING;
okButton = addButton(new Button(MARGIN, buttonTop, buttonWidth, BUTTON_HEIGHT, StringTextComponent.EMPTY, this::handleOkPressed));
addButton(new Button(MARGIN + buttonWidth + WIDGET_SPACING, buttonTop, buttonWidth, BUTTON_HEIGHT, CANCEL_TEXT, this::handleCancelPressed));
fileList.refreshFiles(directory);
updateButtons();
}
///////////////////////////////////////////////////////////////////
private boolean isParentPath() {
if (directory == null) {
return false;
}
final FileList.FileEntry selected = fileList.getSelected();
if (selected != null) {
return selected.file == null || selected.file.equals(directory.getParent());
}
final String selectedFileEntry = fileNameTextField.getText();
return "..".equals(selectedFileEntry);
}
@Nullable
private Optional<Path> getPath() {
final FileList.FileEntry selected = fileList.getSelected();
if (selected != null) {
return Optional.ofNullable(selected.file);
}
if (directory == null) {
return Optional.empty();
}
final String selectedFileEntry = fileNameTextField.getText();
if (selectedFileEntry == null || "".equals(selectedFileEntry) || ".".equals(selectedFileEntry)) {
return Optional.empty();
}
try {
return Optional.of(directory.resolve(selectedFileEntry));
} catch (final InvalidPathException e) {
return Optional.empty();
}
}
private void confirm() {
if (isParentPath()) {
fileList.refreshFiles(getPath().orElse(null));
fileNameTextField.setText("");
return;
}
getPath().ifPresent(path -> {
if (path == null || Files.isDirectory(path)) {
fileList.refreshFiles(path);
fileNameTextField.setText("");
return;
}
if (Files.isRegularFile(path)) {
isComplete = true;
callback.onFileSelected(path);
closeScreen();
} else if (!isLoad) {
isComplete = true;
callback.onFileSelected(path);
closeScreen();
} // else: cannot load non-existing file
});
}
private void cancel() {
isComplete = true;
callback.onCanceled();
closeScreen();
}
private void updateButtons() {
okButton.active = false;
okButton.setMessage(isLoad ? LOAD_TEXT : SAVE_TEXT);
okButton.clearFGColor();
if (isParentPath()) {
okButton.active = true;
return;
}
getPath().ifPresent(path -> {
if (isLoad) {
okButton.active = Files.exists(path);
} else {
okButton.active = true;
if (Files.isRegularFile(path)) {
okButton.setMessage(OVERWRITE_TEXT);
okButton.setFGColor(0xFF0000);
}
}
});
}
private void handleOkPressed(final Button button) {
confirm();
}
private void handleCancelPressed(final Button button) {
cancel();
}
///////////////////////////////////////////////////////////////////
private final class FileList extends ExtendedList<FileList.FileEntry> {
public FileList(final int y, final int height, final int slotHeight) {
super(FileChooserScreen.this.minecraft, FileChooserScreen.this.width, FileChooserScreen.this.height, y, y + height, slotHeight);
}
public void refreshFiles(final Path directory) {
FileChooserScreen.directory = directory;
setScrollAmount(0);
clearEntries();
if (directory != null && Files.isDirectory(directory)) {
addEntry(createDirectoryEntry(directory.getParent(), ".."));
try {
final List<Path> files = Files.list(directory)
.sorted((p1, p2) -> {
if (Files.isDirectory(p1) && !Files.isDirectory(p2)) {
return -1;
}
if (!Files.isDirectory(p1) && Files.isDirectory(p2)) {
return 1;
}
return p1.getFileName().compareTo(p2.getFileName());
})
.collect(Collectors.toList());
for (final Path path : files) {
if (Files.isHidden(path)) {
continue;
}
if (Files.isDirectory(path)) {
addEntry(createDirectoryEntry(path));
} else {
addEntry(createFileEntry(path));
}
}
} catch (final IOException | SecurityException e) {
LOGGER.error(e);
}
} else {
for (final Path path : FileSystems.getDefault().getRootDirectories()) {
addEntry(createDirectoryEntry(path, path.toString()));
}
}
}
@Override
public void setSelected(@Nullable final FileChooserScreen.FileList.FileEntry entry) {
super.setSelected(entry);
updateButtons();
}
private FileList.FileEntry createFileEntry(final Path file) {
return new FileList.FileEntry(file, new StringTextComponent(file.getFileName().toString()));
}
private FileList.FileEntry createDirectoryEntry(final Path path) {
return createDirectoryEntry(path, path.getFileName().toString() + path.getFileSystem().getSeparator());
}
private FileList.FileEntry createDirectoryEntry(final Path path, final String displayName) {
return new FileList.FileEntry(path, new StringTextComponent(displayName)
.modifyStyle(s -> s.setColor(Color.fromInt(0xA0A0FF))));
}
private final class FileEntry extends ExtendedList.AbstractListEntry<FileEntry> {
private final Path file;
private final ITextComponent displayName;
private long lastEntryClickTime = 0;
public FileEntry(final Path file, final ITextComponent displayName) {
this.file = file;
this.displayName = displayName;
}
@Override
public void render(final MatrixStack stack, final int index, final int top, final int left, final int width, final int height,
final int mouseX, final int mouseY, final boolean isHovered, final float deltaTime) {
font.func_243246_a(stack, displayName, left, top, 0xFFFFFFFF);
}
@Override
public boolean mouseClicked(final double mouseX, final double mouseY, final int button) {
final boolean isLeftClick = button == 0;
if (isLeftClick) {
if (file == null || (directory != null && file.equals(directory.getParent()))) {
fileNameTextField.setText("..");
} else {
final Path fileName = file.getFileName();
fileNameTextField.setText(fileName != null ? fileName.toString() : file.toString());
}
fileNameTextField.setCursorPositionZero();
fileNameTextField.setSelectionPos(0);
setSelected(this);
final boolean isDoubleClick = System.currentTimeMillis() - lastEntryClickTime < 250;
if (isDoubleClick) {
confirm();
}
lastEntryClickTime = System.currentTimeMillis();
}
return false;
}
}
}
}

View File

@@ -37,8 +37,9 @@ public final class Config {
public static double memoryEnergyPerMegabytePerTick = 0.5;
public static double hardDriveEnergyPerMegabytePerTick = 1;
public static int networkInterfaceEnergyPerTick = 1;
public static int redstoneInterfaceCardEnergyPerTick = 1;
public static int networkInterfaceEnergyPerTick = 1;
public static int cloudInterfaceCardEnergyPerTick = 1;
public static int blockOperationsModuleEnergyPerTick = 2;
public static int inventoryOperationsModuleEnergyPerTick = 1;
@@ -94,8 +95,9 @@ public final class Config {
memoryEnergyPerMegabytePerTick = COMMON_INSTANCE.memoryEnergyPerMegabytePerTick.get();
hardDriveEnergyPerMegabytePerTick = COMMON_INSTANCE.hardDriveEnergyPerMegabytePerTick.get();
networkInterfaceEnergyPerTick = COMMON_INSTANCE.networkInterfaceEnergyPerTick.get();
redstoneInterfaceCardEnergyPerTick = COMMON_INSTANCE.redstoneInterfaceCardEnergyPerTick.get();
networkInterfaceEnergyPerTick = COMMON_INSTANCE.networkInterfaceEnergyPerTick.get();
cloudInterfaceCardEnergyPerTick = COMMON_INSTANCE.cloudInterfaceCardEnergyPerTick.get();
blockOperationsModuleEnergyPerTick = COMMON_INSTANCE.blockOperationsModuleEnergyPerTick.get();
inventoryOperationsModuleEnergyPerTick = COMMON_INSTANCE.inventoryOperationsModuleEnergyPerTick.get();
@@ -125,8 +127,9 @@ public final class Config {
public final ForgeConfigSpec.DoubleValue memoryEnergyPerMegabytePerTick;
public final ForgeConfigSpec.DoubleValue hardDriveEnergyPerMegabytePerTick;
public final ForgeConfigSpec.IntValue networkInterfaceEnergyPerTick;
public final ForgeConfigSpec.IntValue redstoneInterfaceCardEnergyPerTick;
public final ForgeConfigSpec.IntValue networkInterfaceEnergyPerTick;
public final ForgeConfigSpec.IntValue cloudInterfaceCardEnergyPerTick;
public final ForgeConfigSpec.IntValue blockOperationsModuleEnergyPerTick;
public final ForgeConfigSpec.IntValue inventoryOperationsModuleEnergyPerTick;
@@ -163,8 +166,9 @@ public final class Config {
{
memoryEnergyPerMegabytePerTick = builder.defineInRange("memoryEnergyPerMegabytePerTick", Config.memoryEnergyPerMegabytePerTick, 0, Integer.MAX_VALUE);
hardDriveEnergyPerMegabytePerTick = builder.defineInRange("hardDriveEnergyPerMegabytePerTick", Config.hardDriveEnergyPerMegabytePerTick, 0, Integer.MAX_VALUE);
networkInterfaceEnergyPerTick = builder.defineInRange("networkInterfaceEnergyPerTick", Config.networkInterfaceEnergyPerTick, 0, Integer.MAX_VALUE);
redstoneInterfaceCardEnergyPerTick = builder.defineInRange("redstoneInterfaceCardEnergyPerTick", Config.redstoneInterfaceCardEnergyPerTick, 0, Integer.MAX_VALUE);
networkInterfaceEnergyPerTick = builder.defineInRange("networkInterfaceEnergyPerTick", Config.networkInterfaceEnergyPerTick, 0, Integer.MAX_VALUE);
cloudInterfaceCardEnergyPerTick = builder.defineInRange("cloudInterfaceCardEnergyPerTick", Config.cloudInterfaceCardEnergyPerTick, 0, Integer.MAX_VALUE);
blockOperationsModuleEnergyPerTick = builder.defineInRange("blockOperationsModuleEnergyPerTick", Config.blockOperationsModuleEnergyPerTick, 0, Integer.MAX_VALUE);
inventoryOperationsModuleEnergyPerTick = builder.defineInRange("inventoryOperationsModuleEnergyPerTick", Config.inventoryOperationsModuleEnergyPerTick, 0, Integer.MAX_VALUE);
}

View File

@@ -0,0 +1,292 @@
package li.cil.oc2.common.bus.device.item;
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import li.cil.oc2.api.bus.device.ItemDevice;
import li.cil.oc2.api.bus.device.object.Callback;
import li.cil.oc2.api.bus.device.object.DocumentedDevice;
import li.cil.oc2.api.bus.device.object.ObjectDevice;
import li.cil.oc2.api.bus.device.object.Parameter;
import li.cil.oc2.api.bus.device.rpc.RPCDevice;
import li.cil.oc2.api.bus.device.rpc.RPCMethod;
import li.cil.oc2.api.capabilities.TerminalUserProvider;
import li.cil.oc2.common.bus.device.util.IdentityProxy;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.network.message.ExportedFileMessage;
import li.cil.oc2.common.network.message.RequestImportedFileMessage;
import li.cil.oc2.common.network.message.ServerCanceledImportFileMessage;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.util.StringUtils;
import net.minecraftforge.fml.network.PacketDistributor;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
public final class CloudInterfaceCardItemDevice extends IdentityProxy<ItemStack> implements RPCDevice, DocumentedDevice, ItemDevice {
public static final int MAX_TRANSFERRED_FILE_SIZE = 512 * 1024;
private static final String BEGIN_EXPORT_FILE = "beginExportFile";
private static final String WRITE_EXPORT_FILE = "writeExportFile";
private static final String FINISH_EXPORT_FILE = "finishExportFile";
private static final String BEGIN_IMPORT_FILE = "beginImportFile";
private static final String READ_IMPORT_FILE = "readImportFile";
private static final String RESET = "reset";
private static final String NAME = "name";
private static final String DATA = "data";
///////////////////////////////////////////////////////////////////
private enum State {
IDLE,
EXPORTING,
IMPORTING,
IMPORT_CANCELED,
}
private static final class ExportedFile {
public final String name;
public final ByteArrayOutputStream data = new ByteArrayOutputStream();
private ExportedFile(final String name) {
this.name = name;
}
}
private static final class ImportedFile {
public final ByteArrayInputStream data;
private ImportedFile(final byte[] data) {
this.data = new ByteArrayInputStream(data);
}
}
private static final class ImportFileRequest {
public final Set<ServerPlayerEntity> PendingPlayers = Collections.newSetFromMap(new WeakHashMap<>());
public final WeakReference<CloudInterfaceCardItemDevice> Device;
private ImportFileRequest(final CloudInterfaceCardItemDevice device) {
Device = new WeakReference<>(device);
}
}
///////////////////////////////////////////////////////////////////
private static final Int2ObjectArrayMap<ImportFileRequest> importingDevices = new Int2ObjectArrayMap<>();
private static int nextImportId = 1;
private final TerminalUserProvider userProvider;
private final ObjectDevice device;
private State state;
private ExportedFile exportedFile;
private int importingId;
private ImportedFile importedFile;
///////////////////////////////////////////////////////////////////
public CloudInterfaceCardItemDevice(final ItemStack identity, final TerminalUserProvider userProvider) {
super(identity);
this.userProvider = userProvider;
this.device = new ObjectDevice(this, "cloud");
}
///////////////////////////////////////////////////////////////////
public static void setImportedFile(final int id, final byte[] data) {
final ImportFileRequest request = importingDevices.remove(id);
if (request != null) {
final CloudInterfaceCardItemDevice device = request.Device.get();
if (device != null) {
device.importedFile = new ImportedFile(data);
final ServerCanceledImportFileMessage message = new ServerCanceledImportFileMessage(id);
for (final ServerPlayerEntity player : request.PendingPlayers) {
Network.INSTANCE.send(PacketDistributor.PLAYER.with(() -> player), message);
}
}
}
}
public static void cancelImport(final ServerPlayerEntity player, final int id) {
final ImportFileRequest request = importingDevices.get(id);
if (request != null) {
request.PendingPlayers.remove(player);
if (request.PendingPlayers.isEmpty()) {
importingDevices.remove(id);
final CloudInterfaceCardItemDevice device = request.Device.get();
if (device != null) {
device.state = State.IMPORT_CANCELED;
}
}
}
}
///////////////////////////////////////////////////////////////////
@Override
public List<String> getTypeNames() {
return device.getTypeNames();
}
@Override
public List<RPCMethod> getMethods() {
return device.getMethods();
}
@Callback(name = BEGIN_EXPORT_FILE, synchronize = false)
public void beginExportFile(@Parameter(NAME) final String name) {
if (state != State.IDLE) {
throw new IllegalStateException("invalid state");
}
if (StringUtils.isNullOrEmpty(name)) {
throw new IllegalArgumentException("name must not be empty");
}
state = State.EXPORTING;
exportedFile = new ExportedFile(name);
}
@Callback(name = WRITE_EXPORT_FILE, synchronize = false)
public void writeExportFile(@Parameter(DATA) final byte[] data) throws IOException {
if (state != State.EXPORTING) {
throw new IllegalStateException("invalid state");
}
if (data == null) {
throw new IllegalArgumentException("data is required");
}
exportedFile.data.write(data);
if (exportedFile.data.size() > MAX_TRANSFERRED_FILE_SIZE) {
reset();
throw new IllegalArgumentException("exported file too large");
}
}
@Callback(name = FINISH_EXPORT_FILE)
public void finishExportFile() {
if (state != State.EXPORTING) {
throw new IllegalStateException("invalid state");
}
try {
for (final PlayerEntity player : userProvider.getTerminalUsers()) {
if (player instanceof ServerPlayerEntity) {
final ExportedFileMessage message = new ExportedFileMessage(exportedFile.name, exportedFile.data.toByteArray());
Network.INSTANCE.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) player), message);
}
}
} finally {
reset();
}
}
@Callback(name = BEGIN_IMPORT_FILE)
public void beginImportFile() {
if (state != State.IDLE) {
throw new IllegalStateException("invalid state");
}
state = State.IMPORTING;
importingId = nextImportId++;
importingDevices.put(importingId, new ImportFileRequest(this));
boolean hasAnyUsers = false;
for (final PlayerEntity player : userProvider.getTerminalUsers()) {
if (player instanceof ServerPlayerEntity) {
final RequestImportedFileMessage message = new RequestImportedFileMessage(importingId);
Network.INSTANCE.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) player), message);
hasAnyUsers = true;
}
}
if (!hasAnyUsers) {
importingDevices.remove(importingId);
importedFile = new ImportedFile(new byte[0]);
}
}
@Nullable
@Callback(name = READ_IMPORT_FILE)
public byte[] readImportFile() throws IOException {
if (state == State.IMPORT_CANCELED) {
reset();
throw new IllegalStateException("import was canceled");
}
if (state != State.IMPORTING) {
throw new IllegalStateException("invalid state");
}
if (importedFile == null) {
return new byte[0];
}
final byte[] buffer = new byte[512];
final int count = importedFile.data.read(buffer);
if (count <= 0) {
reset();
return null;
}
if (count < buffer.length) {
final byte[] data = new byte[count];
System.arraycopy(buffer, 0, data, 0, count);
return data;
} else {
return buffer;
}
}
@Callback(name = RESET)
public void reset() {
state = State.IDLE;
exportedFile = null;
importedFile = null;
importingDevices.remove(importingId);
}
@Override
public void getDeviceDocumentation(final DeviceVisitor visitor) {
visitor.visitCallback(BEGIN_EXPORT_FILE)
.description("Begins exporting a file to external data storage. Requires calls to " +
WRITE_EXPORT_FILE + "() to provide data of the exported file and a call " +
"to " + FINISH_EXPORT_FILE + "() to complete the export.\n" +
"This method may error if the device is currently exporting or importing.")
.parameterDescription(NAME, "the name of the file being exported.");
visitor.visitCallback(WRITE_EXPORT_FILE)
.description("Appends more data to the currently being exported file.\n" +
"This method may error if the device is not currently exporting or the " +
"export was interrupted.\n")
.parameterDescription(DATA, "the contents of the file being exported.");
visitor.visitCallback(FINISH_EXPORT_FILE)
.description("Finishes an export. This will prompt present users to select an external " +
"file location for the file being exported. If multiple users are present, " +
"the file is provided to all users.\n" +
"This method may error if the device is not currently exporting or the " +
"export was interrupted.");
visitor.visitCallback(BEGIN_IMPORT_FILE)
.description("Begins a file import operation. This will prompt present users to select " +
"an externally stored file for import. If multiple users are present, the " +
"first user to select a file will have their file uploaded. Use the " +
READ_IMPORT_FILE + "() method to read the contents of the file being imported.\n" +
"This method may error if the device is currently exporting or importing.");
visitor.visitCallback(READ_IMPORT_FILE)
.description("Tries to read some data from a file being imported. Returns zero length " +
"data if no data is available yet. Returns null when no more data is " +
"available.\n" +
"This method may error if the device is not currently importing or the " +
"import was interrupted.")
.returnValueDescription("data from the file being imported.");
visitor.visitCallback(RESET)
.description("Resets the device and cancels any currently running export or import operation.");
}
}

View File

@@ -40,6 +40,7 @@ public final class Providers {
ITEM_DEVICE_PROVIDERS.register("flash_memory_custom", FlashMemoryWithExternalDataItemDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("redstone_interface_card", RedstoneInterfaceCardItemDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("network_interface_card", NetworkInterfaceCardItemDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("cloud_interface_card", CloudInterfaceCardItemDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("inventory_operations_module", InventoryOperationsModuleDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("block_operations_module", BlockOperationsModuleDeviceProvider::new);

View File

@@ -0,0 +1,59 @@
package li.cil.oc2.common.bus.device.provider.item;
import li.cil.oc2.api.bus.device.ItemDevice;
import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery;
import li.cil.oc2.api.capabilities.TerminalUserProvider;
import li.cil.oc2.common.Config;
import li.cil.oc2.common.bus.device.item.CloudInterfaceCardItemDevice;
import li.cil.oc2.common.bus.device.provider.util.AbstractItemDeviceProvider;
import li.cil.oc2.common.capabilities.Capabilities;
import li.cil.oc2.common.item.Items;
import net.minecraftforge.common.util.LazyOptional;
import java.util.Optional;
public final class CloudInterfaceCardItemDeviceProvider extends AbstractItemDeviceProvider {
public CloudInterfaceCardItemDeviceProvider() {
super(Items.CLOUD_INTERFACE_CARD);
}
///////////////////////////////////////////////////////////////////
@Override
protected boolean matches(final ItemDeviceQuery query) {
return super.matches(query) && getTerminalUserProvider(query).isPresent();
}
@Override
protected Optional<ItemDevice> getItemDevice(final ItemDeviceQuery query) {
return getTerminalUserProvider(query).map(provider ->
new CloudInterfaceCardItemDevice(query.getItemStack(), provider));
}
@Override
protected int getItemDeviceEnergyConsumption(final ItemDeviceQuery query) {
return Config.cloudInterfaceCardEnergyPerTick;
}
///////////////////////////////////////////////////////////////////
private Optional<TerminalUserProvider> getTerminalUserProvider(final ItemDeviceQuery query) {
if (query.getContainerTileEntity().isPresent()) {
final LazyOptional<TerminalUserProvider> capability = query.getContainerTileEntity().get()
.getCapability(Capabilities.TERMINAL_USER_PROVIDER);
if (capability.isPresent()) {
return capability.resolve();
}
}
if (query.getContainerEntity().isPresent()) {
final LazyOptional<TerminalUserProvider> capability = query.getContainerEntity().get()
.getCapability(Capabilities.TERMINAL_USER_PROVIDER);
if (capability.isPresent()) {
return capability.resolve();
}
}
return Optional.empty();
}
}

View File

@@ -53,6 +53,11 @@ public abstract class AbstractItemDeviceProvider extends ForgeRegistryEntry<Item
///////////////////////////////////////////////////////////////////
protected boolean matches(final ItemDeviceQuery query) {
final ItemStack stack = query.getItemStack();
return !stack.isEmpty() && predicate.test(stack.getItem());
}
protected abstract Optional<ItemDevice> getItemDevice(final ItemDeviceQuery query);
protected Optional<DeviceType> getItemDeviceType(final ItemDeviceQuery query) {
@@ -62,11 +67,4 @@ public abstract class AbstractItemDeviceProvider extends ForgeRegistryEntry<Item
protected int getItemDeviceEnergyConsumption(final ItemDeviceQuery query) {
return 0;
}
///////////////////////////////////////////////////////////////////
private boolean matches(final ItemDeviceQuery query) {
final ItemStack stack = query.getItemStack();
return !stack.isEmpty() && predicate.test(stack.getItem());
}
}

View File

@@ -4,6 +4,7 @@ import li.cil.oc2.api.bus.DeviceBusElement;
import li.cil.oc2.api.capabilities.NetworkInterface;
import li.cil.oc2.api.capabilities.RedstoneEmitter;
import li.cil.oc2.api.capabilities.Robot;
import li.cil.oc2.api.capabilities.TerminalUserProvider;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityInject;
import net.minecraftforge.common.capabilities.CapabilityManager;
@@ -30,6 +31,9 @@ public final class Capabilities {
@CapabilityInject(NetworkInterface.class)
public static Capability<NetworkInterface> NETWORK_INTERFACE = null;
@CapabilityInject(TerminalUserProvider.class)
public static Capability<TerminalUserProvider> TERMINAL_USER_PROVIDER = null;
@CapabilityInject(Robot.class)
public static Capability<Robot> ROBOT = null;
@@ -39,6 +43,7 @@ public final class Capabilities {
register(DeviceBusElement.class);
register(RedstoneEmitter.class);
register(NetworkInterface.class);
register(TerminalUserProvider.class);
register(Robot.class);
}

View File

@@ -40,6 +40,8 @@ public final class ComputerTerminalContainer extends AbstractContainer {
this.computer = computer;
this.energyInfo = energyInfo;
this.computer.addTerminalUser(player);
assertIntArraySize(energyInfo, ENERGY_INFO_SIZE);
trackIntArray(energyInfo);
}

View File

@@ -59,11 +59,13 @@ public final class Items {
public static final RegistryObject<FlashMemoryWithExternalDataItem> FLASH_MEMORY_CUSTOM = register("flash_memory_custom", () ->
new FlashMemoryWithExternalDataItem(Firmwares.BUILDROOT.getId()));
public static final RegistryObject<Item> REDSTONE_INTERFACE_CARD = register("redstone_interface_card");
public static final RegistryObject<Item> NETWORK_INTERFACE_CARD = register("network_interface_card");
public static final RegistryObject<FloppyItem> FLOPPY = register("floppy", () ->
new FloppyItem(512 * Constants.KILOBYTE));
public static final RegistryObject<Item> REDSTONE_INTERFACE_CARD = register("redstone_interface_card");
public static final RegistryObject<Item> NETWORK_INTERFACE_CARD = register("network_interface_card");
public static final RegistryObject<Item> CLOUD_INTERFACE_CARD = register("cloud_interface_card");
public static final RegistryObject<Item> INVENTORY_OPERATIONS_MODULE = register("inventory_operations_module");
public static final RegistryObject<Item> BLOCK_OPERATIONS_MODULE = register("block_operations_module", BlockOperationsModule::new);

View File

@@ -134,6 +134,36 @@ public final class Network {
.decoder(BusInterfaceNameMessage.ToServer::new)
.consumer(BusInterfaceNameMessage::handleMessageServer)
.add();
INSTANCE.messageBuilder(ExportedFileMessage.class, getNextPacketId(), NetworkDirection.PLAY_TO_CLIENT)
.encoder(ExportedFileMessage::toBytes)
.decoder(ExportedFileMessage::new)
.consumer(ExportedFileMessage::handleMessage)
.add();
INSTANCE.messageBuilder(RequestImportedFileMessage.class, getNextPacketId(), NetworkDirection.PLAY_TO_CLIENT)
.encoder(RequestImportedFileMessage::toBytes)
.decoder(RequestImportedFileMessage::new)
.consumer(RequestImportedFileMessage::handleMessage)
.add();
INSTANCE.messageBuilder(ImportedFileMessage.class, getNextPacketId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(ImportedFileMessage::toBytes)
.decoder(ImportedFileMessage::new)
.consumer(ImportedFileMessage::handleMessage)
.add();
INSTANCE.messageBuilder(ServerCanceledImportFileMessage.class, getNextPacketId(), NetworkDirection.PLAY_TO_CLIENT)
.encoder(ServerCanceledImportFileMessage::toBytes)
.decoder(ServerCanceledImportFileMessage::new)
.consumer(ServerCanceledImportFileMessage::handleMessage)
.add();
INSTANCE.messageBuilder(ClientCanceledImportFileMessage.class, getNextPacketId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(ClientCanceledImportFileMessage::toBytes)
.decoder(ClientCanceledImportFileMessage::new)
.consumer(ClientCanceledImportFileMessage::handleMessage)
.add();
}
public static <T> void sendToClientsTrackingChunk(final T message, final Chunk chunk) {

View File

@@ -0,0 +1,36 @@
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.bus.device.item.CloudInterfaceCardItemDevice;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.fml.network.NetworkEvent;
import java.util.function.Supplier;
public final class ClientCanceledImportFileMessage {
private int id;
///////////////////////////////////////////////////////////////////
public ClientCanceledImportFileMessage(final int id) {
this.id = id;
}
public ClientCanceledImportFileMessage(final PacketBuffer buffer) {
fromBytes(buffer);
}
///////////////////////////////////////////////////////////////////
public static boolean handleMessage(final ClientCanceledImportFileMessage message, final Supplier<NetworkEvent.Context> context) {
CloudInterfaceCardItemDevice.cancelImport(context.get().getSender(), message.id);
return true;
}
public static void toBytes(final ClientCanceledImportFileMessage message, final PacketBuffer buffer) {
buffer.writeVarInt(message.id);
}
public void fromBytes(final PacketBuffer buffer) {
id = buffer.readVarInt();
}
}

View File

@@ -0,0 +1,55 @@
package li.cil.oc2.common.network.message;
import li.cil.oc2.client.gui.FileChooserScreen;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.fml.network.NetworkEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.util.function.Supplier;
public final class ExportedFileMessage {
private static final Logger LOGGER = LogManager.getLogger();
///////////////////////////////////////////////////////////////////
private String name;
private byte[] data;
///////////////////////////////////////////////////////////////////
public ExportedFileMessage(final String name, final byte[] data) {
this.name = name;
this.data = data;
}
public ExportedFileMessage(final PacketBuffer buffer) {
fromBytes(buffer);
}
///////////////////////////////////////////////////////////////////
public static boolean handleMessage(final ExportedFileMessage message, final Supplier<NetworkEvent.Context> context) {
context.get().enqueueWork(() -> FileChooserScreen.openFileChooserForSave(message.name, path -> {
try {
Files.write(path, message.data);
} catch (final IOException e) {
LOGGER.error(e);
}
}));
return true;
}
public static void toBytes(final ExportedFileMessage message, final PacketBuffer buffer) {
buffer.writeString(message.name);
buffer.writeByteArray(message.data);
}
public void fromBytes(final PacketBuffer buffer) {
name = buffer.readString();
data = buffer.readByteArray();
}
}

View File

@@ -0,0 +1,40 @@
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.bus.device.item.CloudInterfaceCardItemDevice;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.fml.network.NetworkEvent;
import java.util.function.Supplier;
public final class ImportedFileMessage {
private int id;
private byte[] data;
///////////////////////////////////////////////////////////////////
public ImportedFileMessage(final int id, final byte[] data) {
this.id = id;
this.data = data;
}
public ImportedFileMessage(final PacketBuffer buffer) {
fromBytes(buffer);
}
///////////////////////////////////////////////////////////////////
public static boolean handleMessage(final ImportedFileMessage message, final Supplier<NetworkEvent.Context> context) {
CloudInterfaceCardItemDevice.setImportedFile(message.id, message.data);
return true;
}
public static void toBytes(final ImportedFileMessage message, final PacketBuffer buffer) {
buffer.writeVarInt(message.id);
buffer.writeByteArray(message.data);
}
public void fromBytes(final PacketBuffer buffer) {
id = buffer.readVarInt();
data = buffer.readByteArray();
}
}

View File

@@ -0,0 +1,74 @@
package li.cil.oc2.common.network.message;
import li.cil.oc2.client.gui.FileChooserScreen;
import li.cil.oc2.common.bus.device.item.CloudInterfaceCardItemDevice;
import li.cil.oc2.common.network.Network;
import net.minecraft.client.Minecraft;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.text.Color;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraftforge.fml.network.NetworkEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Supplier;
public final class RequestImportedFileMessage {
private static final Logger LOGGER = LogManager.getLogger();
private static final TranslationTextComponent FILE_TOO_LARGE_TEXT = new TranslationTextComponent("message.oc2.import_file.file_too_large");
///////////////////////////////////////////////////////////////////
private int id;
///////////////////////////////////////////////////////////////////
public RequestImportedFileMessage(final int id) {
this.id = id;
}
public RequestImportedFileMessage(final PacketBuffer buffer) {
fromBytes(buffer);
}
///////////////////////////////////////////////////////////////////
public static boolean handleMessage(final RequestImportedFileMessage message, final Supplier<NetworkEvent.Context> context) {
context.get().enqueueWork(() -> FileChooserScreen.openFileChooserForLoad(new FileChooserScreen.FileChooserCallback() {
@Override
public void onFileSelected(final Path path) {
try {
final byte[] data = Files.readAllBytes(path);
if (data.length > CloudInterfaceCardItemDevice.MAX_TRANSFERRED_FILE_SIZE) {
Network.INSTANCE.sendToServer(new ClientCanceledImportFileMessage(message.id));
Minecraft.getInstance().player.sendStatusMessage(FILE_TOO_LARGE_TEXT
.modifyStyle(s -> s.setColor(Color.fromInt(0xFFA0A0))), false);
} else {
Network.INSTANCE.sendToServer(new ImportedFileMessage(message.id, data));
}
} catch (final IOException e) {
LOGGER.error(e);
}
}
@Override
public void onCanceled() {
Network.INSTANCE.sendToServer(new ClientCanceledImportFileMessage(message.id));
}
}));
return true;
}
public static void toBytes(final RequestImportedFileMessage message, final PacketBuffer buffer) {
buffer.writeVarInt(message.id);
}
public void fromBytes(final PacketBuffer buffer) {
id = buffer.readVarInt();
}
}

View File

@@ -0,0 +1,36 @@
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.bus.device.item.CloudInterfaceCardItemDevice;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.fml.network.NetworkEvent;
import java.util.function.Supplier;
public final class ServerCanceledImportFileMessage {
private int id;
///////////////////////////////////////////////////////////////////
public ServerCanceledImportFileMessage(final int id) {
this.id = id;
}
public ServerCanceledImportFileMessage(final PacketBuffer buffer) {
fromBytes(buffer);
}
///////////////////////////////////////////////////////////////////
public static boolean handleMessage(final ServerCanceledImportFileMessage message, final Supplier<NetworkEvent.Context> context) {
CloudInterfaceCardItemDevice.cancelImport(context.get().getSender(), message.id);
return true;
}
public static void toBytes(final ServerCanceledImportFileMessage message, final PacketBuffer buffer) {
buffer.writeVarInt(message.id);
}
public void fromBytes(final PacketBuffer buffer) {
id = buffer.readVarInt();
}
}

View File

@@ -5,6 +5,7 @@ import li.cil.oc2.api.bus.device.Device;
import li.cil.oc2.api.bus.device.DeviceTypes;
import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery;
import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery;
import li.cil.oc2.api.capabilities.TerminalUserProvider;
import li.cil.oc2.client.audio.LoopingSoundManager;
import li.cil.oc2.common.Config;
import li.cil.oc2.common.Constants;
@@ -49,14 +50,12 @@ import net.minecraftforge.fml.network.NetworkHooks;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.*;
import static li.cil.oc2.common.Constants.BLOCK_ENTITY_TAG_NAME_IN_ITEM;
import static li.cil.oc2.common.Constants.ITEMS_TAG_NAME;
public final class ComputerTileEntity extends AbstractTileEntity implements ITickableTileEntity {
public final class ComputerTileEntity extends AbstractTileEntity implements ITickableTileEntity, TerminalUserProvider {
private static final String BUS_ELEMENT_TAG_NAME = "busElement";
private static final String TERMINAL_TAG_NAME = "terminal";
private static final String STATE_TAG_NAME = "state";
@@ -80,6 +79,7 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic
private final ComputerItemStackHandlers deviceItems = new ComputerItemStackHandlers();
private final FixedEnergyStorage energy = new FixedEnergyStorage(Config.computerEnergyStorage);
private final ComputerVirtualMachine virtualMachine = new ComputerVirtualMachine(new TileEntityDeviceBusController(busElement, Config.computerEnergyPerTick, this), deviceItems::getDeviceAddressBase);
private final Set<PlayerEntity> terminalUsers = Collections.newSetFromMap(new WeakHashMap<>());
///////////////////////////////////////////////////////////////////
@@ -171,6 +171,19 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic
}, getPos());
}
public void addTerminalUser(final PlayerEntity player) {
terminalUsers.add(player);
}
public void removeTerminalUser(final PlayerEntity player) {
terminalUsers.remove(player);
}
@Override
public Iterable<PlayerEntity> getTerminalUsers() {
return terminalUsers;
}
public void handleNeighborChanged() {
virtualMachine.busController.scheduleBusScan();
}
@@ -292,6 +305,7 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic
protected void collectCapabilities(final CapabilityCollector collector, @Nullable final Direction direction) {
collector.offer(Capabilities.ITEM_HANDLER, deviceItems.combinedItemHandlers);
collector.offer(Capabilities.DEVICE_BUS_ELEMENT, busElement);
collector.offer(Capabilities.TERMINAL_USER_PROVIDER, this);
if (Config.computersUseEnergy()) {
collector.offer(Capabilities.ENERGY_STORAGE, energy);

View File

@@ -35,11 +35,12 @@ public final class ModItemModelProvider extends ItemModelProvider {
.texture("layer1", "item/hard_drive_tint");
simple(Items.FLASH_MEMORY, "item/flash_memory");
simple(Items.FLASH_MEMORY_CUSTOM, "item/flash_memory");
simple(Items.FLOPPY, "item/floppy_base")
.texture("layer1", "item/floppy_tint");
simple(Items.REDSTONE_INTERFACE_CARD, "item/redstone_interface_card");
simple(Items.NETWORK_INTERFACE_CARD, "item/network_interface_card");
simple(Items.FLOPPY, "item/floppy_base")
.texture("layer1", "item/floppy_tint");
simple(Items.CLOUD_INTERFACE_CARD, "item/cloud_interface_card");
simple(Items.INVENTORY_OPERATIONS_MODULE, "item/inventory_operations_module");
simple(Items.BLOCK_OPERATIONS_MODULE, "item/block_operations_module");

View File

@@ -48,17 +48,18 @@ public final class ModItemTagsProvider extends ItemTagsProvider {
Items.FLASH_MEMORY.get(),
Items.FLASH_MEMORY_CUSTOM.get()
);
getOrCreateBuilder(DEVICES_FLOPPY).add(
Items.FLOPPY.get()
);
getOrCreateBuilder(DEVICES_CARD).add(
Items.REDSTONE_INTERFACE_CARD.get(),
Items.NETWORK_INTERFACE_CARD.get()
Items.NETWORK_INTERFACE_CARD.get(),
Items.CLOUD_INTERFACE_CARD.get()
);
getOrCreateBuilder(DEVICES_ROBOT_MODULE).add(
Items.INVENTORY_OPERATIONS_MODULE.get(),
Items.BLOCK_OPERATIONS_MODULE.get()
);
getOrCreateBuilder(DEVICES_FLOPPY).add(
Items.FLOPPY.get()
);
getOrCreateBuilder(TOOL_MATERIALS).addTags(
TOOL_MATERIAL_WOOD,

View File

@@ -56,9 +56,18 @@
"gui.oc2.device_type.flash_memory": "Flash Memory",
"gui.oc2.device_type.card": "Card",
"gui.oc2.file_chooser.title.load": "Open file",
"gui.oc2.file_chooser.title.save": "Save file",
"gui.oc2.file_chooser.text_field.filename": "File name",
"gui.oc2.file_chooser.confirm_button.load": "Open",
"gui.oc2.file_chooser.confirm_button.save": "Save",
"gui.oc2.file_chooser.confirm_button.overwrite": "Overwrite",
"gui.oc2.file_chooser.cancel_button": "Cancel",
"message.oc2.connector.error.full": "Cannot attach more cables.",
"message.oc2.connector.error.too_far": "Distance between connectors is too large.",
"message.oc2.connector.error.obstructed": "No clear line of sight between connectors.",
"message.oc2.import_file.file_too_large": "File is too large.",
"tooltip.oc2.device_needs_reboot": "Requires reboot",
"tooltip.oc2.flash_memory_missing": "A flash memory containing a firmware is required to boot.",

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "oc2:item/cloud_interface_card"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,15 @@
{
"animation": {
"frametime": 1,
"frames": [
{ "index": 0, "time": 4 },
{ "index": 1, "time": 4 },
{ "index": 2, "time": 4 },
{ "index": 3, "time": 4 },
{ "index": 4, "time": 4 },
{ "index": 5, "time": 4 },
{ "index": 6, "time": 4 },
{ "index": 7, "time": 4 }
]
}
}

View File

@@ -0,0 +1,37 @@
#!/usr/bin/lua
local devices = require("devices")
local cloud = devices:find("cloud")
if not cloud then
print("A cloud interface card is required for this functionality.")
return
end
if not arg[1] then
io.write("Usage: export.lua filename\n")
os.exit(1)
end
local file = assert(io.open(arg[1], "rb"))
cloud:reset()
io.write("Exporting")
cloud:beginExportFile(arg[1])
while true do
local str = file:read(512)
if not str then break end
if #str > 0 then
local bytes = {string.byte(str, 1, -1)}
cloud:writeExportFile(bytes)
io.write(".")
end
end
io.write("\n")
cloud:finishExportFile()
assert(file:close())

View File

@@ -0,0 +1,5 @@
{
"attributes": {
"is_executable": true
}
}

View File

@@ -0,0 +1,34 @@
#!/usr/bin/lua
local devices = require("devices")
local cloud = devices:find("cloud")
if not cloud then
print("A cloud interface card is required for this functionality.")
return
end
if not arg[1] then
io.write("Usage: import.lua filename\n")
os.exit(1)
end
local file = assert(io.open(arg[1], "wb"))
cloud:reset()
io.write("Importing")
cloud:beginImportFile()
while true do
local bytes = cloud:readImportFile()
if not bytes then break end
if #bytes > 0 then
file:write(string.char(table.unpack(bytes)))
io.write(".")
end
end
io.write("\n")
assert(file:close())

View File

@@ -0,0 +1,5 @@
{
"attributes": {
"is_executable": true
}
}

View File

@@ -2,6 +2,7 @@
"replace": false,
"values": [
"oc2:redstone_interface_card",
"oc2:network_interface_card"
"oc2:network_interface_card",
"oc2:cloud_interface_card"
]
}