diff --git a/src/main/java/baritone/bot/chunk/CachedChunk.java b/src/main/java/baritone/bot/chunk/CachedChunk.java new file mode 100644 index 00000000..b139eb69 --- /dev/null +++ b/src/main/java/baritone/bot/chunk/CachedChunk.java @@ -0,0 +1,103 @@ +package baritone.bot.chunk; + +import baritone.bot.pathing.util.IBlockTypeAccess; +import baritone.bot.pathing.util.PathingBlockType; + +import java.util.BitSet; + +/** + * @author Brady + * @since 8/3/2018 1:04 AM + */ +public final class CachedChunk implements IBlockTypeAccess { + + /** + * The size of the chunk data in bits. Equal to 16 KiB. + *
+ * Chunks are 16x16x256, each block requires 2 bits. + */ + public static final int SIZE = 2 * 16 * 16 * 256; + + /** + * The size of the chunk data in bytes. Equal to 16 KiB. + */ + public static final int SIZE_IN_BYTES = SIZE / 8; + + /** + * An array of just 0s with the length of {@link CachedChunk#SIZE_IN_BYTES} + */ + public static final byte[] EMPTY_CHUNK = new byte[SIZE_IN_BYTES]; + + /** + * The chunk x coordinate + */ + private final int x; + + /** + * The chunk z coordinate + */ + private final int z; + + /** + * The actual raw data of this packed chunk. + *
+ * Each block is expressed as 2 bits giving a total of 16 KiB + */ + private final BitSet data; + + CachedChunk(int x, int z, BitSet data) { + if (data.size() != SIZE) + throw new IllegalArgumentException("BitSet of invalid length provided"); + + this.x = x; + this.z = z; + this.data = data; + } + + @Override + public final PathingBlockType getBlockType(int x, int y, int z) { + int index = getPositionIndex(x, y, z); + return PathingBlockType.fromBits(data.get(index), data.get(index + 1)); + } + + void updateContents(BitSet data) { + if (data.size() > SIZE) + throw new IllegalArgumentException("BitSet of invalid length provided"); + + for (int i = 0; i < data.length(); i++) + this.data.set(i, data.get(i)); + } + + /** + * @return Thee chunk x coordinat + */ + public final int getX() { + return this.x; + } + + /** + * @return The chunk z coordinate + */ + public final int getZ() { + return this.z; + } + + /** + * @return Returns the raw packed chunk data as a byte array + */ + public final byte[] toByteArray() { + return this.data.toByteArray(); + } + + /** + * Returns the raw bit index of the specified position + * + * @param x The x position + * @param y The y position + * @param z The z position + * @return The bit index + */ + public static int getPositionIndex(int x, int y, int z) { + return (x + (z << 4) + (y << 8)) * 2; + } +} diff --git a/src/main/java/baritone/bot/chunk/CachedRegion.java b/src/main/java/baritone/bot/chunk/CachedRegion.java new file mode 100644 index 00000000..9be288f0 --- /dev/null +++ b/src/main/java/baritone/bot/chunk/CachedRegion.java @@ -0,0 +1,141 @@ +package baritone.bot.chunk; + +import baritone.bot.pathing.util.PathingBlockType; +import baritone.bot.utils.GZIPUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.BitSet; + +/** + * @author Brady + * @since 8/3/2018 9:35 PM + */ +public final class CachedRegion implements ICachedChunkAccess { + + /** + * All of the chunks in this region. A 16x16 array of them. + * + * I would make these 32x32 regions to be in line with the Anvil format, but 16 is a nice number. + */ + private final CachedChunk[][] chunks = new CachedChunk[32][32]; + + /** + * The region x coordinate + */ + private final int x; + + /** + * The region z coordinate + */ + private final int z; + + CachedRegion(int x, int z) { + this.x = x; + this.z = z; + } + + @Override + public final PathingBlockType getBlockType(int x, int y, int z) { + CachedChunk chunk = this.getChunk(x >> 4, z >> 4); + if (chunk != null) { + return chunk.getBlockType(x, y, z); + } + return null; + } + + @Override + public final void updateCachedChunk(int chunkX, int chunkZ, BitSet data) { + CachedChunk chunk = this.getChunk(chunkX, chunkZ); + if (chunk == null) + this.chunks[chunkX][chunkZ] = new CachedChunk(chunkX, chunkZ, data); + else + chunk.updateContents(data); + } + + private CachedChunk getChunk(int chunkX, int chunkZ) { + return this.chunks[chunkX][chunkZ]; + } + + public final void save(String directory) { + try { + Path path = Paths.get(directory); + if (!Files.exists(path)) + Files.createDirectories(path); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(32 * 32 * CachedChunk.SIZE_IN_BYTES); + for (int z = 0; z < 32; z++) { + for (int x = 0; x < 32; x++) { + CachedChunk chunk = this.chunks[x][z]; + if (chunk == null) { + bos.write(CachedChunk.EMPTY_CHUNK); + } else { + byte[] chunkBytes = chunk.toByteArray(); + bos.write(chunkBytes); + // Messy, but fills the empty 0s that should be trailing to fill up the space. + bos.write(new byte[CachedChunk.SIZE_IN_BYTES - chunkBytes.length]); + } + } + } + + Path regionFile = getRegionFile(path, this.x, this.z); + if (!Files.exists(regionFile)) + Files.createFile(regionFile); + + byte[] compressed = GZIPUtils.compress(bos.toByteArray()); + if (compressed != null) + Files.write(regionFile, compressed); + } catch (IOException ignored) {} + } + + public void load(String directory) { + try { + Path path = Paths.get(directory); + if (!Files.exists(path)) + Files.createDirectories(path); + + Path regionFile = getRegionFile(path, this.x, this.z); + if (!Files.exists(regionFile)) + return; + + byte[] fileBytes = Files.readAllBytes(regionFile); + byte[] decompressed = GZIPUtils.decompress(fileBytes); + if (decompressed == null) + return; + + for (int z = 0; z < 32; z++) { + for (int x = 0; x < 32; x++) { + CachedChunk chunk = this.chunks[x][z]; + if (chunk != null) { + int index = (x + (z << 5)) * CachedChunk.SIZE_IN_BYTES; + byte[] bytes = Arrays.copyOfRange(decompressed, index, index + CachedChunk.SIZE_IN_BYTES); + BitSet bits = BitSet.valueOf(bytes); + chunk.updateContents(bits); + } + } + } + } catch (IOException ignored) {} + } + + /** + * @return The region x coordinate + */ + public final int getX() { + return this.x; + } + + /** + * @return The region z coordinate + */ + public final int getZ() { + return this.z; + } + + private static Path getRegionFile(Path cacheDir, int regionX, int regionZ) { + return Paths.get(cacheDir.toString() + "\\r." + regionX + "." + regionZ + ".bcr"); + } +} diff --git a/src/main/java/baritone/bot/chunk/CachedWorld.java b/src/main/java/baritone/bot/chunk/CachedWorld.java new file mode 100644 index 00000000..35aad603 --- /dev/null +++ b/src/main/java/baritone/bot/chunk/CachedWorld.java @@ -0,0 +1,119 @@ +package baritone.bot.chunk; + +import baritone.bot.pathing.util.PathingBlockType; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +import java.util.BitSet; + +/** + * @author Brady + * @since 8/4/2018 12:02 AM + */ +public final class CachedWorld implements ICachedChunkAccess { + + /** + * The maximum number of regions in any direction from (0,0) + */ + private static final int REGION_MAX = 117188; + + /** + * A map of all of the cached regions. + */ + private Long2ObjectMap cachedRegions = new Long2ObjectOpenHashMap<>(); + + /** + * The directory that the cached region files are saved to + */ + private final String directory; + + public CachedWorld(String directory) { + this.directory = directory; + // Insert an invalid region element + cachedRegions.put(0, null); + } + + @Override + public final PathingBlockType getBlockType(int x, int y, int z) { + CachedRegion region = getRegion(x >> 9, z >> 9); + if (region != null) { + return region.getBlockType(x, y, z); + } + return null; + } + + @Override + public final void updateCachedChunk(int chunkX, int chunkZ, BitSet data) { + CachedRegion region = getOrCreateRegion(chunkX >> 5, chunkZ >> 5); + if (region != null) { + region.updateCachedChunk(chunkX & 31, chunkZ & 31, data); + } + } + + public final void save() { + this.cachedRegions.values().forEach(region -> { + if (region != null) + region.save(this.directory); + }); + } + + public final void load() { + this.cachedRegions.values().forEach(region -> { + if (region != null) + region.load(this.directory); + }); + } + + /** + * Returns the region at the specified region coordinates + * + * @param regionX The region X coordinate + * @param regionZ The region Z coordinate + * @return The region located at the specified coordinates + */ + public final CachedRegion getRegion(int regionX, int regionZ) { + return cachedRegions.get(getRegionID(regionX, regionZ)); + } + + /** + * Returns the region at the specified region coordinates. If a + * region is not found, then a new one is created. + * + * @param regionX The region X coordinate + * @param regionZ The region Z coordinate + * @return The region located at the specified coordinates + */ + private CachedRegion getOrCreateRegion(int regionX, int regionZ) { + return cachedRegions.computeIfAbsent(getRegionID(regionX, regionZ), id -> { + CachedRegion newRegion = new CachedRegion(regionX, regionZ); + newRegion.load(this.directory); + return newRegion; + }); + } + + /** + * Returns the region ID based on the region coordinates. 0 will be + * returned if the specified region coordinates are out of bounds. + * + * @param regionX The region X coordinate + * @param regionZ The region Z coordinate + * @return The region ID + */ + private long getRegionID(int regionX, int regionZ) { + if (!isRegionInWorld(regionX, regionZ)) + return 0; + + return (long) regionX & 0xFFFFFFFFL | ((long) regionZ & 0xFFFFFFFFL) << 32; + } + + /** + * Returns whether or not the specified region coordinates is within the world bounds. + * + * @param regionX The region X coordinate + * @param regionZ The region Z coordinate + * @return Whether or not the region is in world bounds + */ + private boolean isRegionInWorld(int regionX, int regionZ) { + return regionX <= REGION_MAX && regionX >= -REGION_MAX && regionZ <= REGION_MAX && regionZ >= -REGION_MAX; + } +} diff --git a/src/main/java/baritone/bot/chunk/CachedWorldProvider.java b/src/main/java/baritone/bot/chunk/CachedWorldProvider.java new file mode 100644 index 00000000..fb2e69fd --- /dev/null +++ b/src/main/java/baritone/bot/chunk/CachedWorldProvider.java @@ -0,0 +1,63 @@ +package baritone.bot.chunk; + +import baritone.bot.utils.Helper; +import baritone.launch.mixins.accessor.IAnvilChunkLoader; +import baritone.launch.mixins.accessor.IChunkProviderServer; +import net.minecraft.client.multiplayer.WorldClient; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.world.WorldServer; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * @author Brady + * @since 8/4/2018 11:06 AM + */ +public enum CachedWorldProvider implements Helper { + + INSTANCE; + + private final Map singlePlayerWorldCache = new HashMap<>(); + + private CachedWorld currentWorld; + + public final CachedWorld getCurrentWorld() { + return this.currentWorld; + } + + public final void initWorld(WorldClient world) { + IntegratedServer integratedServer; + if ((integratedServer = mc.getIntegratedServer()) != null) { + + WorldServer localServerWorld = integratedServer.getWorld(world.provider.getDimensionType().getId()); + IChunkProviderServer provider = (IChunkProviderServer) localServerWorld.getChunkProvider(); + IAnvilChunkLoader loader = (IAnvilChunkLoader) provider.getChunkLoader(); + + Path dir = new File(new File(loader.getChunkSaveLocation(), "region"), "cache").toPath(); + if (!Files.exists(dir)) { + try { + Files.createDirectories(dir); + } catch (IOException ignored) {} + } + + this.currentWorld = this.singlePlayerWorldCache.computeIfAbsent(dir.toString(), CachedWorld::new); + this.currentWorld.load(); + } + // TODO: Store server worlds + } + + public final void closeWorld() { + this.currentWorld = null; + } + + public final void ifWorldLoaded(Consumer currentWorldConsumer) { + if (this.currentWorld != null) + currentWorldConsumer.accept(this.currentWorld); + } +} diff --git a/src/main/java/baritone/bot/chunk/ChunkPacker.java b/src/main/java/baritone/bot/chunk/ChunkPacker.java new file mode 100644 index 00000000..883a7969 --- /dev/null +++ b/src/main/java/baritone/bot/chunk/ChunkPacker.java @@ -0,0 +1,60 @@ +package baritone.bot.chunk; + +import baritone.bot.pathing.movement.MovementHelper; +import baritone.bot.pathing.util.PathingBlockType; +import baritone.bot.utils.Helper; +import net.minecraft.block.Block; +import net.minecraft.block.BlockAir; +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.chunk.Chunk; + +import java.util.BitSet; + +import static net.minecraft.block.Block.NULL_AABB; + +/** + * @author Brady + * @since 8/3/2018 1:09 AM + */ +public final class ChunkPacker implements Helper { + + private ChunkPacker() {} + + public static BitSet createPackedChunk(Chunk chunk) { + BitSet bitSet = new BitSet(CachedChunk.SIZE); + try { + for (int y = 0; y < 256; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int index = CachedChunk.getPositionIndex(x, y, z); + boolean[] bits = getPathingBlockType(new BlockPos(x, y, z), chunk.getBlockState(x, y, z)).getBits(); + bitSet.set(index, bits[0]); + bitSet.set(index + 1, bits[1]); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return bitSet; + } + + private static PathingBlockType getPathingBlockType(BlockPos pos, IBlockState state) { + Block block = state.getBlock(); + + if (MovementHelper.isWater(block)) { + return PathingBlockType.WATER; + } + + if (MovementHelper.avoidWalkingInto(block)) { + return PathingBlockType.AVOID; + } + + if (block instanceof BlockAir || state.getCollisionBoundingBox(mc.world, pos) == NULL_AABB) { + return PathingBlockType.AIR; + } + + return PathingBlockType.SOLID; + } +} diff --git a/src/main/java/baritone/bot/chunk/ICachedChunkAccess.java b/src/main/java/baritone/bot/chunk/ICachedChunkAccess.java new file mode 100644 index 00000000..93aac861 --- /dev/null +++ b/src/main/java/baritone/bot/chunk/ICachedChunkAccess.java @@ -0,0 +1,14 @@ +package baritone.bot.chunk; + +import baritone.bot.pathing.util.IBlockTypeAccess; + +import java.util.BitSet; + +/** + * @author Brady + * @since 8/4/2018 1:10 AM + */ +public interface ICachedChunkAccess extends IBlockTypeAccess { + + void updateCachedChunk(int chunkX, int chunkZ, BitSet data); +} diff --git a/src/main/java/baritone/bot/event/events/WorldEvent.java b/src/main/java/baritone/bot/event/events/WorldEvent.java new file mode 100644 index 00000000..8b219131 --- /dev/null +++ b/src/main/java/baritone/bot/event/events/WorldEvent.java @@ -0,0 +1,40 @@ +package baritone.bot.event.events; + +import baritone.bot.event.events.type.EventState; +import net.minecraft.client.multiplayer.WorldClient; + +/** + * @author Brady + * @since 8/4/2018 3:13 AM + */ +public final class WorldEvent { + + /** + * The new world that is being loaded. {@code null} if being unloaded. + */ + private final WorldClient world; + + /** + * The state of the event + */ + private final EventState state; + + public WorldEvent(WorldClient world, EventState state) { + this.world = world; + this.state = state; + } + + /** + * @return The new world that is being loaded. {@code null} if being unloaded. + */ + public final WorldClient getWorld() { + return this.world; + } + + /** + * @return The state of the event + */ + public final EventState getState() { + return this.state; + } +} diff --git a/src/main/java/baritone/bot/pathing/util/IBlockTypeAccess.java b/src/main/java/baritone/bot/pathing/util/IBlockTypeAccess.java new file mode 100644 index 00000000..07db4ce4 --- /dev/null +++ b/src/main/java/baritone/bot/pathing/util/IBlockTypeAccess.java @@ -0,0 +1,17 @@ +package baritone.bot.pathing.util; + +import baritone.bot.utils.Helper; +import net.minecraft.util.math.BlockPos; + +/** + * @author Brady + * @since 8/4/2018 2:01 AM + */ +public interface IBlockTypeAccess extends Helper { + + PathingBlockType getBlockType(int x, int y, int z); + + default PathingBlockType getBlockType(BlockPos pos) { + return getBlockType(pos.getX(), pos.getY(), pos.getZ()); + } +} diff --git a/src/main/java/baritone/bot/pathing/util/PathingBlockType.java b/src/main/java/baritone/bot/pathing/util/PathingBlockType.java new file mode 100644 index 00000000..433bb580 --- /dev/null +++ b/src/main/java/baritone/bot/pathing/util/PathingBlockType.java @@ -0,0 +1,35 @@ +package baritone.bot.pathing.util; + +/** + * @author Brady + * @since 8/4/2018 1:11 AM + */ +public enum PathingBlockType { + + AIR (0b00), + WATER(0b01), + AVOID(0b10), + SOLID(0b11); + + private final boolean[] bits; + + PathingBlockType(int bits) { + this.bits = new boolean[] { + (bits & 0b10) != 0, + (bits & 0b01) != 0 + }; + } + + public final boolean[] getBits() { + return this.bits; + } + + public static PathingBlockType fromBits(boolean b1, boolean b2) { + for (PathingBlockType type : values()) + if (type.bits[0] == b1 && type.bits[1] == b2) + return type; + + // This will never happen, but if it does, assume it's just AIR + return PathingBlockType.AIR; + } +} diff --git a/src/main/java/baritone/launch/mixins/MixinChunkProviderServer.java b/src/main/java/baritone/launch/mixins/MixinChunkProviderServer.java new file mode 100644 index 00000000..ef1aa344 --- /dev/null +++ b/src/main/java/baritone/launch/mixins/MixinChunkProviderServer.java @@ -0,0 +1,16 @@ +package baritone.launch.mixins; + +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.gen.ChunkProviderServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * @author Brady + * @since 8/4/2018 11:33 AM + */ +@Mixin(ChunkProviderServer.class) +public interface MixinChunkProviderServer { + + @Accessor IChunkLoader getChunkLoader(); +} diff --git a/src/main/java/baritone/launch/mixins/MixinMinecraft.java b/src/main/java/baritone/launch/mixins/MixinMinecraft.java index 526fd31e..4dbc40bf 100755 --- a/src/main/java/baritone/launch/mixins/MixinMinecraft.java +++ b/src/main/java/baritone/launch/mixins/MixinMinecraft.java @@ -1,7 +1,10 @@ package baritone.launch.mixins; import baritone.bot.Baritone; +import baritone.bot.event.events.WorldEvent; +import baritone.bot.event.events.type.EventState; import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.WorldClient; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumActionResult; import net.minecraft.util.math.BlockPos; @@ -21,8 +24,8 @@ import org.spongepowered.asm.mixin.injection.callback.LocalCapture; @Mixin(Minecraft.class) public class MixinMinecraft { - @Shadow - private int leftClickCounter; + @Shadow private int leftClickCounter; + @Shadow public WorldClient world; @Inject( method = "init", @@ -99,4 +102,38 @@ public class MixinMinecraft { bot.getMemory().scanBlock(pos.offset(mc.objectMouseOver.sideHit)); bot.getActionHandler().onPlacedBlock(stack, pos); } + + @Inject( + method = "loadWorld(Lnet/minecraft/client/multiplayer/WorldClient;Ljava/lang/String;)V", + at = @At("HEAD") + ) + private void preLoadWorld(WorldClient world, String loadingMessage, CallbackInfo ci) { + // If we're unloading the world but one doesn't exist, ignore it + if (this.world == null && world == null) + return; + + Baritone.INSTANCE.getGameEventHandler().onWorldEvent( + new WorldEvent( + world, + EventState.PRE + ) + ); + } + + @Inject( + method = "loadWorld(Lnet/minecraft/client/multiplayer/WorldClient;Ljava/lang/String;)V", + at = @At("RETURN") + ) + private void postLoadWorld(WorldClient world, String loadingMessage, CallbackInfo ci) { + // If we're unloading the world but one doesn't exist, ignore it + if (this.world == null && world == null) + return; + + Baritone.INSTANCE.getGameEventHandler().onWorldEvent( + new WorldEvent( + world, + EventState.POST + ) + ); + } } diff --git a/src/main/java/baritone/launch/mixins/accessor/IAnvilChunkLoader.java b/src/main/java/baritone/launch/mixins/accessor/IAnvilChunkLoader.java new file mode 100644 index 00000000..ca19337b --- /dev/null +++ b/src/main/java/baritone/launch/mixins/accessor/IAnvilChunkLoader.java @@ -0,0 +1,17 @@ +package baritone.launch.mixins.accessor; + +import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.io.File; + +/** + * @author Brady + * @since 8/4/2018 11:36 AM + */ +@Mixin(AnvilChunkLoader.class) +public interface IAnvilChunkLoader { + + @Accessor File getChunkSaveLocation(); +} diff --git a/src/main/java/baritone/launch/mixins/accessor/IChunkProviderServer.java b/src/main/java/baritone/launch/mixins/accessor/IChunkProviderServer.java new file mode 100644 index 00000000..455ce22d --- /dev/null +++ b/src/main/java/baritone/launch/mixins/accessor/IChunkProviderServer.java @@ -0,0 +1,16 @@ +package baritone.launch.mixins.accessor; + +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.gen.ChunkProviderServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * @author Brady + * @since 8/4/2018 11:33 AM + */ +@Mixin(ChunkProviderServer.class) +public interface IChunkProviderServer { + + @Accessor IChunkLoader getChunkLoader(); +} diff --git a/src/main/resources/mixins.baritone.json b/src/main/resources/mixins.baritone.json index fa8c9b9a..24bb4c64 100755 --- a/src/main/resources/mixins.baritone.json +++ b/src/main/resources/mixins.baritone.json @@ -14,6 +14,9 @@ "MixinMain", "MixinMinecraft", "MixinNetHandlerPlayClient", - "MixinWorldClient" + "MixinWorldClient", + + "accessor.IAnvilChunkLoader", + "accessor.IChunkProviderServer" ] } \ No newline at end of file