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