From a34034379f994053725d54d2afa79a75bb8f75b2 Mon Sep 17 00:00:00 2001 From: simibubi <31564874+simibubi@users.noreply.github.com> Date: Wed, 17 Jul 2019 14:36:51 +0200 Subject: [PATCH] Schematics Mega Polish - Schematicannon makes less obnoxious noises - Schematic Table Container and Gui Improved - Made New Additions Server Proof - Server side Schematic handler is less trusting - Added a Material Checklist for blueprints loaded in the schematicannon - Added more safety checks regarding Schematics & Chunkloading, Filesystem and Distance - Added new option for Schematicannon to not replace tileentities - Schematic upload progress is now handled on the server --- .../create/ClientSchematicLoader.java | 68 ++--- src/main/java/com/simibubi/create/Create.java | 12 + .../create/ServerSchematicLoader.java | 201 ++++++++++--- .../create/block/SchematicTableContainer.java | 80 +----- .../block/SchematicTableTileEntity.java | 62 +++- .../create/block/SchematicannonRenderer.java | 25 +- .../block/SchematicannonTileEntity.java | 271 +++++++++++++----- .../com/simibubi/create/gui/GuiResources.java | 5 +- .../create/gui/SchematicTableScreen.java | 194 +++++++------ .../create/gui/SchematicannonScreen.java | 37 ++- .../simibubi/create/item/ItemBlueprint.java | 12 +- .../PacketConfigureSchematicannon.java | 5 +- .../PacketSchematicTableContainer.java | 44 --- .../networking/PacketSchematicUpload.java | 10 +- .../simibubi/create/networking/Packets.java | 2 - .../create/schematic/BlueprintHandler.java | 11 +- .../create/schematic/MaterialChecklist.java | 135 +++++++++ .../assets/create/textures/gui/icons.png | Bin 2154 -> 2185 bytes .../create/textures/gui/schematicannon.png | Bin 4327 -> 4376 bytes 19 files changed, 772 insertions(+), 402 deletions(-) delete mode 100644 src/main/java/com/simibubi/create/networking/PacketSchematicTableContainer.java create mode 100644 src/main/java/com/simibubi/create/schematic/MaterialChecklist.java diff --git a/src/main/java/com/simibubi/create/ClientSchematicLoader.java b/src/main/java/com/simibubi/create/ClientSchematicLoader.java index eb12aa3aa..d510fda76 100644 --- a/src/main/java/com/simibubi/create/ClientSchematicLoader.java +++ b/src/main/java/com/simibubi/create/ClientSchematicLoader.java @@ -1,5 +1,7 @@ package com.simibubi.create; +import static com.simibubi.create.ServerSchematicLoader.MAX_PACKET_SIZE; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -19,6 +21,7 @@ import com.simibubi.create.networking.Packets; import com.simibubi.create.utility.FilesHelper; import net.minecraft.client.Minecraft; +import net.minecraft.util.text.StringTextComponent; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -26,39 +29,28 @@ import net.minecraftforge.api.distmarker.OnlyIn; public class ClientSchematicLoader { public static final int PACKET_DELAY = 10; - public static final int PACKET_SIZE = 2048; - + private List availableSchematics; private Map activeUploads; - private Map progress; private int packetCycle; - + public ClientSchematicLoader() { availableSchematics = new ArrayList<>(); activeUploads = new HashMap<>(); - progress = new HashMap<>(); refresh(); } - + public void tick() { if (activeUploads.isEmpty()) return; if (packetCycle-- > 0) return; packetCycle = PACKET_DELAY; - + for (String schematic : new HashSet<>(activeUploads.keySet())) { continueUpload(schematic); } } - - public float getProgress(String schematic) { - if (progress.containsKey(schematic)) { - return progress.get(schematic).getProgress(); - } - - return 0; - } public void startNewUpload(String schematic) { Path path = Paths.get("schematics", schematic); @@ -67,15 +59,24 @@ public class ClientSchematicLoader { Create.logger.fatal("Missing Schematic file: " + path.toString()); return; } - + InputStream in; try { + long size = Files.size(path); + + // Too big + if (size > ServerSchematicLoader.MAX_SCHEMATIC_FILE_SIZE) { + Minecraft.getInstance().player + .sendMessage(new StringTextComponent("Your schematic is too large (" + size / 1024 + " KB).")); + Minecraft.getInstance().player + .sendMessage(new StringTextComponent("The maximum allowed schematic file size is: " + + ServerSchematicLoader.MAX_SCHEMATIC_FILE_SIZE / 1024 + " KB")); + return; + } + in = Files.newInputStream(path, StandardOpenOption.READ); activeUploads.put(schematic, in); - ReadProgress tracker = new ReadProgress(); - tracker.length = Files.size(path); - progress.put(schematic, tracker); - Packets.channel.sendToServer(PacketSchematicUpload.begin(schematic)); + Packets.channel.sendToServer(PacketSchematicUpload.begin(schematic, size)); } catch (IOException e) { e.printStackTrace(); } @@ -83,24 +84,21 @@ public class ClientSchematicLoader { private void continueUpload(String schematic) { if (activeUploads.containsKey(schematic)) { - byte[] data = new byte[PACKET_SIZE]; + byte[] data = new byte[MAX_PACKET_SIZE]; try { int status = activeUploads.get(schematic).read(data); - - progress.get(schematic).progress += status; - - if (status < PACKET_SIZE) { + if (status < MAX_PACKET_SIZE) { data = Arrays.copyOf(data, status); } - + if (Minecraft.getInstance().world != null) Packets.channel.sendToServer(PacketSchematicUpload.write(schematic, data)); else { activeUploads.remove(schematic); return; } - - if (status < PACKET_SIZE) + + if (status < MAX_PACKET_SIZE) finishUpload(schematic); } catch (IOException e) { e.printStackTrace(); @@ -114,17 +112,17 @@ public class ClientSchematicLoader { activeUploads.remove(schematic); } } - + public void refresh() { FilesHelper.createFolderIfMissing("schematics"); availableSchematics.clear(); try { Files.list(Paths.get("schematics/")) - .forEach(path -> { + .filter(f -> !Files.isDirectory(f) && f.getFileName().toString().endsWith(".nbt")).forEach(path -> { if (Files.isDirectory(path)) return; - + availableSchematics.add(path.getFileName().toString()); }); } catch (NoSuchFileException e) { @@ -142,13 +140,5 @@ public class ClientSchematicLoader { public Path getPath(String name) { return Paths.get("schematics", name + ".nbt"); } - - public static class ReadProgress { - public long length; - public long progress; - public float getProgress() { - return (float) (progress * 1d / length); - } - } } diff --git a/src/main/java/com/simibubi/create/Create.java b/src/main/java/com/simibubi/create/Create.java index b6b08b98c..5e985d8d5 100644 --- a/src/main/java/com/simibubi/create/Create.java +++ b/src/main/java/com/simibubi/create/Create.java @@ -23,8 +23,10 @@ import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod.EventBusSubscriber; import net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus; import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent.ServerTickEvent; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.server.FMLServerStoppingEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; @EventBusSubscriber(bus = Bus.FORGE) @@ -73,6 +75,16 @@ public class Create { sSchematicLoader = new ServerSchematicLoader(); } + @SubscribeEvent + public static void onTick(ServerTickEvent event) { + sSchematicLoader.tick(); + } + + @SubscribeEvent + public static void onServerClose(FMLServerStoppingEvent event) { + sSchematicLoader.shutdown(); + } + @OnlyIn(Dist.CLIENT) @SubscribeEvent public static void onClientTick(ClientTickEvent event) { diff --git a/src/main/java/com/simibubi/create/ServerSchematicLoader.java b/src/main/java/com/simibubi/create/ServerSchematicLoader.java index b83d93a5e..02d1ab3fc 100644 --- a/src/main/java/com/simibubi/create/ServerSchematicLoader.java +++ b/src/main/java/com/simibubi/create/ServerSchematicLoader.java @@ -3,12 +3,17 @@ package com.simibubi.create; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; -import com.simibubi.create.block.SchematicTableContainer; import com.simibubi.create.block.SchematicTableTileEntity; import com.simibubi.create.item.ItemBlueprint; import com.simibubi.create.networking.PacketSchematicUpload.DimensionPos; @@ -17,80 +22,196 @@ import com.simibubi.create.utility.FilesHelper; import net.minecraft.block.BlockState; import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.item.ItemStack; +import net.minecraft.util.text.StringTextComponent; public class ServerSchematicLoader { public static final String PATH = "schematics/uploaded"; - private Map activeDownloads; - private Map activeTables; + public static final int IDLE_TIMEOUT = 600; + public static final int MAX_PACKET_SIZE = 1024; + public static final int MAX_SCHEMATICS_PER_PLAYER = 10; + public static final int MAX_SCHEMATIC_FILE_SIZE = 256 * 1024; // 256 kiB + + private Map activeUploads; + + public class SchematicUploadEntry { + public OutputStream stream; + public long bytesUploaded; + public long totalBytes; + public DimensionPos tablePos; + public int idleTime; + + public SchematicUploadEntry(OutputStream stream, long totalBytes, DimensionPos tablePos) { + this.stream = stream; + this.totalBytes = totalBytes; + this.tablePos = tablePos; + this.bytesUploaded = 0; + this.idleTime = 0; + } + } public ServerSchematicLoader() { - activeDownloads = new HashMap<>(); - activeTables = new HashMap<>(); + activeUploads = new HashMap<>(); FilesHelper.createFolderIfMissing("schematics"); FilesHelper.createFolderIfMissing(PATH); } - public void handleNewUpload(ServerPlayerEntity player, String schematic, DimensionPos dimPos) { + public void tick() { + // Detect Timed out Uploads + Set deadEntries = new HashSet<>(); + for (String upload : activeUploads.keySet()) { + SchematicUploadEntry entry = activeUploads.get(upload); + + if (entry.idleTime++ > IDLE_TIMEOUT) { + Create.logger.warn("Schematic Upload timed out: " + upload); + deadEntries.add(upload); + } + + } + + // Remove Timed out Uploads + deadEntries.forEach(this::cancelUpload); + } + + public void shutdown() { + // Close open streams + new HashSet<>(activeUploads.keySet()).forEach(this::cancelUpload); + } + + public void handleNewUpload(ServerPlayerEntity player, String schematic, long size, DimensionPos dimPos) { String playerPath = PATH + "/" + player.getName().getFormattedText(); String playerSchematicId = player.getName().getFormattedText() + "/" + schematic; - FilesHelper.createFolderIfMissing(playerPath); - if (activeDownloads.containsKey(playerSchematicId)) + // Unsupported Format + if (!schematic.endsWith(".nbt")) { + Create.logger.warn("Attempted Schematic Upload with non-supported Format: " + playerSchematicId); + } + + // Too big + if (size > MAX_SCHEMATIC_FILE_SIZE) { + player.sendMessage(new StringTextComponent("Your schematic is too large (" + size/1024 + " KB).")); + player.sendMessage(new StringTextComponent( + "The maximum allowed schematic file size is: " + MAX_SCHEMATIC_FILE_SIZE/1024 + " KB")); + return; + } + + // Skip existing Uploads + if (activeUploads.containsKey(playerSchematicId)) return; try { + // Validate Referenced Block + BlockState blockState = dimPos.world.getBlockState(dimPos.pos); + if (!AllBlocks.SCHEMATIC_TABLE.typeOf(blockState)) + return; + + // Delete schematic with same name Files.deleteIfExists(Paths.get(PATH, playerSchematicId)); + + // Too many Schematics + Stream list = Files.list(Paths.get(playerPath)); + if (list.count() >= MAX_SCHEMATICS_PER_PLAYER) { + Stream list2 = Files.list(Paths.get(playerPath)); + Optional lastFilePath = list2.filter(f -> !Files.isDirectory(f)) + .min(Comparator.comparingLong(f -> f.toFile().lastModified())); + list2.close(); + if (lastFilePath.isPresent()) { + Files.deleteIfExists(lastFilePath.get()); + } + } + list.close(); + + // Open Stream OutputStream writer = Files.newOutputStream(Paths.get(PATH, playerSchematicId), StandardOpenOption.CREATE_NEW); - Create.logger.info("Receiving New Schematic: " + playerSchematicId); - activeDownloads.put(playerSchematicId, writer); - activeTables.put(playerSchematicId, dimPos); + activeUploads.put(playerSchematicId, new SchematicUploadEntry(writer, size, dimPos)); - SchematicTableTileEntity tileEntity = (SchematicTableTileEntity) dimPos.world - .getTileEntity(dimPos.pos); - tileEntity.uploadingSchematic = schematic; - tileEntity.uploadingProgress = 0; - BlockState blockState = dimPos.world.getBlockState(dimPos.pos); - dimPos.world.notifyBlockUpdate(dimPos.pos, blockState, blockState, 6); - - if (player.openContainer instanceof SchematicTableContainer) { - SchematicTableContainer c = (SchematicTableContainer) player.openContainer; - c.sendSchematicUpdate = true; - c.detectAndSendChanges(); - } + // Notify Tile Entity + SchematicTableTileEntity tileEntity = (SchematicTableTileEntity) dimPos.world.getTileEntity(dimPos.pos); + tileEntity.startUpload(schematic); } catch (IOException e) { + Create.logger.error("Exception Thrown when starting Upload: " + playerSchematicId); e.printStackTrace(); } } public void handleWriteRequest(ServerPlayerEntity player, String schematic, byte[] data) { String playerSchematicId = player.getName().getFormattedText() + "/" + schematic; - if (activeDownloads.containsKey(playerSchematicId)) { + + if (activeUploads.containsKey(playerSchematicId)) { + SchematicUploadEntry entry = activeUploads.get(playerSchematicId); + entry.bytesUploaded += data.length; + + // Size Validations + if (data.length > MAX_PACKET_SIZE) { + Create.logger.warn("Oversized Upload Packet received: " + playerSchematicId); + cancelUpload(playerSchematicId); + return; + } + + if (entry.bytesUploaded > entry.totalBytes) { + Create.logger.warn("Received more data than Expected: " + playerSchematicId); + cancelUpload(playerSchematicId); + return; + } try { - activeDownloads.get(playerSchematicId).write(data); - Create.logger.info("Writing to Schematic: " + playerSchematicId); + entry.stream.write(data); + entry.idleTime = 0; + BlockState blockState = entry.tablePos.world.getBlockState(entry.tablePos.pos); + if (!AllBlocks.SCHEMATIC_TABLE.typeOf(blockState)) + return; + + SchematicTableTileEntity tileEntity = (SchematicTableTileEntity) entry.tablePos.world + .getTileEntity(entry.tablePos.pos); + tileEntity.uploadingProgress = (float) ((double) entry.bytesUploaded / entry.totalBytes); + tileEntity.sendUpdate = true; + } catch (IOException e) { + Create.logger.error("Exception Thrown when uploading Schematic: " + playerSchematicId); e.printStackTrace(); - activeDownloads.remove(playerSchematicId); - activeTables.remove(playerSchematicId); + cancelUpload(playerSchematicId); } } } + protected void cancelUpload(String playerSchematicId) { + if (!activeUploads.containsKey(playerSchematicId)) + return; + + SchematicUploadEntry entry = activeUploads.remove(playerSchematicId); + try { + entry.stream.close(); + Files.deleteIfExists(Paths.get(PATH, playerSchematicId)); + Create.logger.warn("Cancelled Schematic Upload: " + playerSchematicId); + + } catch (IOException e) { + Create.logger.error("Exception Thrown when cancelling Upload: " + playerSchematicId); + e.printStackTrace(); + } + + DimensionPos dimpos = entry.tablePos; + if (dimpos == null) + return; + + BlockState blockState = dimpos.world.getBlockState(dimpos.pos); + if (!AllBlocks.SCHEMATIC_TABLE.typeOf(blockState)) + return; + + SchematicTableTileEntity tileEntity = (SchematicTableTileEntity) dimpos.world.getTileEntity(dimpos.pos); + tileEntity.finishUpload(); + } + public void handleFinishedUpload(ServerPlayerEntity player, String schematic) { String playerSchematicId = player.getName().getFormattedText() + "/" + schematic; - if (activeDownloads.containsKey(playerSchematicId)) { + if (activeUploads.containsKey(playerSchematicId)) { try { - activeDownloads.get(playerSchematicId).close(); - activeDownloads.remove(playerSchematicId); - Create.logger.info("Finished receiving Schematic: " + playerSchematicId); - - DimensionPos dimpos = activeTables.remove(playerSchematicId); + activeUploads.get(playerSchematicId).stream.close(); + DimensionPos dimpos = activeUploads.remove(playerSchematicId).tablePos; + Create.logger.info("New Schematic Uploaded: " + playerSchematicId); if (dimpos == null) return; @@ -100,21 +221,13 @@ public class ServerSchematicLoader { return; SchematicTableTileEntity tileEntity = (SchematicTableTileEntity) dimpos.world.getTileEntity(dimpos.pos); - + tileEntity.finishUpload(); tileEntity.inventory.setStackInSlot(0, ItemStack.EMPTY); tileEntity.inventory.setStackInSlot(1, ItemBlueprint.create(schematic, player.getName().getFormattedText())); - tileEntity.uploadingSchematic = null; - tileEntity.uploadingProgress = 0; - dimpos.world.notifyBlockUpdate(dimpos.pos, blockState, blockState, 3); - - if (player.openContainer instanceof SchematicTableContainer) { - SchematicTableContainer c = (SchematicTableContainer) player.openContainer; - c.sendSchematicUpdate = true; - c.detectAndSendChanges(); - } } catch (IOException e) { + Create.logger.error("Exception Thrown when finishing Upload: " + playerSchematicId); e.printStackTrace(); } } diff --git a/src/main/java/com/simibubi/create/block/SchematicTableContainer.java b/src/main/java/com/simibubi/create/block/SchematicTableContainer.java index 75eb6d45a..23b004a84 100644 --- a/src/main/java/com/simibubi/create/block/SchematicTableContainer.java +++ b/src/main/java/com/simibubi/create/block/SchematicTableContainer.java @@ -2,21 +2,15 @@ package com.simibubi.create.block; import com.simibubi.create.AllContainers; import com.simibubi.create.AllItems; -import com.simibubi.create.networking.PacketSchematicTableContainer; -import com.simibubi.create.networking.Packets; import net.minecraft.client.Minecraft; import net.minecraft.client.world.ClientWorld; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.inventory.container.Container; import net.minecraft.inventory.container.Slot; import net.minecraft.item.ItemStack; import net.minecraft.network.PacketBuffer; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.fml.network.PacketDistributor; import net.minecraftforge.items.SlotItemHandler; public class SchematicTableContainer extends Container { @@ -26,11 +20,6 @@ public class SchematicTableContainer extends Container { private Slot outputSlot; private PlayerEntity player; - public String schematicUploading; - public boolean isUploading; - public float progress; - public boolean sendSchematicUpdate; - public SchematicTableContainer(int id, PlayerInventory inv, PacketBuffer extraData) { super(AllContainers.SchematicTable.type, id); player = inv.player; @@ -68,32 +57,17 @@ public class SchematicTableContainer extends Container { // player Slots for (int row = 0; row < 3; ++row) { for (int col = 0; col < 9; ++col) { - this.addSlot(new Slot(player.inventory, col + row * 9 + 9, -8 + col * 18, 102 + row * 18)); + this.addSlot(new Slot(player.inventory, col + row * 9 + 9, 12 + col * 18, 102 + row * 18)); } } for (int hotbarSlot = 0; hotbarSlot < 9; ++hotbarSlot) { - this.addSlot(new Slot(player.inventory, hotbarSlot, -8 + hotbarSlot * 18, 160)); + this.addSlot(new Slot(player.inventory, hotbarSlot, 12 + hotbarSlot * 18, 160)); } detectAndSendChanges(); } - @Override - public void detectAndSendChanges() { - if (te.uploadingSchematic != null) { - schematicUploading = te.uploadingSchematic; - progress = te.uploadingProgress; - isUploading = true; - } else { - schematicUploading = null; - progress = 0; - isUploading = false; - } - super.detectAndSendChanges(); - sendSchematicInfo(); - } - public boolean canWrite() { return inputSlot.getHasStack() && !outputSlot.getHasStack(); } @@ -110,54 +84,14 @@ public class SchematicTableContainer extends Container { return ItemStack.EMPTY; ItemStack stack = clickedSlot.getStack(); - if (clickedSlot == inputSlot || clickedSlot == outputSlot) { - int indexToPut = playerIn.inventory.getFirstEmptyStack(); - - if (indexToPut == -1) - return ItemStack.EMPTY; - - playerIn.inventory.setInventorySlotContents(indexToPut, stack); - clickedSlot.putStack(ItemStack.EMPTY); - return ItemStack.EMPTY; - } - - if (AllItems.EMPTY_BLUEPRINT.typeOf(stack) && !inputSlot.getHasStack()) { - clickedSlot.putStack(ItemStack.EMPTY); - inputSlot.putStack(stack); - } - + if (index < 2) + mergeItemStack(stack, 2, inventorySlots.size(), false); + else + mergeItemStack(stack, 0, 1, false); + return ItemStack.EMPTY; } - public void sendSchematicInfo() { - if (player instanceof ServerPlayerEntity) { - if (sendSchematicUpdate) { - Packets.channel.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) player), - new PacketSchematicTableContainer(schematicUploading, progress)); - sendSchematicUpdate = false; - } - } - } - - @OnlyIn(Dist.CLIENT) - public void receiveSchematicInfo(String schematic, float progress) { - if (schematic.isEmpty()) { - this.schematicUploading = null; - this.isUploading = false; - this.progress = 0; - return; - } - - this.isUploading = true; - this.schematicUploading = schematic; - this.progress = .5f; - } - - @Override - public void onContainerClosed(PlayerEntity playerIn) { - super.onContainerClosed(playerIn); - } - public SchematicTableTileEntity getTileEntity() { return te; } diff --git a/src/main/java/com/simibubi/create/block/SchematicTableTileEntity.java b/src/main/java/com/simibubi/create/block/SchematicTableTileEntity.java index d2726a1df..8e50a4a7c 100644 --- a/src/main/java/com/simibubi/create/block/SchematicTableTileEntity.java +++ b/src/main/java/com/simibubi/create/block/SchematicTableTileEntity.java @@ -18,63 +18,97 @@ import net.minecraftforge.items.ItemStackHandler; public class SchematicTableTileEntity extends TileEntitySynced implements ITickableTileEntity, INamedContainerProvider { public SchematicTableInventory inventory; + public boolean isUploading; public String uploadingSchematic; public float uploadingProgress; - + public boolean sendUpdate; + public class SchematicTableInventory extends ItemStackHandler { public SchematicTableInventory() { super(2); } - + @Override protected void onContentsChanged(int slot) { super.onContentsChanged(slot); markDirty(); } } - + public SchematicTableTileEntity() { this(AllTileEntities.SchematicTable.type); } - + public SchematicTableTileEntity(TileEntityType tileEntityTypeIn) { super(tileEntityTypeIn); inventory = new SchematicTableInventory(); uploadingSchematic = null; uploadingProgress = 0; } - + public void sendToContainer(PacketBuffer buffer) { buffer.writeBlockPos(getPos()); buffer.writeCompoundTag(getUpdateTag()); } - + @Override public void read(CompoundNBT compound) { inventory.deserializeNBT(compound.getCompound("Inventory")); - if (compound.contains("Schematic")) { + readClientUpdate(compound); + super.read(compound); + } + + @Override + public void readClientUpdate(CompoundNBT compound) { + if (compound.contains("Uploading")) { + isUploading = true; uploadingSchematic = compound.getString("Schematic"); uploadingProgress = compound.getFloat("Progress"); } else { + isUploading = false; uploadingSchematic = null; uploadingProgress = 0; } - super.read(compound); } - + @Override public CompoundNBT write(CompoundNBT compound) { compound.put("Inventory", inventory.serializeNBT()); - if (uploadingSchematic != null) { - compound.putString("Schematic", uploadingSchematic); - compound.putFloat("Progress", uploadingProgress); - } + writeToClient(compound); return super.write(compound); } + @Override + public CompoundNBT writeToClient(CompoundNBT compound) { + if (isUploading) { + compound.putBoolean("Uploading", true); + compound.putString("Schematic", uploadingSchematic); + compound.putFloat("Progress", uploadingProgress); + } + return compound; + } + @Override public void tick() { - + // Update Client Tile + if (sendUpdate) { + sendUpdate = false; + world.notifyBlockUpdate(pos, getBlockState(), getBlockState(), 6); + } + } + + public void startUpload(String schematic) { + isUploading = true; + uploadingProgress = 0; + uploadingSchematic = schematic; + sendUpdate = true; + } + + public void finishUpload() { + isUploading = false; + uploadingProgress = 0; + uploadingSchematic = null; + sendUpdate = true; } @Override diff --git a/src/main/java/com/simibubi/create/block/SchematicannonRenderer.java b/src/main/java/com/simibubi/create/block/SchematicannonRenderer.java index b8bbb5321..e48d5c136 100644 --- a/src/main/java/com/simibubi/create/block/SchematicannonRenderer.java +++ b/src/main/java/com/simibubi/create/block/SchematicannonRenderer.java @@ -1,11 +1,14 @@ package com.simibubi.create.block; +import java.util.Random; + import com.mojang.blaze3d.platform.GlStateManager; import com.simibubi.create.AllBlocks; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.texture.AtlasTexture; import net.minecraft.client.renderer.tileentity.TileEntityRenderer; +import net.minecraft.particles.ParticleTypes; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; @@ -50,7 +53,7 @@ public class SchematicannonRenderer extends TileEntityRenderer block.totalTicks - 10) { recoil = Math.max(recoil, (block.ticksRemaining + 1 - partialTicks) - block.totalTicks + 10); } + + // Render particles for launch + if (block.ticksRemaining == block.totalTicks && tileEntityIn.firstRenderTick) { + tileEntityIn.firstRenderTick = false; + for (int i = 0; i < 10; i++) { + Random r = tileEntityIn.getWorld().getRandom(); + double sX = cannonOffset.x * .01f; + double sY = (cannonOffset.y + 1) * .01f; + double sZ = cannonOffset.z * .01f; + double rX = r.nextFloat() - sX * 40; + double rY = r.nextFloat() - sY * 40; + double rZ = r.nextFloat() - sZ * 40; + tileEntityIn.getWorld().addParticle(ParticleTypes.CLOUD, start.x + rX, start.y + rY, start.z + rZ, + sX, sY, sZ); + } + } + } } diff --git a/src/main/java/com/simibubi/create/block/SchematicannonTileEntity.java b/src/main/java/com/simibubi/create/block/SchematicannonTileEntity.java index 6b358c201..25c753371 100644 --- a/src/main/java/com/simibubi/create/block/SchematicannonTileEntity.java +++ b/src/main/java/com/simibubi/create/block/SchematicannonTileEntity.java @@ -9,12 +9,12 @@ import com.simibubi.create.AllItems; import com.simibubi.create.AllTileEntities; import com.simibubi.create.item.ItemBlueprint; import com.simibubi.create.schematic.Cuboid; +import com.simibubi.create.schematic.MaterialChecklist; import com.simibubi.create.schematic.SchematicWorld; import com.simibubi.create.utility.TileEntitySynced; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; -import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.inventory.container.Container; @@ -35,10 +35,8 @@ import net.minecraft.util.SoundEvents; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; -import net.minecraft.util.math.Vec3d; import net.minecraft.util.text.ITextComponent; import net.minecraft.util.text.StringTextComponent; -import net.minecraft.world.Explosion; import net.minecraft.world.gen.feature.template.Template; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -49,9 +47,12 @@ import net.minecraftforge.items.ItemStackHandler; public class SchematicannonTileEntity extends TileEntitySynced implements ITickableTileEntity, INamedContainerProvider { + public static final int NEIGHBOUR_CHECKING = 100; public static final int PLACEMENT_DELAY = 10; public static final float FUEL_PER_GUNPOWDER = .2f; - public static final float FUEL_USAGE_RATE = .0001f; + public static final float FUEL_USAGE_RATE = .0005f; + public static final int SKIPS_PER_TICK = 10; + public static final int MAX_ANCHOR_DISTANCE = 256; public enum State { STOPPED, PAUSED, RUNNING; @@ -62,6 +63,8 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka // Sync public boolean sendUpdate; + public boolean dontUpdateChecklist; + public int neighbourCheckCooldown; // Printer private SchematicWorld blockReader; @@ -69,26 +72,35 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka public BlockPos schematicAnchor; public boolean schematicLoaded; public boolean missingBlock; + public boolean blockNotLoaded; public boolean hasCreativeCrate; private int printerCooldown; + private int skipsLeft; + private boolean blockSkipped; public BlockPos target; public BlockPos previousTarget; public List attachedInventories; public List flyingBlocks; + public MaterialChecklist checklist; // Gui information public float fuelLevel; - public float paperPrintingProgress; + public float bookPrintingProgress; public float schematicProgress; public String statusMsg; public State state; + public int blocksPlaced; + public int blocksToPlace; // Settings public int replaceMode; public boolean skipMissing; public boolean replaceTileEntities; + // Render + public boolean firstRenderTick; + public class SchematicannonInventory extends ItemStackHandler { public SchematicannonInventory() { super(5); @@ -107,8 +119,8 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka return AllItems.BLUEPRINT.typeOf(stack); case 1: // Blueprint output return false; - case 2: // Paper input - return stack.getItem() == Items.PAPER; + case 2: // Book input + return stack.getItem() == Items.BOOK || stack.getItem() == Items.WRITTEN_BOOK; case 3: // Material List output return false; case 4: // Gunpowder @@ -168,12 +180,18 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka statusMsg = "Idle"; state = State.STOPPED; replaceMode = 2; + neighbourCheckCooldown = NEIGHBOUR_CHECKING; + checklist = new MaterialChecklist(); } public void findInventories() { hasCreativeCrate = false; + attachedInventories.clear(); for (Direction facing : Direction.values()) { + if (!world.isBlockPresent(pos.offset(facing))) + continue; + if (AllBlocks.CREATIVE_CRATE.typeOf(world.getBlockState(pos.offset(facing)))) { hasCreativeCrate = true; } @@ -192,19 +210,13 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka @Override public void read(CompoundNBT compound) { inventory.deserializeNBT(compound.getCompound("Inventory")); - + if (compound.contains("Running")) currentPos = NBTUtil.readBlockPos(compound.getCompound("CurrentPos")); - + readClientUpdate(compound); super.read(compound); } - - @Override - public void onLoad() { - findInventories(); - super.onLoad(); - } @Override public void readClientUpdate(CompoundNBT compound) { @@ -212,9 +224,11 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka // Gui information statusMsg = compound.getString("Status"); schematicProgress = compound.getFloat("Progress"); - paperPrintingProgress = compound.getFloat("PaperProgress"); + bookPrintingProgress = compound.getFloat("PaperProgress"); fuelLevel = compound.getFloat("Fuel"); state = State.valueOf(compound.getString("State")); + blocksPlaced = compound.getInt("AmountPlaced"); + blocksToPlace = compound.getInt("AmountToPlace"); // Settings CompoundNBT options = compound.getCompound("Options"); @@ -271,12 +285,12 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka @Override public CompoundNBT write(CompoundNBT compound) { compound.put("Inventory", inventory.serializeNBT()); - + if (state == State.RUNNING) { compound.putBoolean("Running", true); compound.put("CurrentPos", NBTUtil.writeBlockPos(currentPos)); } - + writeToClient(compound); return super.write(compound); } @@ -286,10 +300,12 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka // Gui information compound.putFloat("Progress", schematicProgress); - compound.putFloat("PaperProgress", paperPrintingProgress); + compound.putFloat("PaperProgress", bookPrintingProgress); compound.putFloat("Fuel", fuelLevel); compound.putString("Status", statusMsg); compound.putString("State", state.name()); + compound.putInt("AmountPlaced", blocksPlaced); + compound.putInt("AmountToPlace", blocksToPlace); // Settings CompoundNBT options = new CompoundNBT(); @@ -317,6 +333,12 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka @Override public void tick() { + if (neighbourCheckCooldown-- <= 0) { + neighbourCheckCooldown = NEIGHBOUR_CHECKING; + findInventories(); + } + + firstRenderTick = true; previousTarget = target; tickFlyingBlocks(); @@ -328,7 +350,15 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka refillFuelIfPossible(); // Update Printer - tickPrinter(); + skipsLeft = SKIPS_PER_TICK; + blockSkipped = true; + + while (blockSkipped && skipsLeft-- > 0) + tickPrinter(); + + schematicProgress = 0; + if (blocksToPlace > 0) + schematicProgress = (float) blocksPlaced / blocksToPlace; // Update Client Tile if (sendUpdate) { @@ -339,6 +369,7 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka protected void tickPrinter() { ItemStack blueprint = inventory.getStackInSlot(0); + blockSkipped = false; // Skip if not Active if (state == State.STOPPED) { @@ -346,8 +377,6 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka resetPrinter(); return; } - if (state == State.PAUSED && !missingBlock && fuelLevel > FUEL_USAGE_RATE) - return; if (blueprint.isEmpty()) { state = State.STOPPED; @@ -356,31 +385,12 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka return; } + if (state == State.PAUSED && !blockNotLoaded && !missingBlock && fuelLevel > FUEL_USAGE_RATE) + return; + // Initialize Printer if (!schematicLoaded) { - if (!blueprint.hasTag()) { - state = State.STOPPED; - statusMsg = "Invalid Blueprint"; - sendUpdate = true; - return; - } - - if (!blueprint.getTag().getBoolean("Deployed")) { - state = State.STOPPED; - statusMsg = "Blueprint not Deployed"; - sendUpdate = true; - return; - } - - currentPos = currentPos != null ? currentPos.west() : BlockPos.ZERO.west(); - schematicAnchor = NBTUtil.readBlockPos(blueprint.getTag().getCompound("Anchor")); - - // Load blocks into reader - Template activeTemplate = ItemBlueprint.getSchematic(blueprint); - blockReader = new SchematicWorld(new HashMap<>(), new Cuboid(), schematicAnchor); - activeTemplate.addBlocksToWorld(blockReader, schematicAnchor, ItemBlueprint.getSettings(blueprint)); - schematicLoaded = true; - sendUpdate = true; + initializePrinter(blueprint); return; } @@ -400,7 +410,13 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka } // Update Target - if (!missingBlock) { + if (hasCreativeCrate) { + if (missingBlock) { + missingBlock = false; + state = State.RUNNING; + } + } + if (!missingBlock && !blockNotLoaded) { advanceCurrentPos(); // End reached @@ -412,15 +428,31 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka } // Check block - BlockState blockState = blockReader.getBlockState(target); - if (!shouldPlace(target, blockState)) + if (!getWorld().isAreaLoaded(target, 0)) { + blockNotLoaded = true; + statusMsg = "Block is not loaded"; + state = State.PAUSED; return; + } else { + if (blockNotLoaded) { + blockNotLoaded = false; + state = State.RUNNING; + } + } + + BlockState blockState = blockReader.getBlockState(target); + if (!shouldPlace(target, blockState)) { + statusMsg = "Searching"; + blockSkipped = true; + return; + } // Find Item ItemStack requiredItem = getItemForBlock(blockState); if (!findItemInAttachedInventories(requiredItem)) { if (skipMissing) { statusMsg = "Skipping"; + blockSkipped = true; if (missingBlock) { missingBlock = false; state = State.RUNNING; @@ -436,7 +468,10 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka // Success state = State.RUNNING; - statusMsg = "Running..."; + if (blockState.getBlock() != Blocks.AIR) + statusMsg = "Placing: " + blocksPlaced + " / " + blocksToPlace; + else + statusMsg = "Clearing Blocks"; launchBlock(target, blockState); printerCooldown = PLACEMENT_DELAY; fuelLevel -= FUEL_USAGE_RATE; @@ -444,6 +479,51 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka missingBlock = false; } + protected void initializePrinter(ItemStack blueprint) { + if (!blueprint.hasTag()) { + state = State.STOPPED; + statusMsg = "Invalid Blueprint"; + sendUpdate = true; + return; + } + + if (!blueprint.getTag().getBoolean("Deployed")) { + state = State.STOPPED; + statusMsg = "Blueprint not Deployed"; + sendUpdate = true; + return; + } + + // Load blocks into reader + Template activeTemplate = ItemBlueprint.getSchematic(blueprint); + BlockPos anchor = NBTUtil.readBlockPos(blueprint.getTag().getCompound("Anchor")); + + if (activeTemplate.getSize().equals(BlockPos.ZERO)) { + state = State.STOPPED; + statusMsg = "Schematic File Expired"; + inventory.setStackInSlot(0, ItemStack.EMPTY); + inventory.setStackInSlot(1, new ItemStack(AllItems.EMPTY_BLUEPRINT.get())); + return; + } + + if (!anchor.withinDistance(getPos(), MAX_ANCHOR_DISTANCE)) { + state = State.STOPPED; + statusMsg = "Target too Far Away"; + return; + } + + schematicAnchor = anchor; + blockReader = new SchematicWorld(new HashMap<>(), new Cuboid(), schematicAnchor); + activeTemplate.addBlocksToWorld(blockReader, schematicAnchor, ItemBlueprint.getSettings(blueprint)); + schematicLoaded = true; + state = State.PAUSED; + statusMsg = "Ready"; + updateChecklist(); + sendUpdate = true; + blocksToPlace += blocksPlaced; + currentPos = currentPos != null ? currentPos.west() : blockReader.getBounds().getOrigin().west(); + } + protected ItemStack getItemForBlock(BlockState blockState) { return new ItemStack(BlockItem.BLOCK_TO_ITEM.getOrDefault(blockState.getBlock(), Items.AIR)); } @@ -467,18 +547,18 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka protected void advanceCurrentPos() { BlockPos size = blockReader.getBounds().getSize(); currentPos = currentPos.offset(Direction.EAST); + BlockPos posInBounds = currentPos.subtract(blockReader.getBounds().getOrigin()); - schematicProgress += 1d / (size.getX() * size.getY() * size.getZ()); - - if (currentPos.getX() > size.getX()) - currentPos = new BlockPos(0, currentPos.getY(), currentPos.getZ() + 1); - if (currentPos.getZ() > size.getZ()) - currentPos = new BlockPos(currentPos.getX(), currentPos.getY() + 1, 0); + if (posInBounds.getX() > size.getX()) + currentPos = new BlockPos(blockReader.getBounds().x, currentPos.getY(), currentPos.getZ() + 1).west(); + if (posInBounds.getZ() > size.getZ()) + currentPos = new BlockPos(currentPos.getX(), currentPos.getY() + 1, blockReader.getBounds().z).west(); // End reached if (currentPos.getY() > size.getY()) { inventory.setStackInSlot(0, ItemStack.EMPTY); - inventory.setStackInSlot(1, new ItemStack(AllItems.EMPTY_BLUEPRINT.get())); + inventory.setStackInSlot(1, + new ItemStack(AllItems.EMPTY_BLUEPRINT.get(), inventory.getStackInSlot(1).getCount() + 1)); state = State.STOPPED; statusMsg = "Finished"; resetPrinter(); @@ -498,6 +578,8 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka missingBlock = false; sendUpdate = true; schematicProgress = 0; + blocksPlaced = 0; + blocksToPlace = 0; } protected boolean shouldPlace(BlockPos pos, BlockState state) { @@ -548,37 +630,52 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka } protected void tickPaperPrinter() { - int PaperInput = 2; - int PaperOutput = 3; + int BookInput = 2; + int BookOutput = 3; - ItemStack paper = inventory.extractItem(PaperInput, 1, true); - boolean outputFull = inventory.getStackInSlot(PaperOutput).getCount() == inventory.getSlotLimit(PaperOutput); + ItemStack blueprint = inventory.getStackInSlot(0); + ItemStack paper = inventory.extractItem(BookInput, 1, true); + boolean outputFull = inventory.getStackInSlot(BookOutput).getCount() == inventory.getSlotLimit(BookOutput); if (paper.isEmpty() || outputFull) { - if (paperPrintingProgress != 0) + if (bookPrintingProgress != 0) sendUpdate = true; - paperPrintingProgress = 0; + bookPrintingProgress = 0; + dontUpdateChecklist = false; return; } - if (paperPrintingProgress >= 1) { - paperPrintingProgress = 0; - inventory.extractItem(PaperInput, 1, false); - inventory.setStackInSlot(PaperOutput, - new ItemStack(Items.PAPER, inventory.getStackInSlot(PaperOutput).getCount() + 1)); + if (!schematicLoaded) { + if (!blueprint.isEmpty()) + initializePrinter(blueprint); + return; + } + + if (bookPrintingProgress >= 1) { + bookPrintingProgress = 0; + + if (!dontUpdateChecklist) + updateChecklist(); + + dontUpdateChecklist = true; + inventory.extractItem(BookInput, 1, false); + ItemStack stack = checklist.createItem(); + stack.setCount(inventory.getStackInSlot(BookOutput).getCount() + 1); + inventory.setStackInSlot(BookOutput, stack); sendUpdate = true; return; } - paperPrintingProgress += 0.05f; + bookPrintingProgress += 0.05f; sendUpdate = true; } protected void launchBlock(BlockPos target, BlockState state) { + if (state.getBlock() != Blocks.AIR) + blocksPlaced++; flyingBlocks.add(new LaunchedBlock(target, state)); - Vec3d explosionPos = new Vec3d(pos).add(new Vec3d(target.subtract(pos)).normalize()); - this.world.createExplosion((Entity) null, explosionPos.x, explosionPos.y + 1.5f, explosionPos.z, 0, - Explosion.Mode.NONE); + world.playSound(null, pos.getX(), pos.getY(), pos.getZ(), SoundEvents.ENTITY_GENERIC_EXPLODE, SoundCategory.BLOCKS, + .1f, 1.1f); } public void sendToContainer(PacketBuffer buffer) { @@ -596,4 +693,36 @@ public class SchematicannonTileEntity extends TileEntitySynced implements ITicka return new StringTextComponent(getType().getRegistryName().toString()); } + public void updateChecklist() { + checklist.required.clear(); + checklist.blocksNotLoaded = false; + + if (schematicLoaded) { + blocksToPlace = blocksPlaced; + for (BlockPos pos : blockReader.getAllPositions()) { + BlockState required = blockReader.getBlockState(pos.add(schematicAnchor)); + + if (!getWorld().isAreaLoaded(pos.add(schematicAnchor), 0)) { + checklist.warnBlockNotLoaded(); + continue; + } + if (!shouldPlace(pos.add(schematicAnchor), required)) + continue; + ItemStack requiredItem = getItemForBlock(required); + checklist.require(requiredItem.getItem()); + blocksToPlace++; + } + } + checklist.gathered.clear(); + for (IItemHandler inventory : attachedInventories) { + for (int slot = 0; slot < inventory.getSlots(); slot++) { + ItemStack stackInSlot = inventory.getStackInSlot(slot); + if (inventory.extractItem(slot, 1, true).isEmpty()) + continue; + checklist.collect(stackInSlot); + } + } + sendUpdate = true; + } + } diff --git a/src/main/java/com/simibubi/create/gui/GuiResources.java b/src/main/java/com/simibubi/create/gui/GuiResources.java index f78670bb3..8f42dabac 100644 --- a/src/main/java/com/simibubi/create/gui/GuiResources.java +++ b/src/main/java/com/simibubi/create/gui/GuiResources.java @@ -45,8 +45,8 @@ public enum GuiResources { ICON_TARGET("icons.png", 48, 0, 16, 16), ICON_CONFIRM("icons.png", 0, 16, 16, 16), - ICON_NORMAL_ROOF("icons.png", 32, 16, 16, 16), - ICON_FLAT_ROOF("icons.png", 48, 16, 16, 16), + ICON_OPEN_FOLDER("icons.png", 32, 16, 16, 16), + ICON_REFRESH("icons.png", 48, 16, 16, 16), ICON_DONT_REPLACE("icons.png", 0, 32, 16, 16), ICON_REPLACE_SOLID("icons.png", 16, 32, 16, 16), @@ -55,6 +55,7 @@ public enum GuiResources { ICON_TOOL_DEPLOY("icons.png", 0, 48, 16, 16), ICON_SKIP_MISSING("icons.png", 16, 48, 16, 16), + ICON_SKIP_TILES("icons.png", 32, 48, 16, 16), ICON_TOOL_MOVE_XZ("icons.png", 0, 64, 16, 16), ICON_TOOL_MOVE_Y("icons.png", 16, 64, 16, 16), diff --git a/src/main/java/com/simibubi/create/gui/SchematicTableScreen.java b/src/main/java/com/simibubi/create/gui/SchematicTableScreen.java index c5c472b2d..63d91a901 100644 --- a/src/main/java/com/simibubi/create/gui/SchematicTableScreen.java +++ b/src/main/java/com/simibubi/create/gui/SchematicTableScreen.java @@ -1,12 +1,12 @@ package com.simibubi.create.gui; +import java.nio.file.Paths; import java.util.List; import com.mojang.blaze3d.platform.GlStateManager; import com.simibubi.create.AllBlocks; import com.simibubi.create.Create; import com.simibubi.create.block.SchematicTableContainer; -import com.simibubi.create.gui.widgets.AbstractSimiWidget; import com.simibubi.create.gui.widgets.DynamicLabel; import com.simibubi.create.gui.widgets.OptionScrollArea; import com.simibubi.create.gui.widgets.ScrollArea; @@ -14,29 +14,25 @@ import com.simibubi.create.gui.widgets.SimiButton; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.IHasContainer; -import net.minecraft.client.gui.screen.inventory.ContainerScreen; -import net.minecraft.client.gui.widget.Widget; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.renderer.texture.AtlasTexture; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.util.Util; import net.minecraft.util.math.MathHelper; import net.minecraft.util.text.ITextComponent; -public class SchematicTableScreen extends ContainerScreen +public class SchematicTableScreen extends AbstractSimiContainerScreen implements IHasContainer { - private ScrollArea schematics; - private SimiButton button; - private DynamicLabel label; + private ScrollArea schematicsArea; + private SimiButton confirmButton; + private SimiButton folderButton; + private SimiButton refreshButton; + private DynamicLabel schematicsLabel; private float progress; - private float lastProgress; - - private int xTopLeft; - private int yTopLeft; - - private int xMainWindow; - private int yMainWindow; + private float chasingProgress; + private float lastChasingProgress; public SchematicTableScreen(SchematicTableContainer container, PlayerInventory playerInventory, ITextComponent title) { @@ -45,63 +41,71 @@ public class SchematicTableScreen extends ContainerScreen availableSchematics = Create.cSchematicLoader.getAvailableSchematics(); if (!availableSchematics.isEmpty()) { - label = new DynamicLabel(xMainWindow + 36, yMainWindow + 26, "").withShadow(); - schematics = new OptionScrollArea(xMainWindow + 33, yMainWindow + 23, 134, 14) - .forOptions(availableSchematics).titled("Available Schematics").writingTo(label); - buttons.add(schematics); - buttons.add(label); - } else { + schematicsLabel = new DynamicLabel(mainLeft + 36, mainTop + 26, "").withShadow(); + schematicsArea = new OptionScrollArea(mainLeft + 33, mainTop + 23, 134, 14).forOptions(availableSchematics) + .titled("Available Schematics").writingTo(schematicsLabel); + widgets.add(schematicsArea); + widgets.add(schematicsLabel); + } - } - - button = new SimiButton(xMainWindow + 69, yMainWindow + 55, GuiResources.ICON_CONFIRM); - buttons.add(button); + confirmButton = new SimiButton(mainLeft + 69, mainTop + 55, GuiResources.ICON_CONFIRM); + folderButton = new SimiButton(mainLeft + 204, mainTop + 6, GuiResources.ICON_OPEN_FOLDER); + refreshButton = new SimiButton(mainLeft + 204, mainTop + 26, GuiResources.ICON_REFRESH); + widgets.add(confirmButton); + widgets.add(folderButton); + widgets.add(refreshButton); } @Override protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { - int x = xTopLeft; - int y = yTopLeft; - - GuiResources.SCHEMATIC_TABLE.draw(this, xMainWindow, yMainWindow); - GuiResources.PLAYER_INVENTORY.draw(this, x, y + 20); - - if (container.isUploading) - font.drawString("Uploading...", xMainWindow + 76, yMainWindow + 10, GuiResources.FONT_COLOR); - else - font.drawString("Choose a Schematic", xMainWindow + 50, yMainWindow + 10, GuiResources.FONT_COLOR); - font.drawString("Inventory", x + 7, y + 26, 0x666666); - - if (schematics == null) { - font.drawStringWithShadow(" No Schematics Saved ", xMainWindow + 39, yMainWindow + 26, 0xFFDD44); - } + super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); + } @Override - public void render(int mouseX, int mouseY, float pt) { - renderBackground(); - super.render(mouseX, mouseY, pt); + protected void renderWindow(int mouseX, int mouseY, float partialTicks) { + + int x = guiLeft + 20; + int y = guiTop; + + int mainLeft = guiLeft - 56; + int mainTop = guiTop - 16; + GuiResources.PLAYER_INVENTORY.draw(this, x- 16, y + 70 + 14); + font.drawString("Inventory", x - 15 + 7, y + 64 + 26, 0x666666); + + GuiResources.SCHEMATIC_TABLE.draw(this, mainLeft, mainTop); + if (container.getTileEntity().isUploading) + font.drawString("Uploading...", mainLeft + 76, mainTop + 10, GuiResources.FONT_COLOR); + else if (container.getSlot(1).getHasStack()) + font.drawString("Upload Finished!", mainLeft + 60, mainTop + 10, GuiResources.FONT_COLOR); + else + font.drawString("Schematic Table", mainLeft + 60, mainTop + 10, GuiResources.FONT_COLOR); + + if (schematicsArea == null) { + font.drawStringWithShadow(" No Schematics Saved ", mainLeft + 39, mainTop + 26, 0xFFDD44); + } + minecraft.getTextureManager().bindTexture(GuiResources.SCHEMATIC_TABLE_PROGRESS.location); - int width = (int) (GuiResources.SCHEMATIC_TABLE_PROGRESS.width * MathHelper.lerp(pt, lastProgress, progress)); + int width = (int) (GuiResources.SCHEMATIC_TABLE_PROGRESS.width + * MathHelper.lerp(partialTicks, lastChasingProgress, chasingProgress)); int height = GuiResources.SCHEMATIC_TABLE_PROGRESS.height; GlStateManager.disableLighting(); - blit(xMainWindow + 94, yMainWindow + 56, GuiResources.SCHEMATIC_TABLE_PROGRESS.startX, + blit(mainLeft + 94, mainTop + 56, GuiResources.SCHEMATIC_TABLE_PROGRESS.startX, GuiResources.SCHEMATIC_TABLE_PROGRESS.startY, width, height); - GlStateManager.pushLightingAttributes(); GlStateManager.pushMatrix(); GlStateManager.enableBlend(); @@ -112,73 +116,81 @@ public class SchematicTableScreen extends ContainerScreen toolTip = ((AbstractSimiWidget) w).getToolTip(); - renderTooltip(toolTip, mouseX, mouseY); - } - } - } - - @Override - public boolean isPauseScreen() { - return false; } @Override public void tick() { super.tick(); - if (container.isUploading) { - lastProgress = progress; - progress = Create.cSchematicLoader.getProgress(container.schematicUploading); - label.colored(0xCCDDFF); - button.active = false; - schematics.visible = false; + boolean finished = container.getSlot(1).getHasStack(); + + if (container.getTileEntity().isUploading || finished) { + if (finished) { + chasingProgress = lastChasingProgress = progress = 1; + } else { + lastChasingProgress = chasingProgress; + progress = container.getTileEntity().uploadingProgress; + chasingProgress += (progress - chasingProgress) * .5f; + } + confirmButton.active = false; + + if (schematicsLabel != null) { + schematicsLabel.colored(0xCCDDFF); + schematicsLabel.text = container.getTileEntity().uploadingSchematic; + } + if (schematicsArea != null) + schematicsArea.visible = false; } else { progress = 0; - lastProgress = 0; - label.colored(0xFFFFFF); - button.active = true; - schematics.visible = true; + chasingProgress = lastChasingProgress = 0; + confirmButton.active = true; + + if (schematicsLabel != null) + schematicsLabel.colored(0xFFFFFF); + if (schematicsArea != null) { + schematicsArea.writingTo(schematicsLabel); + schematicsArea.visible = true; + } } } @Override public boolean mouseClicked(double p_mouseClicked_1_, double p_mouseClicked_3_, int p_mouseClicked_5_) { - if (button.active && button.isHovered() && ((SchematicTableContainer) container).canWrite() && schematics != null) { - - lastProgress = progress = 0; + if (confirmButton.active && confirmButton.isHovered() && ((SchematicTableContainer) container).canWrite() + && schematicsArea != null) { + + lastChasingProgress = chasingProgress = progress = 0; List availableSchematics = Create.cSchematicLoader.getAvailableSchematics(); - String schematic = availableSchematics.get(schematics.getState()); + String schematic = availableSchematics.get(schematicsArea.getState()); Create.cSchematicLoader.startNewUpload(schematic); } + + if (folderButton.isHovered()) { + Util.getOSType().openFile(Paths.get("schematics/").toFile()); + } + + if (refreshButton.isHovered()) { + Create.cSchematicLoader.refresh(); + List availableSchematics = Create.cSchematicLoader.getAvailableSchematics(); + widgets.remove(schematicsArea); + schematicsArea = new OptionScrollArea(guiLeft - 56 + 33, guiTop - 16 + 23, 134, 14).forOptions(availableSchematics) + .titled("Available Schematics").writingTo(schematicsLabel); + widgets.add(schematicsArea); + } return super.mouseClicked(p_mouseClicked_1_, p_mouseClicked_3_, p_mouseClicked_5_); } - @Override - public boolean mouseScrolled(double p_mouseScrolled_1_, double p_mouseScrolled_3_, double p_mouseScrolled_5_) { - boolean b = false; - for (Widget w : buttons) { - if (w.mouseScrolled(p_mouseScrolled_1_, p_mouseScrolled_3_, p_mouseScrolled_5_)) - b = true; - } - return b || super.mouseScrolled(p_mouseScrolled_1_, p_mouseScrolled_3_, p_mouseScrolled_5_); - } - } diff --git a/src/main/java/com/simibubi/create/gui/SchematicannonScreen.java b/src/main/java/com/simibubi/create/gui/SchematicannonScreen.java index c7cd9c686..fa8f99263 100644 --- a/src/main/java/com/simibubi/create/gui/SchematicannonScreen.java +++ b/src/main/java/com/simibubi/create/gui/SchematicannonScreen.java @@ -30,6 +30,8 @@ public class SchematicannonScreen extends AbstractSimiContainerScreen tip = skipTilesButton.getToolTip(); + tip.remove(1); + tip.add(TextFormatting.BLUE + + (skipTilesIndicator.state == State.ON ? "Currently Enabled" : "Currently Disabled")); + tip.add(TextFormatting.GRAY + "The Schematicannon will avoid replacing"); + tip.add(TextFormatting.GRAY + "data holding blocks such as Chests."); + } if (replaceLevelButtons.get(0).isHovered()) { List tip = replaceLevelButtons.get(0).getToolTip(); tip.remove(1); @@ -199,7 +215,7 @@ public class SchematicannonScreen extends AbstractSimiContainerScreen () -> { - GuiOpener.open(new BlueprintEditScreen()); + displayBlueprintScreen(); }); return ActionResultType.SUCCESS; } @@ -116,11 +117,16 @@ public class ItemBlueprint extends Item { return super.onItemUse(context); } + @OnlyIn(value = Dist.CLIENT) + protected void displayBlueprintScreen() { + GuiOpener.open(new BlueprintEditScreen()); + } + @Override public ActionResult onItemRightClick(World worldIn, PlayerEntity playerIn, Hand handIn) { if (playerIn.isSneaking() && handIn == Hand.MAIN_HAND) { DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> { - GuiOpener.open(new BlueprintEditScreen()); + displayBlueprintScreen(); }); return new ActionResult(ActionResultType.SUCCESS, playerIn.getHeldItem(handIn)); } diff --git a/src/main/java/com/simibubi/create/networking/PacketConfigureSchematicannon.java b/src/main/java/com/simibubi/create/networking/PacketConfigureSchematicannon.java index 1388ab1ab..aa5590e9a 100644 --- a/src/main/java/com/simibubi/create/networking/PacketConfigureSchematicannon.java +++ b/src/main/java/com/simibubi/create/networking/PacketConfigureSchematicannon.java @@ -15,7 +15,7 @@ import net.minecraftforge.fml.network.NetworkEvent.Context; public class PacketConfigureSchematicannon { public static enum Option { - DONT_REPLACE, REPLACE_SOLID, REPLACE_ANY, REPLACE_EMPTY, SKIP_MISSING, PLAY, PAUSE, STOP; + DONT_REPLACE, REPLACE_SOLID, REPLACE_ANY, REPLACE_EMPTY, SKIP_MISSING, SKIP_TILES, PLAY, PAUSE, STOP; } private Option option; @@ -66,6 +66,9 @@ public class PacketConfigureSchematicannon { case SKIP_MISSING: te.skipMissing = set; break; + case SKIP_TILES: + te.replaceTileEntities = set; + break; case PLAY: te.state = State.RUNNING; diff --git a/src/main/java/com/simibubi/create/networking/PacketSchematicTableContainer.java b/src/main/java/com/simibubi/create/networking/PacketSchematicTableContainer.java deleted file mode 100644 index 5a5605be4..000000000 --- a/src/main/java/com/simibubi/create/networking/PacketSchematicTableContainer.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.simibubi.create.networking; - -import java.util.function.Supplier; - -import com.simibubi.create.block.SchematicTableContainer; - -import net.minecraft.client.Minecraft; -import net.minecraft.inventory.container.Container; -import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.network.NetworkEvent.Context; - -public class PacketSchematicTableContainer { - - public String schematic; - public float progress; - - public PacketSchematicTableContainer(String schematicToUpload, float progress) { - this.schematic = schematicToUpload; - if (this.schematic == null) - this.schematic = ""; - this.progress = progress; - } - - public PacketSchematicTableContainer(PacketBuffer buffer) { - this.schematic = buffer.readString(); - this.progress = buffer.readFloat(); - } - - public void toBytes(PacketBuffer buffer) { - buffer.writeString(schematic); - buffer.writeFloat(progress); - } - - public void handle(Supplier context) { - context.get().enqueueWork(() -> { - Container c = Minecraft.getInstance().player.openContainer; - if (c != null && c instanceof SchematicTableContainer) { - ((SchematicTableContainer) c).receiveSchematicInfo(schematic, progress); - } - }); - - } - -} diff --git a/src/main/java/com/simibubi/create/networking/PacketSchematicUpload.java b/src/main/java/com/simibubi/create/networking/PacketSchematicUpload.java index 71c8a1f10..19fc3c109 100644 --- a/src/main/java/com/simibubi/create/networking/PacketSchematicUpload.java +++ b/src/main/java/com/simibubi/create/networking/PacketSchematicUpload.java @@ -18,6 +18,7 @@ public class PacketSchematicUpload { public static final int FINISH = 2; private int code; + private long size; private String schematic; private byte[] data; @@ -26,8 +27,9 @@ public class PacketSchematicUpload { this.schematic = schematic; } - public static PacketSchematicUpload begin(String schematic) { + public static PacketSchematicUpload begin(String schematic, long size) { PacketSchematicUpload pkt = new PacketSchematicUpload(BEGIN, schematic); + pkt.size = size; return pkt; } @@ -45,6 +47,8 @@ public class PacketSchematicUpload { code = buffer.readInt(); schematic = buffer.readString(256); + if (code == BEGIN) + size = buffer.readLong(); if (code == WRITE) data = buffer.readByteArray(); } @@ -53,6 +57,8 @@ public class PacketSchematicUpload { buffer.writeInt(code); buffer.writeString(schematic); + if (code == BEGIN) + buffer.writeLong(size); if (code == WRITE) buffer.writeByteArray(data); } @@ -62,7 +68,7 @@ public class PacketSchematicUpload { ServerPlayerEntity player = context.get().getSender(); if (code == BEGIN) { BlockPos pos = ((SchematicTableContainer) player.openContainer).getTileEntity().getPos(); - Create.sSchematicLoader.handleNewUpload(player, schematic, new DimensionPos(player, pos)); + Create.sSchematicLoader.handleNewUpload(player, schematic, size, new DimensionPos(player, pos)); } if (code == WRITE) { Create.sSchematicLoader.handleWriteRequest(player, schematic, data); diff --git a/src/main/java/com/simibubi/create/networking/Packets.java b/src/main/java/com/simibubi/create/networking/Packets.java index 8bae2a1bb..2fd1055ee 100644 --- a/src/main/java/com/simibubi/create/networking/Packets.java +++ b/src/main/java/com/simibubi/create/networking/Packets.java @@ -21,8 +21,6 @@ public class Packets { channel.registerMessage(i++, PacketNbt.class, PacketNbt::toBytes, PacketNbt::new, PacketNbt::handle); channel.registerMessage(i++, PacketConfigureSchematicannon.class, PacketConfigureSchematicannon::toBytes, PacketConfigureSchematicannon::new, PacketConfigureSchematicannon::handle); - channel.registerMessage(i++, PacketSchematicTableContainer.class, PacketSchematicTableContainer::toBytes, - PacketSchematicTableContainer::new, PacketSchematicTableContainer::handle); channel.registerMessage(i++, PacketSchematicUpload.class, PacketSchematicUpload::toBytes, PacketSchematicUpload::new, PacketSchematicUpload::handle); channel.registerMessage(i++, PacketSymmetryEffect.class, PacketSymmetryEffect::toBytes, diff --git a/src/main/java/com/simibubi/create/schematic/BlueprintHandler.java b/src/main/java/com/simibubi/create/schematic/BlueprintHandler.java index c1c6508d8..2bfc1190e 100644 --- a/src/main/java/com/simibubi/create/schematic/BlueprintHandler.java +++ b/src/main/java/com/simibubi/create/schematic/BlueprintHandler.java @@ -29,6 +29,7 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.StringTextComponent; import net.minecraft.world.gen.feature.template.PlacementSettings; import net.minecraft.world.gen.feature.template.Template; +import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.InputEvent.KeyInputEvent; import net.minecraftforge.client.event.InputEvent.MouseInputEvent; import net.minecraftforge.client.event.RenderGameOverlayEvent; @@ -38,7 +39,7 @@ import net.minecraftforge.fml.common.Mod.EventBusSubscriber; import net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus; import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent; -@EventBusSubscriber(bus = Bus.FORGE) +@EventBusSubscriber(value = Dist.CLIENT, bus = Bus.FORGE) public class BlueprintHandler { public static BlueprintHandler instance; @@ -227,11 +228,15 @@ public class BlueprintHandler { Packets.channel.sendToServer(new PacketNbt(item, slot)); if (deployed) { + Template schematic = ItemBlueprint.getSchematic(item); + + if (schematic.getSize().equals(BlockPos.ZERO)) + return; + SchematicWorld w = new SchematicWorld(new HashMap<>(), new Cuboid(), anchor); PlacementSettings settings = cachedSettings.copy(); settings.setBoundingBox(null); - ItemBlueprint.getSchematic(item).addBlocksToWorld(w, anchor, settings); - + schematic.addBlocksToWorld(w, anchor, settings); new SchematicHologram().startHologram(w); } } diff --git a/src/main/java/com/simibubi/create/schematic/MaterialChecklist.java b/src/main/java/com/simibubi/create/schematic/MaterialChecklist.java new file mode 100644 index 000000000..084d2f486 --- /dev/null +++ b/src/main/java/com/simibubi/create/schematic/MaterialChecklist.java @@ -0,0 +1,135 @@ +package com.simibubi.create.schematic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.StringNBT; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextFormatting; +import net.minecraft.util.text.TranslationTextComponent; + +public class MaterialChecklist { + + public Map gathered; + public Map required; + public boolean blocksNotLoaded; + + public MaterialChecklist() { + required = new HashMap<>(); + gathered = new HashMap<>(); + } + + public void warnBlockNotLoaded() { + blocksNotLoaded = true; + } + + public void require(Item item) { + if (required.containsKey(item)) + required.put(item, required.get(item) + 1); + else + required.put(item, 1); + } + + public void collect(ItemStack stack) { + Item item = stack.getItem(); + if (required.containsKey(item)) + if (gathered.containsKey(item)) + gathered.put(item, gathered.get(item) + stack.getCount()); + else + gathered.put(item, stack.getCount()); + } + + public ItemStack createItem() { + ItemStack book = new ItemStack(Items.WRITTEN_BOOK); + + CompoundNBT tag = book.getOrCreateTag(); + ListNBT pages = new ListNBT(); + + int itemsWritten = 0; + StringBuilder string = new StringBuilder("{\"text\":\""); + + if (blocksNotLoaded) { + string.append("\n" + TextFormatting.RED + "* Disclaimer *\n\n"); + string.append("Material List may be inaccurate due to relevant chunks not being loaded."); + string.append("\"}"); + pages.add(new StringNBT(string.toString())); + string = new StringBuilder("{\"text\":\""); + } + + List keys = new ArrayList<>(required.keySet()); + Collections.sort(keys, (item1, item2) -> { + String name1 = new TranslationTextComponent(((Item) item1).getTranslationKey()).getFormattedText().toLowerCase(); + String name2 = new TranslationTextComponent(((Item) item2).getTranslationKey()).getFormattedText().toLowerCase(); + return name1.compareTo(name2); + }); + + List completed = new ArrayList<>(); + for (Item item : keys) { + int amount = required.get(item); + if (gathered.containsKey(item)) + amount -= gathered.get(item); + + if (amount <= 0) { + completed.add(item); + continue; + } + + if (itemsWritten == 6) { + itemsWritten = 0; + string.append("\"}"); + pages.add(new StringNBT(string.toString())); + string = new StringBuilder("{\"text\":\""); + } + + itemsWritten++; + string.append(unfinishedEntry(new ItemStack(item), amount)); + } + + for (Item item : completed) { + if (itemsWritten == 6) { + itemsWritten = 0; + string.append("\"}"); + pages.add(new StringNBT(string.toString())); + string = new StringBuilder("{\"text\":\""); + } + + itemsWritten++; + string.append(gatheredEntry(new ItemStack(item), required.get(item))); + } + + string.append("\"}"); + pages.add(new StringNBT(string.toString())); + + tag.put("pages", pages); + tag.putString("author", "Schematicannon"); + tag.putString("title", TextFormatting.BLUE + "Material Checklist"); + book.setTag(tag); + + return book; + } + + private String gatheredEntry(ItemStack item, int amount) { + int stacks = amount / 64; + int remainder = amount % 64; + ITextComponent tc = new TranslationTextComponent(item.getTranslationKey()); + return TextFormatting.DARK_GREEN + tc.getFormattedText() + + " \\u2714\n x" + amount + TextFormatting.GRAY + " | " + stacks + "\\u25A4 +" + remainder + "\n"; + } + + private String unfinishedEntry(ItemStack item, int amount) { + int stacks = amount / 64; + int remainder = amount % 64; + ITextComponent tc = new TranslationTextComponent(item.getTranslationKey()); + return TextFormatting.BLUE + tc.getFormattedText() + "\n x" + amount + + TextFormatting.GRAY + " | " + stacks + "\\u25A4 +" + remainder + "\n"; + } + +} diff --git a/src/main/resources/assets/create/textures/gui/icons.png b/src/main/resources/assets/create/textures/gui/icons.png index 9072b95f40be0120cc8007e4d55e8270f3b6ad09..d2275b6d02dbe8245d7d85740250a7bb232580b7 100644 GIT binary patch delta 782 zcmV+p1M&Ro5Q!16R0ImS000iT0k{Sizmr}BCx7orL_t(|UhS64mg^`G1bhDfqx%9C zAduUV>{(Hnacn0A@}dI0etyNV0P@6t1%`ifv%C*oz^4tR7Erf87!!QpT<^?x0+!gD zZ!du~Q~_*vA?99pe5u=U@f}C(iv^HNFQtf&0FMN`)JyEmcLGlJ&U||bfrJ}+tBU- z*ZtS^KIj=xZB5(+Z+oHRaoAF9O$ZRi0)I$ZEP#|{2&nh;<-UHt(Vy!i9B07sy3=U= zPBV4+rDnV{(&L9&(QISCZKKgw4t@01-z}J+?qH8N+(N0z3vn5G6B;iuu^t; z^k1oaepT;EzXaB1m+HRWnJS>RKezO0y){ih{mu>6?+5kPSU~ySHF#HV&A10d*?;)7 z-kK^v8Vew0u>evQ3m_km=-D@cfc9uFL^W!v;9TdR3y7cIVdUsHWC#fu{V#QoT>L~^ z{6s{fvFQx^T0Y7dFuFUM>?7(D@kvO%1lk7IJbGN4jVi4GZg34|Cl!_C0`6JuQl z%vb;^iv^IfJQk4i8Sp;@WD)X#fPeK(AeDf4&*}Y9EFCVdhHWjEfcT-OqZ$)2?owK? zwmnw@UK;LoEn>JoMF5?*mSaHG>8JcOHxAyOpSrEBmDW=oP1Vxl+Hk4(XS$HfUD;7Y?VgaNq2*~h6|5lDJV0p~7 zH=OA;r@j_dC{uBq0aJUt2Ok2K$i)ya8)%z7?Ree&RZ$)e0rQS)3RoJJ6cEEg8&4B$ zRp1gBv|^g6tN8`QpwX&ns*7?j`Nf2ShJP9^f}<^TWy M07*qoM6N<$f?U{lbpQYW delta 751 zcmVDUHUR_q zfq&Rw{EM{(C}9DlEEYh@@}_`?XTrn`0&Yv@Lvc+5y@1<}vECVo!-9pG=>*&ss`aCA z92GTB%^~2nPv5(aR-y^0TA#G5KUn6!!L0tW|GDsao|(7>;@W<>|Emi~13BHWyntpP z&9k!I0%O@> zdY)${1VsJ5(x>y(q=0z!1>^O>JT(^3UQY#|=Be2~18Ph@ou?)QNMiw{EEYh@Vt)ao zEC|R)d!wd(W?Gp|K>gYpS1R=XTfp95K*3LyC}%^|LUyw(d=I~A+LhoCMNK+0kPq%0OdKCC$3@jWS6NZYONSh!96;oQ8u0=9n5#7Rb-JFm^in@@o&?eyb&W*`N;!@27M-IK2ctisy@ zR>cBHSuB8*1pzBO(Z6F8Fg)h+8&3B%C%zU1j7xD`0bM=52Ok24Sc_diZ#N(}yW8>D z{ZY|690K|s*Ay@mmK0EjjXdra@~pruu#-c#5@**JPzOm)-BQ#K7?)xfP^kw3bd#|M hz?0AiTNsqz-~X*w@Br(gb-e%p002ovPDHLkV1hKGZi@f_ diff --git a/src/main/resources/assets/create/textures/gui/schematicannon.png b/src/main/resources/assets/create/textures/gui/schematicannon.png index 3ec65b3d02b2f247a0ca4dcadcbe9bc52833734e..2417f1f2a7f0632cb76d8045d5322b858653fb85 100644 GIT binary patch literal 4376 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 literal 4327 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