Initial Chunk Caching Commit

The actual code to run is commented out in GameEventHandler, uncomment for testing.
This commit is contained in:
Brady 2018-08-04 23:36:59 -05:00
parent 4fc74a85c1
commit 10c847074a
No known key found for this signature in database
GPG Key ID: 73A788379A197567
14 changed files with 684 additions and 3 deletions

View File

@ -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.
* <br>
* 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.
* <br>
* 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;
}
}

View File

@ -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");
}
}

View File

@ -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<CachedRegion> 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;
}
}

View File

@ -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<String, CachedWorld> 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<CachedWorld> currentWorldConsumer) {
if (this.currentWorld != null)
currentWorldConsumer.accept(this.currentWorld);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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
)
);
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -14,6 +14,9 @@
"MixinMain",
"MixinMinecraft",
"MixinNetHandlerPlayClient",
"MixinWorldClient"
"MixinWorldClient",
"accessor.IAnvilChunkLoader",
"accessor.IChunkProviderServer"
]
}