diff --git a/Movecraft/build.gradle.kts b/Movecraft/build.gradle.kts index 21589eb98..0220d341d 100644 --- a/Movecraft/build.gradle.kts +++ b/Movecraft/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { runtimeOnly(project(":movecraft-v1_21_4", "reobf")) runtimeOnly(project(":movecraft-v1_21_5", "reobf")) runtimeOnly(project(":movecraft-v1_21_8", "reobf")) + runtimeOnly(project(":movecraft-v1_21_10", "reobf")) implementation(project(":movecraft-api")) compileOnly("org.yaml:snakeyaml:2.0") } @@ -29,6 +30,7 @@ tasks.shadowJar { include(project(":movecraft-v1_21_4")) include(project(":movecraft-v1_21_5")) include(project(":movecraft-v1_21_8")) + include(project(":movecraft-v1_21_10")) } } @@ -72,7 +74,7 @@ hangarPublish { platforms { register(io.papermc.hangarpublishplugin.model.Platforms.PAPER) { jar.set(tasks.shadowJar.flatMap { it.archiveFile }) - platformVersions.set(listOf("1.20.6", "1.21.1", "1.21.4", "1.21.5", "1.21.8")) + platformVersions.set(listOf("1.20.6", "1.21.1", "1.21.4", "1.21.5", "1.21.8", "1.21.10")) } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index aa4f77998..560b76631 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ include(":movecraft-v1_21_1") include(":movecraft-v1_21_4") include(":movecraft-v1_21_5") include(":movecraft-v1_21_8") +include(":movecraft-v1_21_10") include(":movecraft-api") include(":movecraft-datapack") include(":movecraft") @@ -12,6 +13,7 @@ project(":movecraft-v1_21_1").projectDir = file("v1_21_1") project(":movecraft-v1_21_4").projectDir = file("v1_21_4") project(":movecraft-v1_21_5").projectDir = file("v1_21_5") project(":movecraft-v1_21_8").projectDir = file("v1_21_8") +project(":movecraft-v1_21_10").projectDir = file("v1_21_10") project(":movecraft-api").projectDir = file("api") project(":movecraft-datapack").projectDir = file("datapack") project(":movecraft").projectDir = file("Movecraft") diff --git a/v1_21_10/build.gradle.kts b/v1_21_10/build.gradle.kts new file mode 100644 index 000000000..c33d4b898 --- /dev/null +++ b/v1_21_10/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("buildlogic.java-conventions") + id("io.papermc.paperweight.userdev") +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +dependencies { + api(project(":movecraft-api")) + paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") +} + +description = "Movecraft-v1_21_10" +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION diff --git a/v1_21_10/src/main/java/net/countercraft/movecraft/compat/v1_21_10/IWorldHandler.java b/v1_21_10/src/main/java/net/countercraft/movecraft/compat/v1_21_10/IWorldHandler.java new file mode 100644 index 000000000..fcc2f7c54 --- /dev/null +++ b/v1_21_10/src/main/java/net/countercraft/movecraft/compat/v1_21_10/IWorldHandler.java @@ -0,0 +1,342 @@ +package net.countercraft.movecraft.compat.v1_21_10; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.MovecraftRotation; +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.countercraft.movecraft.util.UnsafeUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.ticks.LevelChunkTicks; +import net.minecraft.world.ticks.ScheduledTick; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +@SuppressWarnings("unused") +public class IWorldHandler extends WorldHandler { + private static final Rotation ROTATION[]; + + static { + ROTATION = new Rotation[3]; + ROTATION[MovecraftRotation.NONE.ordinal()] = Rotation.NONE; + ROTATION[MovecraftRotation.CLOCKWISE.ordinal()] = Rotation.CLOCKWISE_90; + ROTATION[MovecraftRotation.ANTICLOCKWISE.ordinal()] = Rotation.COUNTERCLOCKWISE_90; + } + + private final NextTickProvider tickProvider = new NextTickProvider(); + + public IWorldHandler() { + String version = Bukkit.getServer().getMinecraftVersion(); + if (!version.equals("1.21.10")) + throw new IllegalStateException("Movecraft is not compatible with this version of Minecraft: " + version); + } + + @Override + public void rotateCraft(@NotNull Craft craft, @NotNull MovecraftLocation originPoint, @NotNull MovecraftRotation rotation) { + //******************************************* + //* Step one: Convert to Positions * + //******************************************* + HashMap rotatedPositions = new HashMap<>(); + MovecraftRotation counterRotation = rotation == MovecraftRotation.CLOCKWISE ? MovecraftRotation.ANTICLOCKWISE : MovecraftRotation.CLOCKWISE; + for (MovecraftLocation newLocation : craft.getHitBox()) { + rotatedPositions.put(locationToPosition(MathUtils.rotateVec(counterRotation, newLocation.subtract(originPoint)).add(originPoint)), locationToPosition(newLocation)); + } + //******************************************* + //* Step two: Get the tiles * + //******************************************* + ServerLevel nativeWorld = ((CraftWorld) craft.getWorld()).getHandle(); + List tiles = new ArrayList<>(); + List ticks = new ArrayList<>(); + //get the tiles + for (BlockPos position : rotatedPositions.keySet()) { + //BlockEntity tile = nativeWorld.removeBlockEntity(position); + BlockEntity tile = removeBlockEntity(nativeWorld, position); + if (tile != null) + tiles.add(new TileHolder(tile, position)); + + //get the nextTick to move with the tile + ScheduledTick tickHere = tickProvider.getNextTick(nativeWorld, position); + if (tickHere != null) { + ((LevelChunkTicks) nativeWorld.getChunkAt(position).getBlockTicks()).removeIf( + (Predicate) scheduledTick -> scheduledTick.equals(tickHere)); + ticks.add(new TickHolder(tickHere, 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 (BlockPos position : rotatedPositions.keySet()) { + blockData.put(position, nativeWorld.getBlockState(position).rotate(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) + moveBlockEntity(nativeWorld, rotatedPositions.get(tileHolder.getTilePosition()), tileHolder.getTile()); + for (TickHolder tickHolder : ticks) { + final long currentTime = nativeWorld.serverLevelData.getGameTime(); + nativeWorld.getBlockTicks().schedule(new ScheduledTick<>( + (Block) tickHolder.getTick().type(), + rotatedPositions.get(tickHolder.getTick().pos()), + tickHolder.getTick().triggerTick() - currentTime, + tickHolder.getTick().priority(), + tickHolder.getTick().subTickOrder())); + } + + //******************************************* + //* Step five: Destroy the leftovers * + //******************************************* + //TODO: add support for pass-through + Collection deletePositions = CollectionUtils.filter(rotatedPositions.keySet(), rotatedPositions.values()); + for (BlockPos position : deletePositions) { + setBlockFast(nativeWorld, position, Blocks.AIR.defaultBlockState()); + } + } + + @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 * + //******************************************* + BlockPos translateVector = locationToPosition(displacement); + List positions = new ArrayList<>(craft.getHitBox().size()); + craft.getHitBox().forEach((movecraftLocation) -> positions.add(locationToPosition((movecraftLocation)).subtract(translateVector))); + ServerLevel oldNativeWorld = ((CraftWorld) craft.getWorld()).getHandle(); + ServerLevel nativeWorld = ((CraftWorld) world).getHandle(); + //******************************************* + //* Step two: Get the tiles * + //******************************************* + List tiles = new ArrayList<>(); + List ticks = new ArrayList<>(); + //get the tiles + for (int i = 0, positionsSize = positions.size(); i < positionsSize; i++) { + BlockPos position = positions.get(i); + if (oldNativeWorld.getBlockState(position) == Blocks.AIR.defaultBlockState()) + continue; + //BlockEntity tile = nativeWorld.removeBlockEntity(position); + BlockEntity tile = removeBlockEntity(oldNativeWorld, position); + if (tile != null) + tiles.add(new TileHolder(tile,position)); + + //get the nextTick to move with the tile + ScheduledTick tickHere = tickProvider.getNextTick(nativeWorld, position); + if (tickHere != null) { + ((LevelChunkTicks) nativeWorld.getChunkAt(position).getBlockTicks()).removeIf( + (Predicate) scheduledTick -> scheduledTick.equals(tickHere)); + ticks.add(new TickHolder(tickHere, 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++) { + BlockPos position = positions.get(i); + blockData.add(oldNativeWorld.getBlockState(position)); + newPositions.add(position.offset(translateVector)); + } + //create the new block + for (int i = 0, positionSize = newPositions.size(); i < positionSize; i++) { + setBlockFast(nativeWorld, newPositions.get(i), blockData.get(i)); + } + //******************************************* + //* Step four: replace all the tiles * + //******************************************* + //TODO: go by chunks + for (TileHolder tileHolder : tiles) + moveBlockEntity(nativeWorld, tileHolder.getTilePosition().offset(translateVector), tileHolder.getTile()); + for (TickHolder tickHolder : ticks) { + final long currentTime = nativeWorld.getGameTime(); + nativeWorld.getBlockTicks().schedule(new ScheduledTick<>((Block) tickHolder.getTick().type(), tickHolder.getTickPosition().offset(translateVector), tickHolder.getTick().triggerTick() - currentTime, tickHolder.getTick().priority(), tickHolder.getTick().subTickOrder())); + } + //******************************************* + //* Step five: Destroy the leftovers * + //******************************************* + List deletePositions = positions; + if (oldNativeWorld == nativeWorld) + deletePositions = CollectionUtils.filter(positions, newPositions); + for (int i = 0, deletePositionsSize = deletePositions.size(); i < deletePositionsSize; i++) { + BlockPos position = deletePositions.get(i); + setBlockFast(oldNativeWorld, position, Blocks.AIR.defaultBlockState()); + } + } + + @Nullable + private BlockEntity removeBlockEntity(@NotNull Level world, @NotNull BlockPos position) { + BlockEntity testEntity = world.getChunkAt(position).getBlockEntity(position); + //Prevents moving pistons by locking up by forcing their movement to finish + if (testEntity instanceof PistonMovingBlockEntity) + { + BlockState oldState; + if (((PistonMovingBlockEntity) testEntity).isSourcePiston() && testEntity.getBlockState().getBlock() instanceof PistonBaseBlock) { + if (((PistonMovingBlockEntity) testEntity).getMovedState().is(Blocks.PISTON)) + oldState = Blocks.PISTON.defaultBlockState() + .setValue(PistonBaseBlock.FACING, ((PistonMovingBlockEntity) testEntity).getMovedState().getValue(PistonBaseBlock.FACING)); + else + oldState = Blocks.STICKY_PISTON.defaultBlockState() + .setValue(PistonBaseBlock.FACING, ((PistonMovingBlockEntity) testEntity).getMovedState().getValue(PistonBaseBlock.FACING)); + } else + oldState = ((PistonMovingBlockEntity) testEntity).getMovedState(); + ((PistonMovingBlockEntity) testEntity).finalTick(); + setBlockFast(world, position, oldState); + return world.getBlockEntity(position); + } + return world.getChunkAt(position).blockEntities.remove(position); + } + + @NotNull + private BlockPos locationToPosition(@NotNull MovecraftLocation loc) { + return new BlockPos(loc.getX(), loc.getY(), loc.getZ()); + } + + private void setBlockFast(@NotNull Level world, @NotNull BlockPos position, @NotNull BlockState data) { + LevelChunk chunk = world.getChunkAt(position); + int chunkSection = (position.getY() >> 4) - WorldUtil.getMinSection(world); + LevelChunkSection section = chunk.getSections()[chunkSection]; + if (section == null) { + // Put a GLASS block to initialize the section. It will be replaced next with the real block. + chunk.setBlockState(position, Blocks.GLASS.defaultBlockState(), 0); + section = chunk.getSections()[chunkSection]; + } + if (section.getBlockState(position.getX() & 15, position.getY() & 15, position.getZ() & 15).equals(data)) { + //Block is already of correct type and data, don't overwrite + return; + } + section.setBlockState(position.getX() & 15, position.getY() & 15, position.getZ() & 15, data); + world.sendBlockUpdated(position, data, data, 3); + world.getLightEngine().checkBlock(position); // boolean corresponds to if chunk section empty + chunk.markUnsaved(); + } + + @Override + public void setBlockFast(@NotNull Location location, @NotNull BlockData data) { + setBlockFast(location, MovecraftRotation.NONE, data); + } + + @Override + public void setBlockFast(@NotNull Location location, @NotNull MovecraftRotation rotation, @NotNull BlockData data) { + BlockState blockData; + if (data instanceof CraftBlockData) { + blockData = ((CraftBlockData) data).getState(); + } + else { + blockData = (BlockState) data; + } + blockData = blockData.rotate(ROTATION[rotation.ordinal()]); + Level world = ((CraftWorld) (location.getWorld())).getHandle(); + BlockPos BlockPos = locationToPosition(MathUtils.bukkit2MovecraftLoc(location)); + setBlockFast(world, BlockPos, blockData); + } + + private void moveBlockEntity(@NotNull Level nativeWorld, @NotNull BlockPos newPosition, @NotNull BlockEntity tile) { + LevelChunk chunk = nativeWorld.getChunkAt(newPosition); + try { + var positionField = BlockEntity.class.getDeclaredField("o"); // o is obfuscated worldPosition + UnsafeUtils.setField(positionField, tile, newPosition); + } + catch (NoSuchFieldException e) { + e.printStackTrace(); + } + tile.setLevel(nativeWorld); + tile.clearRemoved(); + if (nativeWorld.captureBlockStates) { + nativeWorld.capturedTileEntities.put(newPosition, tile); + return; + } + chunk.setBlockEntity(tile); + chunk.blockEntities.put(newPosition, tile); + } + + private static class TileHolder { + @NotNull + private final BlockEntity tile; + @NotNull + private final BlockPos tilePosition; + + public TileHolder(@NotNull BlockEntity tile, @NotNull BlockPos tilePosition) { + this.tile = tile; + this.tilePosition = tilePosition; + } + + + @NotNull + public BlockEntity getTile() { + return tile; + } + + @NotNull + public BlockPos getTilePosition() { + return tilePosition; + } + } + + private static class TickHolder { + @NotNull + private final ScheduledTick tick; + @NotNull + private final BlockPos tickPosition; + + public TickHolder(@NotNull ScheduledTick tick, @NotNull BlockPos tilePosition) { + this.tick = tick; + this.tickPosition = tilePosition; + } + + + @NotNull + public ScheduledTick getTick() { + return tick; + } + + @NotNull + public BlockPos getTickPosition() { + return tickPosition; + } + } +} \ No newline at end of file diff --git a/v1_21_10/src/main/java/net/countercraft/movecraft/compat/v1_21_10/NextTickProvider.java b/v1_21_10/src/main/java/net/countercraft/movecraft/compat/v1_21_10/NextTickProvider.java new file mode 100644 index 000000000..edcd3dc9c --- /dev/null +++ b/v1_21_10/src/main/java/net/countercraft/movecraft/compat/v1_21_10/NextTickProvider.java @@ -0,0 +1,39 @@ +package net.countercraft.movecraft.compat.v1_21_10; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.ticks.LevelChunkTicks; +import net.minecraft.world.ticks.ScheduledTick; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Stream; + +public class NextTickProvider { + + @Nullable + public ScheduledTick getNextTick(@NotNull ServerLevel world, @NotNull BlockPos position){ + LevelChunkTicks tickList = (LevelChunkTicks) world + .getChunk(position) + .getBlockTicks(); + + var box = BoundingBox.encapsulatingPositions(List.of(position)); + if(box.isEmpty()){ + return null; + } + + Stream> ticks = tickList.getAll(); + + for (var iter = ticks.iterator(); iter.hasNext(); ) { + var next = iter.next(); + if (!next.pos().equals(position)) { + continue; + } + return next; + } + return null; + } +} diff --git a/v1_21_10/src/main/java/net/countercraft/movecraft/support/v1_21_10/IAsyncChunk.java b/v1_21_10/src/main/java/net/countercraft/movecraft/support/v1_21_10/IAsyncChunk.java new file mode 100644 index 000000000..45f8b461f --- /dev/null +++ b/v1_21_10/src/main/java/net/countercraft/movecraft/support/v1_21_10/IAsyncChunk.java @@ -0,0 +1,61 @@ +package net.countercraft.movecraft.support.v1_21_10; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.processing.WorldManager; +import net.countercraft.movecraft.support.AsyncChunk; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.bukkit.Chunk; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public class IAsyncChunk extends AsyncChunk { + private final @NotNull LoadingCache stateCache = CacheBuilder.newBuilder().maximumSize(1000).build(new CacheLoader<>() { + @Override + public BlockState load(@NotNull MovecraftLocation movecraftLocation) { + var block = chunk.getBlock(movecraftLocation.getX(), movecraftLocation.getY(), movecraftLocation.getZ()); + return WorldManager.INSTANCE.executeMain(() -> block.getState()); + } + }); + + // getHandle needs to be access in the main thread as of 1.19.4 + private final ChunkAccess handle; + + public IAsyncChunk(@NotNull Chunk chunk) { + super(chunk); + handle = this.chunk.getHandle(ChunkStatus.FULL); + } + + @NotNull + @Override + protected CraftChunk adapt(@NotNull org.bukkit.Chunk chunk) { + return (CraftChunk) chunk; + } + + @NotNull + @Override + public BlockState getState(@NotNull MovecraftLocation location) { + return stateCache.getUnchecked(location); + } + + @Override + @NotNull + public Material getType(@NotNull MovecraftLocation location){ + return this.getData(location).getMaterial(); + } + + @Override + @NotNull + public BlockData getData(@NotNull MovecraftLocation location){ + return CraftBlockData.fromData(handle.getBlockState(new BlockPos(location.getX(), location.getY(), location.getZ()))); + } +} diff --git a/v1_21_10/src/main/java/net/countercraft/movecraft/support/v1_21_10/ISmoothTeleport.java b/v1_21_10/src/main/java/net/countercraft/movecraft/support/v1_21_10/ISmoothTeleport.java new file mode 100644 index 000000000..0390bc045 --- /dev/null +++ b/v1_21_10/src/main/java/net/countercraft/movecraft/support/v1_21_10/ISmoothTeleport.java @@ -0,0 +1,23 @@ +package net.countercraft.movecraft.support.v1_21_10; + +import io.papermc.paper.entity.TeleportFlag; +import net.countercraft.movecraft.SmoothTeleport; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class ISmoothTeleport extends SmoothTeleport { + public void teleport(@NotNull Player player, @NotNull Location location) { + player.teleport( + location, + TeleportFlag.Relative.VELOCITY_X,//x + TeleportFlag.Relative.VELOCITY_Y,//y + TeleportFlag.Relative.VELOCITY_Z,//z + TeleportFlag.Relative.VELOCITY_ROTATION,//pitch + TeleportFlag.Relative.VELOCITY_ROTATION,//yaw + TeleportFlag.EntityState.RETAIN_OPEN_INVENTORY, + TeleportFlag.EntityState.RETAIN_VEHICLE, + TeleportFlag.EntityState.RETAIN_PASSENGERS + ); + } +}