]> git.lizzy.rs Git - BoundingBoxOutlineReloaded.git/commitdiff
Add logic to load structures from save files
authorIrtimaled <irtimaled@gmail.com>
Fri, 1 May 2020 05:37:29 +0000 (22:37 -0700)
committerIrtimaled <irtimaled@gmail.com>
Mon, 18 May 2020 00:28:01 +0000 (17:28 -0700)
src/main/java/com/irtimaled/bbor/client/ClientProxy.java
src/main/java/com/irtimaled/bbor/client/commands/StructuresCommand.java [new file with mode: 0644]
src/main/java/com/irtimaled/bbor/client/events/SaveLoaded.java [new file with mode: 0644]
src/main/java/com/irtimaled/bbor/client/gui/LoadSavesScreen.java [new file with mode: 0644]
src/main/java/com/irtimaled/bbor/client/gui/WorldSaveRow.java [new file with mode: 0644]
src/main/java/com/irtimaled/bbor/client/interop/ClientInterop.java
src/main/java/com/irtimaled/bbor/client/interop/NBTStructureLoader.java [new file with mode: 0644]
src/main/java/com/irtimaled/bbor/client/interop/SaveGameStructureLoader.java [new file with mode: 0644]
src/main/java/com/irtimaled/bbor/mixin/client/network/MixinNetHandlerPlayClient.java

index 3974c42360af6d7907cd8f4a4d015feac63db99f..8d527ae39faf8da42a8607ef121386df98f53515 100644 (file)
@@ -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 (file)
index 0000000..b0ef080
--- /dev/null
@@ -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<ISuggestionProvider> 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 (file)
index 0000000..f40380b
--- /dev/null
@@ -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 (file)
index 0000000..9d941e1
--- /dev/null
@@ -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<WorldSummary> 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 (file)
index 0000000..5f26c74
--- /dev/null
@@ -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<WorldSaveRow> {
+    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);
+    }
+}
index d540dd2c0de1a3e7a8983470517846f679b1a554..e1d0fdf50009ab82b2bcc143393822412fa93984 100644 (file)
@@ -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 (file)
index 0000000..c15b2e1
--- /dev/null
@@ -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<String> 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<String, StructureStart> 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 (file)
index 0000000..3802936
--- /dev/null
@@ -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<Integer, NBTStructureLoader> 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();
+    }
+}
index 6f8e1a08be88d3cc753a95c7dffac823f64d5139..e1a66bc032844fec7a4d1a42e8806d1021412f78 100644 (file)
@@ -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());
+    }
 }