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;
.onKeyPressHandler(SettingsScreen::show);
mainKey.register("key.keyboard.o")
.onKeyPressHandler(() -> ConfigManager.Toggle(ConfigManager.outerBoxesOnly));
+ mainKey.register("key.keyboard.l")
+ .onKeyPressHandler(LoadSavesScreen::show);
}
@Override
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));
private void disconnectedFromServer() {
ClientRenderer.deactivate();
if (ConfigManager.keepCacheBetweenSessions.get()) return;
+ clear();
+ }
+
+ private void clear() {
SlimeChunkProvider.clear();
WorldSpawnProvider.clear();
SpawningSphereProvider.clear();
--- /dev/null
+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);
+ }
+}
--- /dev/null
+package com.irtimaled.bbor.client.events;
+
+public class SaveLoaded {
+ public SaveLoaded() {
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
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;
public class ClientInterop {
public static void disconnectedFromRemoteServer() {
+ SaveGameStructureLoader.clear();
EventBus.publish(new DisconnectedFromRemoteServer());
}
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();
}
}
--- /dev/null
+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;
+ }
+ }
+}
--- /dev/null
+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();
+ }
+}
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;
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());
+ }
}