diff --git a/modules/Movecraft/pom.xml b/modules/Movecraft/pom.xml
index 448f18ce6..dc9f29f22 100644
--- a/modules/Movecraft/pom.xml
+++ b/modules/Movecraft/pom.xml
@@ -33,6 +33,12 @@
v1_14_R1
jar
+
+ net.countercraft
+ movecraft-v1_15_R1
+ v1_15_R1
+ jar
+
net.countercraft
movecraft-v1_16_R1
diff --git a/modules/v1_15_R1/pom.xml b/modules/v1_15_R1/pom.xml
new file mode 100644
index 000000000..ac7eb6b50
--- /dev/null
+++ b/modules/v1_15_R1/pom.xml
@@ -0,0 +1,61 @@
+
+
+
+ movecraft-parent
+ net.countercraft
+ parent
+ ../../pom.xml
+
+ 4.0.0
+
+ movecraft-v1_15_R1
+ Movecraft-v1_15_R1
+ v1_15_R1
+ jar
+
+
+ org.bukkit
+ craftbukkit
+ 1.15.2-R0.1-SNAPSHOT
+ provided
+
+
+ net.countercraft
+ movecraft-api
+ API
+ jar
+
+
+
+ src/main/java
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ net/countercraft/movecraft/compat/v1_15_R1/**
+ net/countercraft/movecraft/support/v1_15_R1/**
+
+
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.4
+
+
+ net/countercraft/movecraft/compat/v1_15_R1/**
+ net/countercraft/movecraft/support/v1_15_R1/**
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/compat/v1_15_R1/IWorldHandler.java b/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/compat/v1_15_R1/IWorldHandler.java
new file mode 100644
index 000000000..64d737e09
--- /dev/null
+++ b/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/compat/v1_15_R1/IWorldHandler.java
@@ -0,0 +1,320 @@
+package net.countercraft.movecraft.compat.v1_15_R1;
+
+import net.countercraft.movecraft.MovecraftLocation;
+import net.countercraft.movecraft.Rotation;
+import net.countercraft.movecraft.WorldHandler;
+import net.countercraft.movecraft.craft.Craft;
+import net.countercraft.movecraft.util.CollectionUtils;
+import net.countercraft.movecraft.util.MathUtils;
+import net.minecraft.server.v1_15_R1.Block;
+import net.minecraft.server.v1_15_R1.BlockPosition;
+import net.minecraft.server.v1_15_R1.Blocks;
+import net.minecraft.server.v1_15_R1.Chunk;
+import net.minecraft.server.v1_15_R1.ChunkSection;
+import net.minecraft.server.v1_15_R1.EntityPlayer;
+import net.minecraft.server.v1_15_R1.EnumBlockRotation;
+import net.minecraft.server.v1_15_R1.IBlockData;
+import net.minecraft.server.v1_15_R1.NextTickListEntry;
+import net.minecraft.server.v1_15_R1.PacketPlayOutPosition;
+import net.minecraft.server.v1_15_R1.PlayerConnection;
+import net.minecraft.server.v1_15_R1.TileEntity;
+import net.minecraft.server.v1_15_R1.World;
+import net.minecraft.server.v1_15_R1.WorldServer;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.craftbukkit.v1_15_R1.CraftWorld;
+import org.bukkit.craftbukkit.v1_15_R1.block.data.CraftBlockData;
+import org.bukkit.craftbukkit.v1_15_R1.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@SuppressWarnings("unused")
+public class IWorldHandler extends WorldHandler {
+ private static final EnumBlockRotation ROTATION[];
+ static {
+ ROTATION = new EnumBlockRotation[3];
+ ROTATION[Rotation.NONE.ordinal()] = EnumBlockRotation.NONE;
+ ROTATION[Rotation.CLOCKWISE.ordinal()] = EnumBlockRotation.CLOCKWISE_90;
+ ROTATION[Rotation.ANTICLOCKWISE.ordinal()] = EnumBlockRotation.COUNTERCLOCKWISE_90;
+ }
+ private final NextTickProvider tickProvider = new NextTickProvider();
+ private MethodHandle internalTeleportMH;
+
+ public IWorldHandler() {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ Method teleport = null;
+ try {
+ teleport = PlayerConnection.class.getDeclaredMethod("a", double.class, double.class, double.class, float.class, float.class, Set.class);
+ teleport.setAccessible(true);
+ internalTeleportMH = lookup.unreflect(teleport);
+
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void addPlayerLocation(Player player, double x, double y, double z, float yaw, float pitch){
+ EntityPlayer ePlayer = ((CraftPlayer) player).getHandle();
+ if(internalTeleportMH == null) {
+ //something went wrong
+ super.addPlayerLocation(player, x, y, z, yaw, pitch);
+ return;
+ }
+ try {
+ internalTeleportMH.invoke(ePlayer.playerConnection, x, y, z, yaw, pitch, EnumSet.allOf(PacketPlayOutPosition.EnumPlayerTeleportFlags.class));
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ }
+
+ @Override
+ public void rotateCraft(@NotNull Craft craft, @NotNull MovecraftLocation originPoint, @NotNull Rotation rotation) {
+ //*******************************************
+ //* Step one: Convert to Positions *
+ //*******************************************
+ HashMap rotatedPositions = new HashMap<>();
+ Rotation counterRotation = rotation == Rotation.CLOCKWISE ? Rotation.ANTICLOCKWISE : Rotation.CLOCKWISE;
+ for(MovecraftLocation newLocation : craft.getHitBox()){
+ rotatedPositions.put(locationToPosition(MathUtils.rotateVec(counterRotation, newLocation.subtract(originPoint)).add(originPoint)),locationToPosition(newLocation));
+ }
+ //*******************************************
+ //* Step two: Get the tiles *
+ //*******************************************
+ WorldServer nativeWorld = ((CraftWorld) craft.getWorld()).getHandle();
+ List tiles = new ArrayList<>();
+ //get the tiles
+ for(BlockPosition position : rotatedPositions.keySet()){
+ //TileEntity tile = nativeWorld.removeTileEntity(position);
+ TileEntity tile = removeTileEntity(nativeWorld,position);
+ if(tile == null)
+ continue;
+ tile.a(ROTATION[rotation.ordinal()]);
+ //get the nextTick to move with the tile
+ tiles.add(new TileHolder(tile, tickProvider.getNextTick(nativeWorld,position), position));
+ }
+
+ //*******************************************
+ //* Step three: Translate all the blocks *
+ //*******************************************
+ // blockedByWater=false means an ocean-going vessel
+ //TODO: Simplify
+ //TODO: go by chunks
+ //TODO: Don't move unnecessary blocks
+ //get the blocks and rotate them
+ HashMap blockData = new HashMap<>();
+ for(BlockPosition position : rotatedPositions.keySet()){
+ blockData.put(position,nativeWorld.getType(position).a(ROTATION[rotation.ordinal()]));
+ }
+ //create the new block
+ for(Map.Entry entry : blockData.entrySet()) {
+ setBlockFast(nativeWorld, rotatedPositions.get(entry.getKey()), entry.getValue());
+ }
+
+
+ //*******************************************
+ //* Step four: replace all the tiles *
+ //*******************************************
+ //TODO: go by chunks
+ for(TileHolder tileHolder : tiles){
+ moveTileEntity(nativeWorld, rotatedPositions.get(tileHolder.getTilePosition()),tileHolder.getTile());
+ if(tileHolder.getNextTick()==null)
+ continue;
+ final long currentTime = nativeWorld.worldData.getTime();
+ nativeWorld.getBlockTickList().a(rotatedPositions.get(tileHolder.getNextTick().a), (Block)tileHolder.getNextTick().b(), (int) (tileHolder.getNextTick().b - currentTime), tileHolder.getNextTick().c);
+ }
+
+ //*******************************************
+ //* Step five: Destroy the leftovers *
+ //*******************************************
+ //TODO: add support for pass-through
+ Collection deletePositions = CollectionUtils.filter(rotatedPositions.keySet(),rotatedPositions.values());
+ for(BlockPosition position : deletePositions){
+ setBlockFast(nativeWorld, position, Blocks.AIR.getBlockData());
+ }
+ }
+
+ @Override
+ public void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation displacement, @NotNull org.bukkit.World world) {
+ //TODO: Add support for rotations
+ //A craftTranslateCommand should only occur if the craft is moving to a valid position
+ //*******************************************
+ //* Step one: Convert to Positions *
+ //*******************************************
+ BlockPosition translateVector = locationToPosition(displacement);
+ List positions = new ArrayList<>(craft.getHitBox().size());
+ craft.getHitBox().forEach((movecraftLocation) -> positions.add(locationToPosition((movecraftLocation)).b(translateVector)));
+ WorldServer oldNativeWorld = ((CraftWorld) craft.getWorld()).getHandle();
+ World nativeWorld = ((CraftWorld) world).getHandle();
+ //*******************************************
+ //* Step two: Get the tiles *
+ //*******************************************
+ List tiles = new ArrayList<>();
+ //get the tiles
+ for (int i = 0, positionsSize = positions.size(); i < positionsSize; i++) {
+ BlockPosition position = positions.get(i);
+ if (oldNativeWorld.getType(position) == Blocks.AIR.getBlockData())
+ continue;
+ //TileEntity tile = nativeWorld.removeTileEntity(position);
+ TileEntity tile = removeTileEntity(oldNativeWorld, position);
+ if (tile == null)
+ continue;
+ //get the nextTick to move with the tile
+
+ //nativeWorld.capturedTileEntities.remove(position);
+ //nativeWorld.getChunkAtWorldCoords(position).getTileEntities().remove(position);
+ tiles.add(new TileHolder(tile, tickProvider.getNextTick(oldNativeWorld, position), position));
+
+ }
+ //*******************************************
+ //* Step three: Translate all the blocks *
+ //*******************************************
+ // blockedByWater=false means an ocean-going vessel
+ //TODO: Simplify
+ //TODO: go by chunks
+ //TODO: Don't move unnecessary blocks
+ //get the blocks and translate the positions
+ List blockData = new ArrayList<>();
+ List newPositions = new ArrayList<>();
+ for (int i = 0, positionsSize = positions.size(); i < positionsSize; i++) {
+ BlockPosition position = positions.get(i);
+ blockData.add(oldNativeWorld.getType(position));
+ newPositions.add(position.a(translateVector));
+ }
+ //create the new block
+ for(int i = 0, positionSize = newPositions.size(); i deletePositions = positions;
+ if (oldNativeWorld == nativeWorld) deletePositions = CollectionUtils.filter(positions,newPositions);
+ for (int i = 0, deletePositionsSize = deletePositions.size(); i < deletePositionsSize; i++) {
+ BlockPosition position = deletePositions.get(i);
+ setBlockFast(oldNativeWorld, position, Blocks.AIR.getBlockData());
+ }
+ }
+
+ @Nullable
+ private TileEntity removeTileEntity(@NotNull World world, @NotNull BlockPosition position){
+ return world.getChunkAtWorldCoords(position).tileEntities.remove(position);
+ }
+
+ @NotNull
+ private BlockPosition locationToPosition(@NotNull MovecraftLocation loc) {
+ return new BlockPosition(loc.getX(), loc.getY(), loc.getZ());
+ }
+
+ private void setBlockFast(@NotNull World world, @NotNull BlockPosition position,@NotNull IBlockData data) {
+ Chunk chunk = world.getChunkAtWorldCoords(position);
+ ChunkSection chunkSection = chunk.getSections()[position.getY()>>4];
+ if (chunkSection == null) {
+ // Put a GLASS block to initialize the section. It will be replaced next with the real block.
+ chunk.setType(position, Blocks.GLASS.getBlockData(), false);
+ chunkSection = chunk.getSections()[position.getY() >> 4];
+ }
+ if(chunkSection.getType(position.getX()&15, position.getY()&15, position.getZ()&15).equals(data)){
+ //Block is already of correct type and data, don't overwrite
+ return;
+ }
+
+ chunkSection.setType(position.getX()&15, position.getY()&15, position.getZ()&15, data);
+ world.notify(position, data, data, 3);
+ chunk.markDirty();
+ }
+
+ @Override
+ public void setBlockFast(@NotNull Location location, @NotNull BlockData data){
+ setBlockFast(location, Rotation.NONE, data);
+ }
+
+ @Override
+ public void setBlockFast(@NotNull Location location, @NotNull Rotation rotation, @NotNull BlockData data) {
+ IBlockData blockData;
+ if(data instanceof CraftBlockData){
+ blockData = ((CraftBlockData) data).getState();
+ } else {
+ blockData = (IBlockData) data;
+ }
+ blockData = blockData.a(ROTATION[rotation.ordinal()]);
+ World world = ((CraftWorld)(location.getWorld())).getHandle();
+ BlockPosition blockPosition = locationToPosition(bukkit2MovecraftLoc(location));
+ setBlockFast(world,blockPosition,blockData);
+ }
+
+ @Override
+ public void disableShadow(@NotNull Material type) {
+ // Disabled
+ }
+
+ private static MovecraftLocation bukkit2MovecraftLoc(Location l) {
+ return new MovecraftLocation(l.getBlockX(), l.getBlockY(), l.getBlockZ());
+ }
+
+ private void moveTileEntity(@NotNull World nativeWorld, @NotNull BlockPosition newPosition, @NotNull TileEntity tile){
+ Chunk chunk = nativeWorld.getChunkAtWorldCoords(newPosition);
+ tile.invalidateBlockCache();
+ tile.setLocation(nativeWorld, newPosition);
+ if(nativeWorld.captureBlockStates) {
+ tile.setLocation(nativeWorld, newPosition);
+ nativeWorld.capturedTileEntities.put(newPosition, tile);
+ return;
+ }
+ chunk.tileEntities.put(newPosition, tile);
+ }
+
+ private class TileHolder{
+ @NotNull private final TileEntity tile;
+ @Nullable
+ private final NextTickListEntry> nextTick;
+ @NotNull private final BlockPosition tilePosition;
+
+ public TileHolder(@NotNull TileEntity tile, @Nullable NextTickListEntry> nextTick, @NotNull BlockPosition tilePosition){
+ this.tile = tile;
+ this.nextTick = nextTick;
+ this.tilePosition = tilePosition;
+ }
+
+
+ @NotNull
+ public TileEntity getTile() {
+ return tile;
+ }
+
+ @Nullable
+ public NextTickListEntry> getNextTick() {
+ return nextTick;
+ }
+
+ @NotNull
+ public BlockPosition getTilePosition() {
+ return tilePosition;
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/compat/v1_15_R1/NextTickProvider.java b/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/compat/v1_15_R1/NextTickProvider.java
new file mode 100644
index 000000000..62f15bfdd
--- /dev/null
+++ b/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/compat/v1_15_R1/NextTickProvider.java
@@ -0,0 +1,31 @@
+package net.countercraft.movecraft.compat.v1_15_R1;
+
+import net.minecraft.server.v1_15_R1.BlockPosition;
+import net.minecraft.server.v1_15_R1.NextTickListEntry;
+import net.minecraft.server.v1_15_R1.WorldServer;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class NextTickProvider {
+
+ @Nullable
+ public NextTickListEntry> getNextTick(@NotNull WorldServer world,@NotNull BlockPosition position){
+ return null;
+ }
+ @NotNull
+ public Object fakeEntry(@NotNull BlockPosition position){
+ return new Object(){
+ @Override
+ public int hashCode() {
+ return position.hashCode();
+ }
+ @Override
+ public boolean equals(Object other){
+ if (!(other instanceof NextTickListEntry)) {
+ return false;
+ }
+ return position.equals(((NextTickListEntry>)other).a);
+ }
+ };
+ }
+}
diff --git a/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/support/v1_15_R1/IAsyncChunk.java b/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/support/v1_15_R1/IAsyncChunk.java
new file mode 100644
index 000000000..187945867
--- /dev/null
+++ b/modules/v1_15_R1/src/main/java/net/countercraft/movecraft/support/v1_15_R1/IAsyncChunk.java
@@ -0,0 +1,49 @@
+package net.countercraft.movecraft.support.v1_15_R1;
+
+import net.countercraft.movecraft.MovecraftLocation;
+import net.countercraft.movecraft.processing.WorldManager;
+import net.countercraft.movecraft.support.AsyncChunk;
+import net.minecraft.server.v1_15_R1.BlockPosition;
+import net.minecraft.server.v1_15_R1.IBlockData;
+import org.bukkit.Chunk;
+import org.bukkit.Material;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.craftbukkit.v1_15_R1.CraftChunk;
+import org.bukkit.craftbukkit.v1_15_R1.block.data.CraftBlockData;
+import org.jetbrains.annotations.NotNull;
+
+@SuppressWarnings("unused")
+public class IAsyncChunk extends AsyncChunk {
+
+ public IAsyncChunk(@NotNull Chunk chunk) {
+ super(chunk);
+ }
+
+ @NotNull
+ @Override
+ protected CraftChunk adapt(@NotNull org.bukkit.Chunk chunk) {
+ return (CraftChunk) chunk;
+ }
+
+ @NotNull
+ @Override
+ public BlockState getState(@NotNull MovecraftLocation location) {
+ var block = chunk.getBlock(location.getX(), location.getY(), location.getZ());
+ return WorldManager.INSTANCE.executeMain(block::getState);
+ }
+
+ @Override
+ @NotNull
+ public Material getType(@NotNull MovecraftLocation location){
+ return CraftBlockData.fromData(chunk.getHandle().getType(new BlockPosition(location.getX(), location.getY(), location.getZ()))).getMaterial();
+ }
+
+ @Override
+ @NotNull
+ public BlockData getData(@NotNull MovecraftLocation location){
+ IBlockData data = chunk.getHandle().getType(new BlockPosition(location.getX(), location.getY(), location.getZ()));
+ return CraftBlockData.fromData(data);
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 4cefb6de6..55988ea5c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,7 @@
modules/api
modules/v1_14_R1
+ modules/v1_15_R1
modules/v1_16_R1
modules/v1_16_R3
modules/datapack