diff --git a/src/main/java/li/cil/oc2/api/capabilities/TerminalUserProvider.java b/src/main/java/li/cil/oc2/api/capabilities/TerminalUserProvider.java
new file mode 100644
index 00000000..63eb82df
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/capabilities/TerminalUserProvider.java
@@ -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.
+ *
+ * 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 getTerminalUsers();
+}
diff --git a/src/main/java/li/cil/oc2/client/gui/ComputerTerminalScreen.java b/src/main/java/li/cil/oc2/client/gui/ComputerTerminalScreen.java
index be0ecb65..ef18c734 100644
--- a/src/main/java/li/cil/oc2/client/gui/ComputerTerminalScreen.java
+++ b/src/main/java/li/cil/oc2/client/gui/ComputerTerminalScreen.java
@@ -28,7 +28,6 @@ public final class ComputerTerminalScreen extends ContainerScreen 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 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 {
+ 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 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 {
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/Config.java b/src/main/java/li/cil/oc2/common/Config.java
index 3d06e09e..6a03dc23 100644
--- a/src/main/java/li/cil/oc2/common/Config.java
+++ b/src/main/java/li/cil/oc2/common/Config.java
@@ -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);
}
diff --git a/src/main/java/li/cil/oc2/common/bus/device/item/CloudInterfaceCardItemDevice.java b/src/main/java/li/cil/oc2/common/bus/device/item/CloudInterfaceCardItemDevice.java
new file mode 100644
index 00000000..edd10106
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/bus/device/item/CloudInterfaceCardItemDevice.java
@@ -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 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 PendingPlayers = Collections.newSetFromMap(new WeakHashMap<>());
+ public final WeakReference Device;
+
+ private ImportFileRequest(final CloudInterfaceCardItemDevice device) {
+ Device = new WeakReference<>(device);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ private static final Int2ObjectArrayMap 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 getTypeNames() {
+ return device.getTypeNames();
+ }
+
+ @Override
+ public List 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.");
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java
index a2438658..e1db3bf7 100644
--- a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java
+++ b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java
@@ -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);
diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/item/CloudInterfaceCardItemDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/item/CloudInterfaceCardItemDeviceProvider.java
new file mode 100644
index 00000000..86df0383
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/bus/device/provider/item/CloudInterfaceCardItemDeviceProvider.java
@@ -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 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 getTerminalUserProvider(final ItemDeviceQuery query) {
+ if (query.getContainerTileEntity().isPresent()) {
+ final LazyOptional capability = query.getContainerTileEntity().get()
+ .getCapability(Capabilities.TERMINAL_USER_PROVIDER);
+ if (capability.isPresent()) {
+ return capability.resolve();
+ }
+ }
+
+ if (query.getContainerEntity().isPresent()) {
+ final LazyOptional capability = query.getContainerEntity().get()
+ .getCapability(Capabilities.TERMINAL_USER_PROVIDER);
+ if (capability.isPresent()) {
+ return capability.resolve();
+ }
+ }
+
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractItemDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractItemDeviceProvider.java
index ef1775c1..03558197 100644
--- a/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractItemDeviceProvider.java
+++ b/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractItemDeviceProvider.java
@@ -53,6 +53,11 @@ public abstract class AbstractItemDeviceProvider extends ForgeRegistryEntry- getItemDevice(final ItemDeviceQuery query);
protected Optional getItemDeviceType(final ItemDeviceQuery query) {
@@ -62,11 +67,4 @@ public abstract class AbstractItemDeviceProvider extends ForgeRegistryEntry
- NETWORK_INTERFACE = null;
+ @CapabilityInject(TerminalUserProvider.class)
+ public static Capability TERMINAL_USER_PROVIDER = null;
+
@CapabilityInject(Robot.class)
public static Capability 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);
}
diff --git a/src/main/java/li/cil/oc2/common/container/ComputerTerminalContainer.java b/src/main/java/li/cil/oc2/common/container/ComputerTerminalContainer.java
index cb2d1e37..30e8f551 100644
--- a/src/main/java/li/cil/oc2/common/container/ComputerTerminalContainer.java
+++ b/src/main/java/li/cil/oc2/common/container/ComputerTerminalContainer.java
@@ -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);
}
diff --git a/src/main/java/li/cil/oc2/common/item/Items.java b/src/main/java/li/cil/oc2/common/item/Items.java
index 99681e86..c1de71de 100644
--- a/src/main/java/li/cil/oc2/common/item/Items.java
+++ b/src/main/java/li/cil/oc2/common/item/Items.java
@@ -59,11 +59,13 @@ public final class Items {
public static final RegistryObject FLASH_MEMORY_CUSTOM = register("flash_memory_custom", () ->
new FlashMemoryWithExternalDataItem(Firmwares.BUILDROOT.getId()));
- public static final RegistryObject
- REDSTONE_INTERFACE_CARD = register("redstone_interface_card");
- public static final RegistryObject
- NETWORK_INTERFACE_CARD = register("network_interface_card");
public static final RegistryObject FLOPPY = register("floppy", () ->
new FloppyItem(512 * Constants.KILOBYTE));
+ public static final RegistryObject
- REDSTONE_INTERFACE_CARD = register("redstone_interface_card");
+ public static final RegistryObject
- NETWORK_INTERFACE_CARD = register("network_interface_card");
+ public static final RegistryObject
- CLOUD_INTERFACE_CARD = register("cloud_interface_card");
+
public static final RegistryObject
- INVENTORY_OPERATIONS_MODULE = register("inventory_operations_module");
public static final RegistryObject
- BLOCK_OPERATIONS_MODULE = register("block_operations_module", BlockOperationsModule::new);
diff --git a/src/main/java/li/cil/oc2/common/network/Network.java b/src/main/java/li/cil/oc2/common/network/Network.java
index 54b075ee..b2d776d4 100644
--- a/src/main/java/li/cil/oc2/common/network/Network.java
+++ b/src/main/java/li/cil/oc2/common/network/Network.java
@@ -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 void sendToClientsTrackingChunk(final T message, final Chunk chunk) {
diff --git a/src/main/java/li/cil/oc2/common/network/message/ClientCanceledImportFileMessage.java b/src/main/java/li/cil/oc2/common/network/message/ClientCanceledImportFileMessage.java
new file mode 100644
index 00000000..3a688d4a
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/network/message/ClientCanceledImportFileMessage.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/network/message/ExportedFileMessage.java b/src/main/java/li/cil/oc2/common/network/message/ExportedFileMessage.java
new file mode 100644
index 00000000..37f4fe98
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/network/message/ExportedFileMessage.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/network/message/ImportedFileMessage.java b/src/main/java/li/cil/oc2/common/network/message/ImportedFileMessage.java
new file mode 100644
index 00000000..bb346274
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/network/message/ImportedFileMessage.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/network/message/RequestImportedFileMessage.java b/src/main/java/li/cil/oc2/common/network/message/RequestImportedFileMessage.java
new file mode 100644
index 00000000..0ff9ce58
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/network/message/RequestImportedFileMessage.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/network/message/ServerCanceledImportFileMessage.java b/src/main/java/li/cil/oc2/common/network/message/ServerCanceledImportFileMessage.java
new file mode 100644
index 00000000..48024a11
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/network/message/ServerCanceledImportFileMessage.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java b/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java
index b1955531..1609a597 100644
--- a/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java
+++ b/src/main/java/li/cil/oc2/common/tileentity/ComputerTileEntity.java
@@ -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 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 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);
diff --git a/src/main/java/li/cil/oc2/data/ModItemModelProvider.java b/src/main/java/li/cil/oc2/data/ModItemModelProvider.java
index 90e86cf4..2ac00912 100644
--- a/src/main/java/li/cil/oc2/data/ModItemModelProvider.java
+++ b/src/main/java/li/cil/oc2/data/ModItemModelProvider.java
@@ -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");
diff --git a/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java b/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java
index 15ab7c30..e534e3c9 100644
--- a/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java
+++ b/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java
@@ -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,
diff --git a/src/main/resources/assets/oc2/lang/en_us.json b/src/main/resources/assets/oc2/lang/en_us.json
index e85dfc48..39895c75 100644
--- a/src/main/resources/assets/oc2/lang/en_us.json
+++ b/src/main/resources/assets/oc2/lang/en_us.json
@@ -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.",
diff --git a/src/main/resources/assets/oc2/models/item/cloud_interface_card.json b/src/main/resources/assets/oc2/models/item/cloud_interface_card.json
new file mode 100644
index 00000000..2450b182
--- /dev/null
+++ b/src/main/resources/assets/oc2/models/item/cloud_interface_card.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "oc2:item/cloud_interface_card"
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/textures/item/cloud_interface_card.png b/src/main/resources/assets/oc2/textures/item/cloud_interface_card.png
new file mode 100644
index 00000000..c5fc9464
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/item/cloud_interface_card.png differ
diff --git a/src/main/resources/assets/oc2/textures/item/cloud_interface_card.png.mcmeta b/src/main/resources/assets/oc2/textures/item/cloud_interface_card.png.mcmeta
new file mode 100644
index 00000000..341f7f62
--- /dev/null
+++ b/src/main/resources/assets/oc2/textures/item/cloud_interface_card.png.mcmeta
@@ -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 }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/data/oc2/file_systems/scripts/bin/export.lua b/src/main/resources/data/oc2/file_systems/scripts/bin/export.lua
new file mode 100644
index 00000000..431e2be4
--- /dev/null
+++ b/src/main/resources/data/oc2/file_systems/scripts/bin/export.lua
@@ -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())
diff --git a/src/main/resources/data/oc2/file_systems/scripts/bin/export.lua.mcmeta b/src/main/resources/data/oc2/file_systems/scripts/bin/export.lua.mcmeta
new file mode 100644
index 00000000..ded6d87e
--- /dev/null
+++ b/src/main/resources/data/oc2/file_systems/scripts/bin/export.lua.mcmeta
@@ -0,0 +1,5 @@
+{
+ "attributes": {
+ "is_executable": true
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/data/oc2/file_systems/scripts/bin/import.lua b/src/main/resources/data/oc2/file_systems/scripts/bin/import.lua
new file mode 100644
index 00000000..6dbbc6b4
--- /dev/null
+++ b/src/main/resources/data/oc2/file_systems/scripts/bin/import.lua
@@ -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())
diff --git a/src/main/resources/data/oc2/file_systems/scripts/bin/import.lua.mcmeta b/src/main/resources/data/oc2/file_systems/scripts/bin/import.lua.mcmeta
new file mode 100644
index 00000000..ded6d87e
--- /dev/null
+++ b/src/main/resources/data/oc2/file_systems/scripts/bin/import.lua.mcmeta
@@ -0,0 +1,5 @@
+{
+ "attributes": {
+ "is_executable": true
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/data/oc2/tags/items/devices/card.json b/src/main/resources/data/oc2/tags/items/devices/card.json
index 5f1eae84..49559e37 100644
--- a/src/main/resources/data/oc2/tags/items/devices/card.json
+++ b/src/main/resources/data/oc2/tags/items/devices/card.json
@@ -2,6 +2,7 @@
"replace": false,
"values": [
"oc2:redstone_interface_card",
- "oc2:network_interface_card"
+ "oc2:network_interface_card",
+ "oc2:cloud_interface_card"
]
}
\ No newline at end of file