From 7eb884029a015d273418ed148f0c805ef4ffbde3 Mon Sep 17 00:00:00 2001 From: ohnoey Date: Sat, 14 Sep 2024 20:38:09 -0700 Subject: [PATCH 1/3] Extract affine changes to discreet branch --- Movecraft/src/main/resources/plugin.yml | 1 + api/build.gradle.kts | 1 + .../countercraft/movecraft/WorldHandler.java | 30 ++- .../movecraft/util/AffineTransformation.java | 35 +++ .../movecraft/util/hitboxes/HitBox.java | 8 + gradle/libs.versions.toml | 2 + .../movecraft/compat/v1_20/IWorldHandler.java | 237 ++++-------------- .../movecraft/compat/v1_21/IWorldHandler.java | 222 ++++------------ 8 files changed, 175 insertions(+), 361 deletions(-) create mode 100644 api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java diff --git a/Movecraft/src/main/resources/plugin.yml b/Movecraft/src/main/resources/plugin.yml index 02b88a40d..83e983ffc 100644 --- a/Movecraft/src/main/resources/plugin.yml +++ b/Movecraft/src/main/resources/plugin.yml @@ -42,3 +42,4 @@ commands: usage: /craftinfo [player] [page] libraries: - org.roaringbitmap:RoaringBitmap:1.0.6 + - org.ejml:ejml-simple:0.43 diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 77042e3d8..0c090bb6d 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api(libs.it.unimi.dsi.fastutil) api(libs.net.kyori.adventure.api) api(libs.net.kyori.adventure.platform.bukkit) + api(libs.org.ejml.simple.library) testImplementation(libs.org.junit.jupiter.junit.jupiter.api) testImplementation(libs.junit.junit) testImplementation(libs.org.hamcrest.hamcrest.library) diff --git a/api/src/main/java/net/countercraft/movecraft/WorldHandler.java b/api/src/main/java/net/countercraft/movecraft/WorldHandler.java index e0d19564c..d61e301fd 100644 --- a/api/src/main/java/net/countercraft/movecraft/WorldHandler.java +++ b/api/src/main/java/net/countercraft/movecraft/WorldHandler.java @@ -1,6 +1,8 @@ package net.countercraft.movecraft; import net.countercraft.movecraft.craft.Craft; +import net.countercraft.movecraft.util.AffineTransformation; +import net.countercraft.movecraft.util.hitboxes.HitBox; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -11,11 +13,33 @@ import org.jetbrains.annotations.Nullable; public abstract class WorldHandler { - public abstract void rotateCraft(@NotNull Craft craft, @NotNull MovecraftLocation originLocation, @NotNull MovecraftRotation rotation); - public abstract void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation newLocation, @NotNull World world); + @Deprecated(forRemoval = true) + public void rotateCraft(@NotNull Craft craft, @NotNull MovecraftLocation originLocation, @NotNull MovecraftRotation rotation){ + transformHitBox( + craft.getHitBox(), + AffineTransformation.of(originLocation) + .mult(AffineTransformation.of(rotation)) + .mult(AffineTransformation.of(originLocation.scalarMultiply(-1))), + craft.getWorld(), + craft.getWorld()); + } + + @Deprecated(forRemoval = true) + public void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation displacement, @NotNull World world){ + transformHitBox(craft.getHitBox(), AffineTransformation.of(displacement), craft.getWorld(), world); + } + public abstract void transformHitBox(@NotNull HitBox hitbox, @NotNull AffineTransformation transformation, @NotNull World originWorld, @NotNull World destinationWorld); public abstract void setBlockFast(@NotNull Location location, @NotNull BlockData data); public abstract void setBlockFast(@NotNull Location location, @NotNull MovecraftRotation rotation, @NotNull BlockData data); - + @Deprecated(forRemoval = true) + public @Nullable Location getAccessLocation(@NotNull InventoryView inventoryView){ + // Not needed for 1.20+, remove when dropping support for 1.18.2 + return null; + } + @Deprecated(forRemoval = true) + public void setAccessLocation(@NotNull InventoryView inventoryView, @NotNull Location location){ + // Not needed for 1.20+, remove when dropping support for 1.18.2 + } public static @NotNull String getPackageName(@NotNull String minecraftVersion) { String[] parts = minecraftVersion.split("\\."); if (parts.length < 2) diff --git a/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java b/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java new file mode 100644 index 000000000..25987116c --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java @@ -0,0 +1,35 @@ +package net.countercraft.movecraft.util; + +import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.MovecraftRotation; +import org.bukkit.block.structure.Mirror; +import org.ejml.simple.SimpleMatrix; +import org.jetbrains.annotations.NotNull; + +// TODO: Implement with 4d matrix +public record AffineTransformation(SimpleMatrix backingMatrix){ + public static @NotNull AffineTransformation NONE = new AffineTransformation(SimpleMatrix.identity(4)); + public static @NotNull AffineTransformation of(MovecraftLocation translation){ + var ret = SimpleMatrix.identity(4); + ret.set(3, 0, translation.getX()); + ret.set(3, 1, translation.getY()); + ret.set(3, 2, translation.getZ()); + + return new AffineTransformation(ret); + } + public static @NotNull AffineTransformation of(MovecraftRotation rotation){ + + return null; + } + public @NotNull AffineTransformation mult(AffineTransformation other){ + return new AffineTransformation(backingMatrix.mult(other.backingMatrix)); + } + + public @NotNull MovecraftLocation apply(MovecraftLocation location){ + var transformed = backingMatrix.mult(new SimpleMatrix(new double[]{location.getX(), location.getY(), location.getZ(), 1})); + + return new MovecraftLocation((int) transformed.get(0), (int) transformed.get(1), (int) transformed.get(2)); + } + public @NotNull MovecraftRotation extractRotation(){ return null; } + public @NotNull Mirror extractMirror(){ return null; } +} diff --git a/api/src/main/java/net/countercraft/movecraft/util/hitboxes/HitBox.java b/api/src/main/java/net/countercraft/movecraft/util/hitboxes/HitBox.java index 14ba27760..25a7fbfca 100644 --- a/api/src/main/java/net/countercraft/movecraft/util/hitboxes/HitBox.java +++ b/api/src/main/java/net/countercraft/movecraft/util/hitboxes/HitBox.java @@ -4,12 +4,14 @@ import com.google.common.collect.UnmodifiableIterator; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.exception.EmptyHitBoxException; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import java.util.AbstractSet; import java.util.Collection; import java.util.Iterator; import java.util.Set; +import java.util.stream.Stream; public interface HitBox extends Iterable{ int getMinX(); @@ -94,6 +96,12 @@ default Set asSet(){ return new HitBoxSetView(this); } + @NotNull + @Contract(pure = true) + default Stream stream(){ + return asSet().stream(); + } + @NotNull HitBox difference(HitBox other); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d65f94ec..75a656f28 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ it-unimi-dsi-fastutil = "8.5.13" junit-junit = "4.13.2" net-kyori-adventure-api = "4.17.0" net-kyori-adventure-platform-bukkit = "4.3.2" +org-ejml-library = "0.42" org-hamcrest-hamcrest-library = "1.3" org-jetbrains-annotations = "24.1.0" org-junit-jupiter-junit-jupiter-api = "5.10.2" @@ -21,6 +22,7 @@ it-unimi-dsi-fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "it-un junit-junit = { module = "junit:junit", version.ref = "junit-junit" } net-kyori-adventure-api = { module = "net.kyori:adventure-api", version.ref = "net-kyori-adventure-api" } net-kyori-adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "net-kyori-adventure-platform-bukkit" } +org-ejml-simple-library = { module = "org.ejml:ejml-simple", version.ref = "org-ejml-library" } org-hamcrest-hamcrest-library = { module = "org.hamcrest:hamcrest-library", version.ref = "org-hamcrest-hamcrest-library" } org-jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "org-jetbrains-annotations" } org-junit-jupiter-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "org-junit-jupiter-junit-jupiter-api" } diff --git a/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java b/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java index ae8d6a930..1d0aa8d73 100644 --- a/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java +++ b/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java @@ -3,59 +3,51 @@ 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.AffineTransformation; import net.countercraft.movecraft.util.CollectionUtils; import net.countercraft.movecraft.util.MathUtils; import net.countercraft.movecraft.util.UnsafeUtils; +import net.countercraft.movecraft.util.hitboxes.HitBox; import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.ContainerLevelAccess; -import net.minecraft.world.level.BlockEventData; 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.DirectionalBlock; +import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.BlockEntityTicker; -import net.minecraft.world.level.block.piston.PistonBaseBlock; -import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; -import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.properties.DirectionProperty; 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.World; import org.bukkit.block.data.BlockData; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; -import org.bukkit.craftbukkit.inventory.CraftInventoryView; -import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.lang.reflect.Field; 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; +import java.util.stream.Collectors; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "ForLoopReplaceableByForEach"}) public class IWorldHandler extends WorldHandler { - private static final Rotation ROTATION[]; + private static final Rotation[] ROTATION = new Rotation[3]; + private static final Mirror[] MIRROR = new Mirror[3]; 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; + + MIRROR[org.bukkit.block.structure.Mirror.NONE.ordinal()] = Mirror.NONE; + MIRROR[org.bukkit.block.structure.Mirror.LEFT_RIGHT.ordinal()] = Mirror.LEFT_RIGHT; + MIRROR[org.bukkit.block.structure.Mirror.FRONT_BACK.ordinal()] = Mirror.FRONT_BACK; } private final NextTickProvider tickProvider = new NextTickProvider(); @@ -67,92 +59,17 @@ public IWorldHandler() { } @Override - public void rotateCraft(@NotNull Craft craft, @NotNull MovecraftLocation originPoint, @NotNull MovecraftRotation rotation) { + public void transformHitBox(@NotNull HitBox hitbox, @NotNull AffineTransformation transformation, @NotNull World originWorld, @NotNull World destinationWorld) { //******************************************* //* 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 = 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(); + List positions = hitbox + .stream() + .map(transformation::apply) + .map(this::locationToPosition) + .collect(Collectors.toList()); + ServerLevel originLevel = ((CraftWorld) originWorld).getHandle(); + ServerLevel destinationLevel = ((CraftWorld) destinationWorld).getHandle(); //******************************************* //* Step two: Get the tiles * //******************************************* @@ -161,28 +78,23 @@ public void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation disp //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()) + if (originLevel.getBlockState(position) == Blocks.AIR.defaultBlockState()) continue; - - BlockEntity tile = removeBlockEntity(oldNativeWorld, position); + BlockEntity tile = removeBlockEntity(originLevel, position); if (tile != null) tiles.add(new TileHolder(tile,position)); //get the nextTick to move with the tile - ScheduledTick tickHere = tickProvider.getNextTick(nativeWorld, position); - while (tickHere != null) { - ScheduledTick tickToRemove = tickHere; - ((LevelChunkTicks) nativeWorld.getChunkAt(position).getBlockTicks()).removeIf( - (Predicate) scheduledTick -> scheduledTick.equals(tickToRemove)); + ScheduledTick tickHere = tickProvider.getNextTick(destinationLevel, position); + if (tickHere != null) { + var levelTicks = ((LevelChunkTicks) destinationLevel.getChunkAt(position).getBlockTicks()); + levelTicks.removeIf(tickHere::equals); ticks.add(new TickHolder(tickHere, position)); - tickHere = tickProvider.getNextTick(nativeWorld, position); } - } //******************************************* - //* Step three: Translate all the blocks * + //* Step three: Transform all the blocks * //******************************************* - // blockedByWater=false means an ocean-going vessel //TODO: Simplify //TODO: go by chunks //TODO: Don't move unnecessary blocks @@ -191,63 +103,57 @@ public void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation disp 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)); + blockData.add(locallyTransformState(transformation, originLevel.getBlockState(position))); + newPositions.add(transformPosition(transformation, position)); } //create the new block for (int i = 0, positionSize = newPositions.size(); i < positionSize; i++) { - setBlockFast(nativeWorld, newPositions.get(i), blockData.get(i)); + setBlockFast(destinationLevel, 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()); + moveBlockEntity(destinationLevel, transformPosition(transformation, tileHolder.tilePosition()), tileHolder.tile()); 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())); + final long currentTime = destinationLevel.getGameTime(); + destinationLevel.getBlockTicks().schedule(new ScheduledTick<>((Block) tickHolder.tick().type(), transformPosition(transformation, tickHolder.tickPosition()), tickHolder.tick().triggerTick() - currentTime, tickHolder.tick().priority(), tickHolder.tick().subTickOrder())); } //******************************************* //* Step five: Destroy the leftovers * //******************************************* List deletePositions = positions; - if (oldNativeWorld == nativeWorld) + if (originLevel == destinationLevel) 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()); + setBlockFast(originLevel, 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 BlockState locallyTransformState(@NotNull AffineTransformation transformation, @NotNull BlockState state){ + return state + .rotate(ROTATION[transformation.extractRotation().ordinal()]) + .mirror(MIRROR[transformation.extractMirror().ordinal()]); + } + @NotNull private BlockPos locationToPosition(@NotNull MovecraftLocation loc) { return new BlockPos(loc.getX(), loc.getY(), loc.getZ()); } + @NotNull @Contract(pure = true) + private BlockPos transformPosition(@NotNull AffineTransformation transformation, @NotNull BlockPos pos){ + return locationToPosition(transformation.apply(new MovecraftLocation(pos.getX(), pos.getY(), pos.getZ()))); + } + private void setBlockFast(@NotNull Level world, @NotNull BlockPos position, @NotNull BlockState data) { LevelChunk chunk = world.getChunkAt(position); int chunkSection = (position.getY() >> 4) - chunk.getMinSection(); @@ -290,6 +196,7 @@ public void setBlockFast(@NotNull Location location, @NotNull MovecraftRotation private void moveBlockEntity(@NotNull Level nativeWorld, @NotNull BlockPos newPosition, @NotNull BlockEntity tile) { LevelChunk chunk = nativeWorld.getChunkAt(newPosition); try { + //noinspection JavaReflectionMemberAccess var positionField = BlockEntity.class.getDeclaredField("o"); // o is obfuscated worldPosition UnsafeUtils.setField(positionField, tile, newPosition); } @@ -306,49 +213,7 @@ private void moveBlockEntity(@NotNull Level nativeWorld, @NotNull BlockPos newPo 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 record TileHolder(@NotNull BlockEntity tile, @NotNull BlockPos 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; - } - } + private record TickHolder(@NotNull ScheduledTick tick, @NotNull BlockPos tickPosition) { } } \ No newline at end of file diff --git a/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java b/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java index d4e3cba4b..2dc197e80 100644 --- a/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java +++ b/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java @@ -3,19 +3,19 @@ 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.AffineTransformation; import net.countercraft.movecraft.util.CollectionUtils; import net.countercraft.movecraft.util.MathUtils; import net.countercraft.movecraft.util.UnsafeUtils; +import net.countercraft.movecraft.util.hitboxes.HitBox; 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.Mirror; 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; @@ -23,28 +23,31 @@ import net.minecraft.world.ticks.ScheduledTick; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.block.data.BlockData; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.jetbrains.annotations.Contract; 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; +import java.util.stream.Collectors; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "ForLoopReplaceableByForEach"}) public class IWorldHandler extends WorldHandler { - private static final Rotation ROTATION[]; + private static final Rotation[] ROTATION = new Rotation[3]; + private static final Mirror[] MIRROR = new Mirror[3]; 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; + + MIRROR[org.bukkit.block.structure.Mirror.NONE.ordinal()] = Mirror.NONE; + MIRROR[org.bukkit.block.structure.Mirror.LEFT_RIGHT.ordinal()] = Mirror.LEFT_RIGHT; + MIRROR[org.bukkit.block.structure.Mirror.FRONT_BACK.ordinal()] = Mirror.FRONT_BACK; } private final NextTickProvider tickProvider = new NextTickProvider(); @@ -56,93 +59,17 @@ public IWorldHandler() { } @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 + public void transformHitBox(@NotNull HitBox hitbox, @NotNull AffineTransformation transformation, @NotNull World originWorld, @NotNull World destinationWorld) { //******************************************* //* 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(); + List positions = hitbox + .stream() + .map(transformation::apply) + .map(this::locationToPosition) + .collect(Collectors.toList()); + ServerLevel originLevel = ((CraftWorld) originWorld).getHandle(); + ServerLevel destinationLevel = ((CraftWorld) destinationWorld).getHandle(); //******************************************* //* Step two: Get the tiles * //******************************************* @@ -151,25 +78,23 @@ public void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation disp //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()) + if (originLevel.getBlockState(position) == Blocks.AIR.defaultBlockState()) continue; - //BlockEntity tile = nativeWorld.removeBlockEntity(position); - BlockEntity tile = removeBlockEntity(oldNativeWorld, position); + BlockEntity tile = removeBlockEntity(originLevel, position); if (tile != null) tiles.add(new TileHolder(tile,position)); //get the nextTick to move with the tile - ScheduledTick tickHere = tickProvider.getNextTick(nativeWorld, position); + ScheduledTick tickHere = tickProvider.getNextTick(destinationLevel, position); if (tickHere != null) { - ((LevelChunkTicks) nativeWorld.getChunkAt(position).getBlockTicks()).removeIf( - (Predicate) scheduledTick -> scheduledTick.equals(tickHere)); + var levelTicks = ((LevelChunkTicks) destinationLevel.getChunkAt(position).getBlockTicks()); + levelTicks.removeIf(tickHere::equals); ticks.add(new TickHolder(tickHere, position)); } } //******************************************* - //* Step three: Translate all the blocks * + //* Step three: Transform all the blocks * //******************************************* - // blockedByWater=false means an ocean-going vessel //TODO: Simplify //TODO: go by chunks //TODO: Don't move unnecessary blocks @@ -178,55 +103,37 @@ public void translateCraft(@NotNull Craft craft, @NotNull MovecraftLocation disp 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)); + blockData.add(locallyTransformState(transformation, originLevel.getBlockState(position))); + newPositions.add(transformPosition(transformation, position)); } //create the new block for (int i = 0, positionSize = newPositions.size(); i < positionSize; i++) { - setBlockFast(nativeWorld, newPositions.get(i), blockData.get(i)); + setBlockFast(destinationLevel, 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()); + moveBlockEntity(destinationLevel, transformPosition(transformation, tileHolder.tilePosition()), tileHolder.tile()); 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())); + final long currentTime = destinationLevel.getGameTime(); + destinationLevel.getBlockTicks().schedule(new ScheduledTick<>((Block) tickHolder.tick().type(), transformPosition(transformation, tickHolder.tickPosition()), tickHolder.tick().triggerTick() - currentTime, tickHolder.tick().priority(), tickHolder.tick().subTickOrder())); } //******************************************* //* Step five: Destroy the leftovers * //******************************************* List deletePositions = positions; - if (oldNativeWorld == nativeWorld) + if (originLevel == destinationLevel) 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()); + setBlockFast(originLevel, 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); } @@ -235,6 +142,18 @@ private BlockPos locationToPosition(@NotNull MovecraftLocation loc) { return new BlockPos(loc.getX(), loc.getY(), loc.getZ()); } + @NotNull @Contract(pure = true) + private BlockPos transformPosition(@NotNull AffineTransformation transformation, @NotNull BlockPos pos){ + return locationToPosition(transformation.apply(new MovecraftLocation(pos.getX(), pos.getY(), pos.getZ()))); + } + + @NotNull + private BlockState locallyTransformState(@NotNull AffineTransformation transformation, @NotNull BlockState state){ + return state + .rotate(ROTATION[transformation.extractRotation().ordinal()]) + .mirror(MIRROR[transformation.extractMirror().ordinal()]); + } + private void setBlockFast(@NotNull Level world, @NotNull BlockPos position, @NotNull BlockState data) { LevelChunk chunk = world.getChunkAt(position); int chunkSection = (position.getY() >> 4) - chunk.getMinSection(); @@ -277,6 +196,7 @@ public void setBlockFast(@NotNull Location location, @NotNull MovecraftRotation private void moveBlockEntity(@NotNull Level nativeWorld, @NotNull BlockPos newPosition, @NotNull BlockEntity tile) { LevelChunk chunk = nativeWorld.getChunkAt(newPosition); try { + //noinspection JavaReflectionMemberAccess var positionField = BlockEntity.class.getDeclaredField("o"); // o is obfuscated worldPosition UnsafeUtils.setField(positionField, tile, newPosition); } @@ -293,49 +213,7 @@ private void moveBlockEntity(@NotNull Level nativeWorld, @NotNull BlockPos newPo chunk.blockEntities.put(newPosition, tile); } - private static class TileHolder { - @NotNull - private final BlockEntity tile; - @NotNull - private final BlockPos tilePosition; + private record TileHolder(@NotNull BlockEntity tile, @NotNull 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; - } - } + private record TickHolder(@NotNull ScheduledTick tick, @NotNull BlockPos tickPosition) { } } \ No newline at end of file From 92aa6c71e0964b61572a7b42acc4e36754f4d6c7 Mon Sep 17 00:00:00 2001 From: ohnoey Date: Sat, 14 Sep 2024 20:41:28 -0700 Subject: [PATCH 2/3] re-add upstream piston fixes --- .../movecraft/compat/v1_20/IWorldHandler.java | 20 +++++++++++++++++++ .../movecraft/compat/v1_21/IWorldHandler.java | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java b/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java index 1d0aa8d73..04c0b4342 100644 --- a/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java +++ b/v1_20/src/main/java/net/countercraft/movecraft/compat/v1_20/IWorldHandler.java @@ -16,6 +16,8 @@ import net.minecraft.world.level.block.Mirror; 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; @@ -134,6 +136,24 @@ public void transformHitBox(@NotNull HitBox hitbox, @NotNull AffineTransformatio @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); } diff --git a/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java b/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java index 2dc197e80..63749209a 100644 --- a/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java +++ b/v1_21/src/main/java/net/countercraft/movecraft/compat/v1_21/IWorldHandler.java @@ -16,6 +16,8 @@ import net.minecraft.world.level.block.Mirror; 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; @@ -134,6 +136,24 @@ public void transformHitBox(@NotNull HitBox hitbox, @NotNull AffineTransformatio @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); } From c80d7325fa5d43df77c0df492ca0b10244f52c8f Mon Sep 17 00:00:00 2001 From: ohnoey Date: Sat, 14 Sep 2024 21:11:34 -0700 Subject: [PATCH 3/3] Implement transform --- .../movecraft/util/AffineTransformation.java | 84 ++++++++++++++++--- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java b/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java index 25987116c..53087bf59 100644 --- a/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java +++ b/api/src/main/java/net/countercraft/movecraft/util/AffineTransformation.java @@ -6,30 +6,90 @@ import org.ejml.simple.SimpleMatrix; import org.jetbrains.annotations.NotNull; -// TODO: Implement with 4d matrix -public record AffineTransformation(SimpleMatrix backingMatrix){ - public static @NotNull AffineTransformation NONE = new AffineTransformation(SimpleMatrix.identity(4)); - public static @NotNull AffineTransformation of(MovecraftLocation translation){ +import java.util.Objects; + +public final class AffineTransformation { + public static @NotNull AffineTransformation UNIT = new AffineTransformation(SimpleMatrix.identity(4), MovecraftRotation.NONE); + private final SimpleMatrix backingMatrix; + private final MovecraftRotation rotation; + + private AffineTransformation(SimpleMatrix backingMatrix, MovecraftRotation rotation) { + this.backingMatrix = backingMatrix; + this.rotation = rotation; + } + + public static @NotNull AffineTransformation of(MovecraftLocation translation) { var ret = SimpleMatrix.identity(4); ret.set(3, 0, translation.getX()); ret.set(3, 1, translation.getY()); ret.set(3, 2, translation.getZ()); - return new AffineTransformation(ret); + return new AffineTransformation(ret, MovecraftRotation.NONE); } - public static @NotNull AffineTransformation of(MovecraftRotation rotation){ - return null; + public static @NotNull AffineTransformation of(MovecraftRotation rotation) { + var ret = SimpleMatrix.identity(4); + switch (rotation) { + case NONE: + break; + case CLOCKWISE: + ret.set(0, 0, 0); + ret.set(1, 1, 0); + ret.set(0, 1, -1); + ret.set(1, 0, 1); + break; + case ANTICLOCKWISE: + ret.set(0, 0, 0); + ret.set(1, 1, 0); + ret.set(0, 1, 1); + ret.set(1, 0, -1); + break; + } + + return new AffineTransformation(ret, rotation); } - public @NotNull AffineTransformation mult(AffineTransformation other){ - return new AffineTransformation(backingMatrix.mult(other.backingMatrix)); + + public @NotNull AffineTransformation mult(AffineTransformation other) { + // Currently, MovecraftRotation does not support 180 degree rotations + // To work around this, we simply prefer the other transformations rotation + // TODO: Implement 180 degree MovecraftRotation + + return new AffineTransformation(backingMatrix.mult(other.backingMatrix), other.rotation == MovecraftRotation.NONE ? rotation : other.rotation); } - public @NotNull MovecraftLocation apply(MovecraftLocation location){ + public @NotNull MovecraftLocation apply(MovecraftLocation location) { var transformed = backingMatrix.mult(new SimpleMatrix(new double[]{location.getX(), location.getY(), location.getZ(), 1})); return new MovecraftLocation((int) transformed.get(0), (int) transformed.get(1), (int) transformed.get(2)); } - public @NotNull MovecraftRotation extractRotation(){ return null; } - public @NotNull Mirror extractMirror(){ return null; } + + public @NotNull MovecraftRotation extractRotation() { + return rotation; + } + + public @NotNull Mirror extractMirror() { + return Mirror.NONE; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (AffineTransformation) obj; + return Objects.equals(this.backingMatrix, that.backingMatrix) && + Objects.equals(this.rotation, that.rotation); + } + + @Override + public int hashCode() { + return Objects.hash(backingMatrix, rotation); + } + + @Override + public String toString() { + return "AffineTransformation[" + + "backingMatrix=" + backingMatrix + ", " + + "rotation=" + rotation + ']'; + } + }