From 598c166700f096c195ea23acaf190f84c38c49e9 Mon Sep 17 00:00:00 2001 From: Irtimaled Date: Thu, 30 Apr 2020 22:37:29 -0700 Subject: [PATCH] Add logic to load structures from save files --- .../irtimaled/bbor/client/ClientProxy.java | 8 ++ .../client/commands/StructuresCommand.java | 30 ++++ .../bbor/client/events/SaveLoaded.java | 6 + .../bbor/client/gui/LoadSavesScreen.java | 40 ++++++ .../bbor/client/gui/WorldSaveRow.java | 131 ++++++++++++++++++ .../bbor/client/interop/ClientInterop.java | 23 +++ .../client/interop/NBTStructureLoader.java | 126 +++++++++++++++++ .../interop/SaveGameStructureLoader.java | 68 +++++++++ .../network/MixinNetHandlerPlayClient.java | 6 + 9 files changed, 438 insertions(+) create mode 100644 src/main/java/com/irtimaled/bbor/client/commands/StructuresCommand.java create mode 100644 src/main/java/com/irtimaled/bbor/client/events/SaveLoaded.java create mode 100644 src/main/java/com/irtimaled/bbor/client/gui/LoadSavesScreen.java create mode 100644 src/main/java/com/irtimaled/bbor/client/gui/WorldSaveRow.java create mode 100644 src/main/java/com/irtimaled/bbor/client/interop/NBTStructureLoader.java create mode 100644 src/main/java/com/irtimaled/bbor/client/interop/SaveGameStructureLoader.java diff --git a/src/main/java/com/irtimaled/bbor/client/ClientProxy.java b/src/main/java/com/irtimaled/bbor/client/ClientProxy.java index 3974c42..8d527ae 100644 --- a/src/main/java/com/irtimaled/bbor/client/ClientProxy.java +++ b/src/main/java/com/irtimaled/bbor/client/ClientProxy.java @@ -1,6 +1,7 @@ package com.irtimaled.bbor.client; import com.irtimaled.bbor.client.events.*; +import com.irtimaled.bbor.client.gui.LoadSavesScreen; import com.irtimaled.bbor.client.gui.SettingsScreen; import com.irtimaled.bbor.client.keyboard.Key; import com.irtimaled.bbor.client.keyboard.KeyListener; @@ -19,6 +20,8 @@ public class ClientProxy extends CommonProxy { .onKeyPressHandler(SettingsScreen::show); mainKey.register("key.keyboard.o") .onKeyPressHandler(() -> ConfigManager.Toggle(ConfigManager.outerBoxesOnly)); + mainKey.register("key.keyboard.l") + .onKeyPressHandler(LoadSavesScreen::show); } @Override @@ -29,6 +32,7 @@ public class ClientProxy extends CommonProxy { EventBus.subscribe(AddBoundingBoxReceived.class, this::addBoundingBox); EventBus.subscribe(RemoveBoundingBoxReceived.class, this::onRemoveBoundingBoxReceived); EventBus.subscribe(UpdateWorldSpawnReceived.class, this::onUpdateWorldSpawnReceived); + EventBus.subscribe(SaveLoaded.class, e -> clear()); ClientRenderer.registerProvider(new CacheProvider(this::getCache)); @@ -38,6 +42,10 @@ public class ClientProxy extends CommonProxy { private void disconnectedFromServer() { ClientRenderer.deactivate(); if (ConfigManager.keepCacheBetweenSessions.get()) return; + clear(); + } + + private void clear() { SlimeChunkProvider.clear(); WorldSpawnProvider.clear(); SpawningSphereProvider.clear(); diff --git a/src/main/java/com/irtimaled/bbor/client/commands/StructuresCommand.java b/src/main/java/com/irtimaled/bbor/client/commands/StructuresCommand.java new file mode 100644 index 0000000..b0ef080 --- /dev/null +++ b/src/main/java/com/irtimaled/bbor/client/commands/StructuresCommand.java @@ -0,0 +1,30 @@ +package com.irtimaled.bbor.client.commands; + +import com.irtimaled.bbor.client.gui.LoadSavesScreen; +import com.irtimaled.bbor.client.interop.ClientInterop; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.command.Commands; +import net.minecraft.command.ISuggestionProvider; + +public class StructuresCommand { + private static final String COMMAND = "bbor:structures"; + private static final String LOAD = "load"; + private static final String CLEAR = "clear"; + + public static void register(CommandDispatcher commandDispatcher) { + LiteralArgumentBuilder command = Commands.literal(COMMAND) + .then(Commands.literal(LOAD) + .executes(context -> { + LoadSavesScreen.show(); + return 0; + })) + .then(Commands.literal(CLEAR) + .executes(context -> { + ClientInterop.clearStructures(); + return 0; + })); + + commandDispatcher.register(command); + } +} diff --git a/src/main/java/com/irtimaled/bbor/client/events/SaveLoaded.java b/src/main/java/com/irtimaled/bbor/client/events/SaveLoaded.java new file mode 100644 index 0000000..f40380b --- /dev/null +++ b/src/main/java/com/irtimaled/bbor/client/events/SaveLoaded.java @@ -0,0 +1,6 @@ +package com.irtimaled.bbor.client.events; + +public class SaveLoaded { + public SaveLoaded() { + } +} diff --git a/src/main/java/com/irtimaled/bbor/client/gui/LoadSavesScreen.java b/src/main/java/com/irtimaled/bbor/client/gui/LoadSavesScreen.java new file mode 100644 index 0000000..9d941e1 --- /dev/null +++ b/src/main/java/com/irtimaled/bbor/client/gui/LoadSavesScreen.java @@ -0,0 +1,40 @@ +package com.irtimaled.bbor.client.gui; + +import net.minecraft.client.AnvilConverterException; +import net.minecraft.client.Minecraft; +import net.minecraft.world.storage.ISaveFormat; +import net.minecraft.world.storage.WorldSummary; + +import java.util.List; + +public class LoadSavesScreen extends ListScreen { + public static void show() { + Minecraft.getInstance().displayGuiScreen(new LoadSavesScreen()); + } + + @Override + protected void setup() { + ControlList controlList = this.getControlList(); + controlList.showSelectionBox(); + try { + final ISaveFormat saveLoader = this.mc.getSaveLoader(); + List saveList = saveLoader.getSaveList(); + saveList.sort(null); + saveList.forEach(world -> controlList.add(new WorldSaveRow(world, saveLoader))); + } catch (AnvilConverterException e) { + e.printStackTrace(); + } + } + + @Override + protected void onDoneClicked() { + ((WorldSaveRow) this.getControlList().getSelectedEntry()).loadWorld(); + } + + @Override + public void render(int mouseX, int mouseY, float unknown) { + ControlListEntry selectedEntry = this.getControlList().getSelectedEntry(); + this.getDoneButton().enabled = selectedEntry != null && selectedEntry.getVisible(); + super.render(mouseX, mouseY, unknown); + } +} diff --git a/src/main/java/com/irtimaled/bbor/client/gui/WorldSaveRow.java b/src/main/java/com/irtimaled/bbor/client/gui/WorldSaveRow.java new file mode 100644 index 0000000..5f26c74 --- /dev/null +++ b/src/main/java/com/irtimaled/bbor/client/gui/WorldSaveRow.java @@ -0,0 +1,131 @@ +package com.irtimaled.bbor.client.gui; + +import com.google.common.hash.Hashing; +import com.irtimaled.bbor.client.interop.ClientInterop; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.NativeImage; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.Util; +import net.minecraft.world.storage.ISaveFormat; +import net.minecraft.world.storage.WorldInfo; +import net.minecraft.world.storage.WorldSummary; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class WorldSaveRow extends ControlListEntry implements Comparable { + private static final Logger LOGGER = LogManager.getLogger(); + private static final DateFormat DATE_FORMAT = new SimpleDateFormat(); + private static final ResourceLocation ICON_MISSING = new ResourceLocation("textures/misc/unknown_server.png"); + private static final int ICON_SIZE = 20; + private final Minecraft client; + private final WorldSummary worldSummary; + private final ISaveFormat saveLoader; + private final ResourceLocation iconLocation; + private final DynamicTexture icon; + + private File iconFile; + private long lastClickTime; + + WorldSaveRow(WorldSummary worldSummary, ISaveFormat saveLoader) { + this.worldSummary = worldSummary; + this.saveLoader = saveLoader; + this.client = Minecraft.getInstance(); + this.iconLocation = new ResourceLocation("worlds/" + Hashing.sha1().hashUnencodedChars(worldSummary.getFileName()) + "/icon"); + this.iconFile = saveLoader.getFile(worldSummary.getFileName(), "icon.png"); + if (!this.iconFile.isFile()) { + this.iconFile = null; + } + + this.icon = this.loadIcon(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + this.list.setSelectedIndex(this.index); + if (Util.milliTime() - this.lastClickTime < 250L) { + loadWorld(); + return true; + } else { + this.lastClickTime = Util.milliTime(); + return false; + } + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return false; + } + + void loadWorld() { + String fileName = this.worldSummary.getFileName(); + WorldInfo worldInfo = saveLoader.getWorldInfo(fileName); + long seed = worldInfo.getSeed(); + ClientInterop.saveLoaded(fileName, seed); + } + + private DynamicTexture loadIcon() { + if (this.iconFile == null || !this.iconFile.isFile()) { + this.client.getTextureManager().deleteTexture(this.iconLocation); + return null; + } + + try (InputStream stream = new FileInputStream(this.iconFile)) { + DynamicTexture texture = new DynamicTexture(NativeImage.read(stream)); + this.client.getTextureManager().loadTexture(this.iconLocation, texture); + return texture; + } catch (Throwable exception) { + LOGGER.error("Invalid icon for world {}", this.worldSummary.getFileName(), exception); + this.iconFile = null; + return null; + } + } + + @Override + public void render(int mouseX, int mouseY) { + String displayName = this.worldSummary.getDisplayName(); + String details = this.worldSummary.getFileName() + " (" + DATE_FORMAT.format(new Date(this.worldSummary.getLastTimePlayed())) + ")"; + + int x = this.getX(); + int y = this.getY(); + this.client.fontRenderer.drawString(displayName, (float) (x + ICON_SIZE + 3), (float) (y + 1), 16777215); + this.client.fontRenderer.drawString(details, (float) (x + ICON_SIZE + 3), (float) (y + 1 + this.client.fontRenderer.FONT_HEIGHT + 1), 8421504); + this.client.getTextureManager().bindTexture(this.icon != null ? this.iconLocation : ICON_MISSING); + GL11.glEnable(GL11.GL_BLEND); + Gui.drawModalRectWithCustomSizedTexture(x, y, 0.0F, 0.0F, ICON_SIZE, ICON_SIZE, 32.0F, 32.0F); + GL11.glDisable(GL11.GL_BLEND); + } + + @Override + public int getControlWidth() { + return 310; + } + + @Override + public void filter(String lowerValue) { + setVisible(lowerValue == "" || + this.worldSummary.getDisplayName().toLowerCase().contains(lowerValue) || + this.worldSummary.getFileName().toLowerCase().contains(lowerValue)); + } + + @Override + public void close() { + if (this.icon != null) { + this.icon.close(); + } + } + + @Override + public int compareTo(WorldSaveRow other) { + return this.worldSummary.compareTo(other.worldSummary); + } +} diff --git a/src/main/java/com/irtimaled/bbor/client/interop/ClientInterop.java b/src/main/java/com/irtimaled/bbor/client/interop/ClientInterop.java index d540dd2..e1d0fdf 100644 --- a/src/main/java/com/irtimaled/bbor/client/interop/ClientInterop.java +++ b/src/main/java/com/irtimaled/bbor/client/interop/ClientInterop.java @@ -4,6 +4,7 @@ import com.irtimaled.bbor.client.ClientRenderer; import com.irtimaled.bbor.client.Player; import com.irtimaled.bbor.client.commands.*; import com.irtimaled.bbor.client.events.DisconnectedFromRemoteServer; +import com.irtimaled.bbor.client.events.SaveLoaded; import com.irtimaled.bbor.client.events.UpdateWorldSpawnReceived; import com.irtimaled.bbor.client.providers.SlimeChunkProvider; import com.irtimaled.bbor.common.EventBus; @@ -21,6 +22,7 @@ import net.minecraft.util.text.event.ClickEvent; public class ClientInterop { public static void disconnectedFromRemoteServer() { + SaveGameStructureLoader.clear(); EventBus.publish(new DisconnectedFromRemoteServer()); } @@ -91,5 +93,26 @@ public class ClientInterop { BoxCommand.register(commandDispatcher); BeaconCommand.register(commandDispatcher); ConfigCommand.register(commandDispatcher); + StructuresCommand.register(commandDispatcher); + } + + public static void receivedChunk(int chunkX, int chunkZ) { + SaveGameStructureLoader.loadStructures(chunkX, chunkZ); + } + + public static void saveLoaded(String fileName, long seed) { + Minecraft minecraft = Minecraft.getInstance(); + minecraft.displayGuiScreen(null); + minecraft.mouseHelper.grabMouse(); + + clearStructures(); + + SlimeChunkProvider.setSeed(seed); + SaveGameStructureLoader.loadSaveGame(fileName); + } + + public static void clearStructures() { + EventBus.publish(new SaveLoaded()); + SaveGameStructureLoader.clear(); } } diff --git a/src/main/java/com/irtimaled/bbor/client/interop/NBTStructureLoader.java b/src/main/java/com/irtimaled/bbor/client/interop/NBTStructureLoader.java new file mode 100644 index 0000000..c15b2e1 --- /dev/null +++ b/src/main/java/com/irtimaled/bbor/client/interop/NBTStructureLoader.java @@ -0,0 +1,126 @@ +package com.irtimaled.bbor.client.interop; + +import com.irtimaled.bbor.common.EventBus; +import com.irtimaled.bbor.common.events.StructuresLoaded; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.MutableBoundingBox; +import net.minecraft.world.IWorld; +import net.minecraft.world.chunk.storage.RegionFileCache; +import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.gen.feature.structure.LegacyStructureDataUtil; +import net.minecraft.world.gen.feature.structure.StructurePiece; +import net.minecraft.world.gen.feature.structure.StructureStart; +import net.minecraft.world.gen.feature.template.TemplateManager; +import net.minecraft.world.storage.ISaveHandler; +import net.minecraft.world.storage.WorldSavedDataStorage; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.*; + +class NBTStructureLoader { + private final int dimensionId; + private final Set loadedChunks = new HashSet<>(); + + private LegacyStructureDataUtil legacyStructureDataUtil = null; + private ISaveHandler saveHandler = null; + private File chunkSaveLocation = null; + + NBTStructureLoader(int dimensionId, ISaveHandler saveHandler, File worldDirectory) { + this.dimensionId = dimensionId; + this.configure(saveHandler, worldDirectory); + } + + void clear() { + this.saveHandler = null; + this.chunkSaveLocation = null; + this.loadedChunks.clear(); + } + + void configure(ISaveHandler saveHandler, File worldDirectory) { + this.saveHandler = saveHandler; + if(worldDirectory != null) { + this.chunkSaveLocation = DimensionType.getById(dimensionId).getDirectory(worldDirectory); + } + } + + private LegacyStructureDataUtil getLegacyStructureDataUtil() { + if (this.legacyStructureDataUtil == null) { + this.legacyStructureDataUtil = LegacyStructureDataUtil.func_212183_a(DimensionType.getById(dimensionId), new WorldSavedDataStorage(saveHandler)); + } + return this.legacyStructureDataUtil; + } + + private NBTTagCompound loadStructureStarts(int chunkX, int chunkZ) { + try { + DataInputStream stream = RegionFileCache.getChunkInputStream(chunkSaveLocation, chunkX, chunkZ); + if (stream != null) { + NBTTagCompound compound = CompressedStreamTools.read(stream); + stream.close(); + int dataVersion = compound.contains("DataVersion", 99) ? compound.getInt("DataVersion") : -1; + if (dataVersion < 1493) { + if (compound.getCompound("Level").getBoolean("hasLegacyStructureData")) { + compound = getLegacyStructureDataUtil().func_212181_a(compound); + } + } + return compound.getCompound("Level").getCompound("Structures").getCompound("Starts"); + } + } catch (IOException ignored) { + } + return null; + } + + void loadStructures(int chunkX, int chunkZ) { + if (saveHandler == null) return; + + if (!loadedChunks.add(String.format("%s,%s", chunkX, chunkZ))) return; + + NBTTagCompound structureStarts = loadStructureStarts(chunkX, chunkZ); + if (structureStarts == null || structureStarts.size() == 0) return; + + Map structureStartMap = new HashMap<>(); + for (String key : structureStarts.keySet()) { + NBTTagCompound compound = structureStarts.getCompound(key); + if (compound.contains("BB")) { + structureStartMap.put(key, new SimpleStructureStart(compound)); + } + } + + EventBus.publish(new StructuresLoaded(structureStartMap, dimensionId)); + } + + private static class SimpleStructureStart extends StructureStart { + SimpleStructureStart(NBTTagCompound compound) { + this.boundingBox = new MutableBoundingBox(compound.getIntArray("BB")); + + NBTTagList children = compound.getList("Children", 10); + for (int index = 0; index < children.size(); ++index) { + NBTTagCompound child = children.getCompound(index); + if (child.contains("BB")) this.components.add(new SimpleStructurePiece(child)); + } + } + } + + private static class SimpleStructurePiece extends StructurePiece { + SimpleStructurePiece(NBTTagCompound compound) { + this.boundingBox = new MutableBoundingBox(compound.getIntArray("BB")); + } + + @Override + protected void writeAdditional(NBTTagCompound nbtTagCompound) { + } + + @Override + protected void readAdditional(NBTTagCompound nbtTagCompound, TemplateManager templateManager) { + } + + @Override + public boolean addComponentParts(IWorld iWorld, Random random, MutableBoundingBox mutableBoundingBox, ChunkPos chunkPos) { + return false; + } + } +} diff --git a/src/main/java/com/irtimaled/bbor/client/interop/SaveGameStructureLoader.java b/src/main/java/com/irtimaled/bbor/client/interop/SaveGameStructureLoader.java new file mode 100644 index 0000000..3802936 --- /dev/null +++ b/src/main/java/com/irtimaled/bbor/client/interop/SaveGameStructureLoader.java @@ -0,0 +1,68 @@ +package com.irtimaled.bbor.client.interop; + +import com.irtimaled.bbor.client.Player; +import net.minecraft.client.Minecraft; +import net.minecraft.world.chunk.storage.RegionFileCache; +import net.minecraft.world.storage.ISaveFormat; +import net.minecraft.world.storage.ISaveHandler; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class SaveGameStructureLoader { + private static final Map nbtStructureLoaders = new HashMap<>(); + private static ISaveHandler saveHandler = null; + private static File worldDirectory = null; + + static void loadSaveGame(String fileName) { + Minecraft minecraft = Minecraft.getInstance(); + ISaveFormat saveLoader = minecraft.getSaveLoader(); + saveHandler = saveLoader.getSaveLoader(fileName, null); + worldDirectory = saveLoader.getWorldFolder(fileName).toFile(); + + for (int dimensionId : nbtStructureLoaders.keySet()) { + NBTStructureLoader dimensionProcessor = getNBTStructureLoader(dimensionId); + dimensionProcessor.configure(saveHandler, worldDirectory); + } + + loadChunksAroundPlayer(); + } + + private static void loadChunksAroundPlayer() { + NBTStructureLoader dimensionProcessor = getNBTStructureLoader(Player.getDimensionId()); + int renderDistance = ClientInterop.getRenderDistanceChunks(); + + int playerChunkX = (int) Player.getX() >> 4; + int minChunkX = playerChunkX - renderDistance; + int maxChunkX = playerChunkX + renderDistance; + + int playerChunkZ = (int) Player.getZ() >> 4; + int minChunkZ = playerChunkZ - renderDistance; + int maxChunkZ = playerChunkZ + renderDistance; + + for (int chunkX = minChunkX; chunkX < maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ < maxChunkZ; chunkZ++) { + dimensionProcessor.loadStructures(chunkX, chunkZ); + } + } + } + + static void loadStructures(int chunkX, int chunkZ) { + NBTStructureLoader dimensionProcessor = getNBTStructureLoader(Player.getDimensionId()); + dimensionProcessor.loadStructures(chunkX, chunkZ); + } + + private static NBTStructureLoader getNBTStructureLoader(int dimensionId) { + return nbtStructureLoaders.computeIfAbsent(dimensionId, + id -> new NBTStructureLoader(id, saveHandler, worldDirectory)); + } + + public static void clear() { + nbtStructureLoaders.values().forEach(NBTStructureLoader::clear); + nbtStructureLoaders.clear(); + saveHandler = null; + worldDirectory = null; + RegionFileCache.clearRegionFileReferences(); + } +} diff --git a/src/main/java/com/irtimaled/bbor/mixin/client/network/MixinNetHandlerPlayClient.java b/src/main/java/com/irtimaled/bbor/mixin/client/network/MixinNetHandlerPlayClient.java index 6f8e1a0..e1a66bc 100644 --- a/src/main/java/com/irtimaled/bbor/mixin/client/network/MixinNetHandlerPlayClient.java +++ b/src/main/java/com/irtimaled/bbor/mixin/client/network/MixinNetHandlerPlayClient.java @@ -2,6 +2,7 @@ package com.irtimaled.bbor.mixin.client.network; import com.irtimaled.bbor.client.interop.ClientInterop; import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.play.server.SPacketChunkData; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -13,4 +14,9 @@ public class MixinNetHandlerPlayClient { private void onDisconnect(CallbackInfo ci) { ClientInterop.disconnectedFromRemoteServer(); } + + @Inject(method="handleChunkData", at = @At("RETURN")) + private void onChunkData(SPacketChunkData packet, CallbackInfo ci) { + ClientInterop.receivedChunk(packet.getChunkX(), packet.getChunkZ()); + } } -- 2.44.0