diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 2e5671e61..82cb6dc21 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -7,6 +7,8 @@ plugins { dependencies { implementation(rootProject) + // https://mvnrepository.com/artifact/org.jctools/jctools-core + implementation("org.jctools:jctools-core:4.0.3") runtimeOnly(libs.bundles.logback) } diff --git a/demo/src/main/java/net/minestom/scratch/Scratch.java b/demo/src/main/java/net/minestom/scratch/Scratch.java new file mode 100644 index 000000000..e3afc48ea --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/Scratch.java @@ -0,0 +1,515 @@ +package net.minestom.scratch; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.minestom.scratch.tools.ScratchBlockTools.BlockHolder; +import net.minestom.scratch.tools.ScratchFeature; +import net.minestom.scratch.tools.ScratchNetworkTools.NetworkContext; +import net.minestom.scratch.tools.ScratchVelocityTools; +import net.minestom.server.ServerFlag; +import net.minestom.server.collision.Aerodynamics; +import net.minestom.server.collision.PhysicsResult; +import net.minestom.server.collision.PhysicsUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.GameMode; +import net.minestom.server.instance.WorldBorder; +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.packet.client.ClientPacket; +import net.minestom.server.network.packet.client.common.ClientPingRequestPacket; +import net.minestom.server.network.packet.client.configuration.ClientFinishConfigurationPacket; +import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket; +import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; +import net.minestom.server.network.packet.client.status.StatusRequestPacket; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.network.packet.server.common.KeepAlivePacket; +import net.minestom.server.network.packet.server.common.PingResponsePacket; +import net.minestom.server.network.packet.server.configuration.FinishConfigurationPacket; +import net.minestom.server.network.packet.server.login.LoginSuccessPacket; +import net.minestom.server.network.packet.server.play.*; +import net.minestom.server.network.packet.server.play.data.WorldPos; +import net.minestom.server.network.packet.server.status.ResponsePacket; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.world.DimensionType; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static net.minestom.scratch.tools.ScratchTools.REGISTRY_DATA_PACKET; +import static net.minestom.scratch.tools.ScratchViewTools.Broadcaster; +import static net.minestom.scratch.tools.ScratchViewTools.Synchronizer; + +/** + * Example of the minestom API used to create a server from scratch. + *

+ * Tools are used to ease the implementation. + */ +public final class Scratch { + private static final SocketAddress ADDRESS = new InetSocketAddress("0.0.0.0", 25565); + private static final int VIEW_DISTANCE = 8; + + public static void main(String[] args) throws Exception { + new Scratch(); + } + + private final AtomicInteger lastEntityId = new AtomicInteger(); + private final AtomicBoolean stop = new AtomicBoolean(false); + private final ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.INET); + private final ConcurrentLinkedQueue waitingPlayers = new ConcurrentLinkedQueue<>(); + + private final Instance instance = new Instance(DimensionType.OVERWORLD, new BlockHolder(DimensionType.OVERWORLD)); + private final Map players = new HashMap<>(); + private final Map entities = new HashMap<>(); + + Scratch() throws Exception { + server.bind(ADDRESS); + System.out.println("Server started on: " + ADDRESS); + Thread.startVirtualThread(this::listenCommands); + Thread.startVirtualThread(this::listenConnections); + ticks(); + server.close(); + System.out.println("Server stopped"); + } + + void listenCommands() { + Scanner scanner = new Scanner(System.in); + while (serverRunning()) { + final String line = scanner.nextLine(); + if (line.equals("stop")) { + stop.set(true); + System.out.println("Stopping server..."); + } else if (line.equals("gc")) { + System.gc(); + } + } + } + + void listenConnections() { + while (serverRunning()) { + try { + final SocketChannel client = server.accept(); + System.out.println("Accepted connection from " + client.getRemoteAddress()); + Connection connection = new Connection(client); + Thread.startVirtualThread(connection::networkLoopRead); + Thread.startVirtualThread(connection::networkLoopWrite); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + void ticks() { + { + var entity = new Entity(EntityType.ZOMBIE, instance, new Pos(0, 60, 0)); + entities.put(entity.id, entity); + } + int keepAliveId = 0; + while (serverRunning()) { + final long time = System.nanoTime(); + // Connect waiting players + PlayerInfo playerInfo; + while ((playerInfo = waitingPlayers.poll()) != null) { + final Player player = new Player(playerInfo, instance, new Pos(0, 55, 0)); + this.players.put(player.id, player); + } + // Tick playing players + List toRemove = new ArrayList<>(); + final boolean sendKeepAlive = keepAliveId++ % (20 * 20) == 0; + for (Player player : players.values()) { + if (sendKeepAlive) player.connection.networkContext.write(new KeepAlivePacket(keepAliveId)); + if (!player.tick()) toRemove.add(player); + } + // Tick entities + for (Entity entity : entities.values()) { + entity.tick(); + } + // Remove disconnected players + for (Player player : toRemove) { + this.players.remove(player.id); + var synchronizerEntry = player.synchronizerEntry; + if (synchronizerEntry != null) synchronizerEntry.unmake(); + } + // Compute broadcast packets + try (Broadcaster.Collector collector = instance.broadcaster.collector()) { + final List packets = collector.packets(); + if (!packets.isEmpty()) { + for (Player player : players.values()) { + final int[] exception = collector.exception(player.id); + player.connection.networkContext.write(new NetworkContext.Packet.PlayList(packets, exception)); + } + } + } + // Compute view packets + this.instance.synchronizer.computePackets((playerId, packet) -> { + final Player player = players.get(playerId); + if (player != null) player.connection.networkContext.write(packet); + }); + { + final long heapUsage = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final double elapsed = (System.nanoTime() - time) / 1_000_000.0; + final PlayerListHeaderAndFooterPacket packet = new PlayerListHeaderAndFooterPacket( + Component.text("Welcome to Minestom!"), + Component.text("Tick: " + String.format("%.2f", elapsed) + "ms") + .append(Component.newline()) + .append(Component.text("Heap: " + heapUsage / 1024 / 1024 + "MB")) + ); + players.values().forEach(player -> player.connection.networkContext.write(packet)); + } + // Flush all connections + for (Player player : players.values()) { + player.connection.networkContext.flush(); + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + boolean serverRunning() { + return !stop.get(); + } + + static final class Instance { + final DimensionType dimensionType; + final BlockHolder blockHolder; + final Broadcaster broadcaster = new Broadcaster(); + final Synchronizer synchronizer = new Synchronizer(VIEW_DISTANCE); + + public Instance(DimensionType dimensionType, + BlockHolder blockHolder) { + this.dimensionType = dimensionType; + this.blockHolder = blockHolder; + } + } + + final class Connection { + final SocketChannel client; + final NetworkContext.Async networkContext = new NetworkContext.Async(); + final ConcurrentLinkedQueue packetQueue = new ConcurrentLinkedQueue<>(); + volatile boolean online = true; + + final AtomicReference nameRef = new AtomicReference<>(); + final AtomicReference uuidRef = new AtomicReference<>(); + + Connection(SocketChannel client) { + this.client = client; + } + + void networkLoopRead() { + while (online) { + this.online = this.networkContext.read(buffer -> { + try { + return client.read(buffer); + } catch (IOException e) { + return -1; + } + }, this::handleAsyncPacket); + } + } + + void networkLoopWrite() { + while (online) { + this.online = this.networkContext.write(buffer -> { + try { + return client.write(buffer.flip()); + } catch (IOException e) { + return -1; + } + }); + } + } + + void handleAsyncPacket(ClientPacket packet) { + if (packet instanceof ClientFinishConfigurationPacket) { + waitingPlayers.offer(new PlayerInfo(this, nameRef.get(), uuidRef.get())); + return; + } + if (networkContext.state() == ConnectionState.PLAY) { + packetQueue.add(packet); + return; + } + switch (packet) { + case StatusRequestPacket ignored -> { + this.networkContext.write(new ResponsePacket(""" + { + "version": { + "name": "1.20.4", + "protocol": 765 + }, + "players": { + "max": 100, + "online": 0 + }, + "description": { + "text": "Awesome Minestom" + }, + "enforcesSecureChat": false, + "previewsChat": false + } + """)); + } + case ClientPingRequestPacket pingRequestPacket -> { + this.networkContext.write(new PingResponsePacket(pingRequestPacket.number())); + } + case ClientLoginStartPacket startPacket -> { + nameRef.set(startPacket.username()); + uuidRef.set(UUID.randomUUID()); + this.networkContext.write(new LoginSuccessPacket(startPacket.profileId(), startPacket.username(), 0)); + } + case ClientLoginAcknowledgedPacket ignored -> { + this.networkContext.write(REGISTRY_DATA_PACKET); + this.networkContext.write(new FinishConfigurationPacket()); + } + default -> { + } + } + this.networkContext.flush(); + } + } + + record PlayerInfo(Connection connection, String username, UUID uuid) { + } + + final class Entity { + private final int id = lastEntityId.incrementAndGet(); + private final UUID uuid = UUID.randomUUID(); + final EntityType type; + final Instance instance; + final Synchronizer.Entry synchronizerEntry; + + final Aerodynamics aerodynamics; + + Pos position; + Vec velocity = Vec.ZERO; + boolean onGround; + + Entity(EntityType type, Instance instance, Pos position) { + this.type = type; + this.instance = instance; + this.position = position; + this.synchronizerEntry = instance.synchronizer.makeEntry(false, id, position, + () -> { + final var spawnPacket = new SpawnEntityPacket( + id, uuid, type.id(), + this.position, 0, 0, (short) 0, (short) 0, (short) 0 + ); + return List.of(spawnPacket); + }, + () -> List.of(new DestroyEntitiesPacket(id))); + + this.aerodynamics = new Aerodynamics(type.registry().acceleration(), 0.91, 1 - type.registry().drag()); + } + + void tick() { + var worldBorder = new WorldBorder(null); + PhysicsResult physicsResult = PhysicsUtils.simulateMovement(position, velocity, type.registry().boundingBox(), + worldBorder, instance.blockHolder, aerodynamics, false, true, onGround, false); + + this.position = physicsResult.newPosition(); + velocity = physicsResult.newVelocity(); + onGround = physicsResult.isOnGround(); + + synchronizerEntry.move(physicsResult.newPosition()); + synchronizerEntry.signal(new EntityTeleportPacket(id, physicsResult.newPosition(), onGround)); + if (!velocity.isZero()) + synchronizerEntry.signal(new EntityVelocityPacket(id, velocity.mul(8000f / ServerFlag.SERVER_TICKS_PER_SECOND))); + } + } + + final class Player { + private final int id = lastEntityId.incrementAndGet(); + private final Connection connection; + private final String username; + private final UUID uuid; + + final Instance instance; + final Synchronizer.Entry synchronizerEntry; + Pos position; + Pos oldPosition; + + final ScratchFeature.Messaging messaging; + final ScratchFeature.Movement movement; + final ScratchFeature.ChunkLoading chunkLoading; + final ScratchFeature.EntityInteract entityInteract; + final ScratchFeature.BlockInteract blockInteract; + + Player(PlayerInfo info, Instance spawnInstance, Pos spawnPosition) { + this.connection = info.connection; + this.username = info.username; + this.uuid = info.uuid; + + this.instance = spawnInstance; + this.position = spawnPosition; + this.oldPosition = spawnPosition; + + this.synchronizerEntry = instance.synchronizer.makeEntry(true, id, position, + () -> { + final var spawnPacket = new SpawnEntityPacket( + id, uuid, EntityType.PLAYER.id(), + this.position, 0, 0, (short) 0, (short) 0, (short) 0 + ); + return List.of(getAddPlayerToList(), spawnPacket); + }, + () -> List.of(new DestroyEntitiesPacket(id))); + + this.messaging = new ScratchFeature.Messaging(new ScratchFeature.Messaging.Mapping() { + @Override + public Component formatMessage(String message) { + return Component.text(username).color(TextColor.color(0x00FF00)) + .append(Component.text(" > ")) + .append(Component.text(message)); + } + + @Override + public void signal(ServerPacket.Play packet) { + instance.broadcaster.append(packet); + } + }); + + this.movement = new ScratchFeature.Movement(new ScratchFeature.Movement.Mapping() { + @Override + public int id() { + return id; + } + + @Override + public Pos position() { + return position; + } + + @Override + public void updatePosition(Pos position) { + Player.this.position = position; + synchronizerEntry.move(position); + } + + @Override + public void signalMovement(ServerPacket.Play packet) { + synchronizerEntry.signal(packet); + } + }); + + this.chunkLoading = new ScratchFeature.ChunkLoading(new ScratchFeature.ChunkLoading.Mapping() { + @Override + public int viewDistance() { + return VIEW_DISTANCE; + } + + @Override + public Pos oldPosition() { + return oldPosition; + } + + @Override + public ChunkDataPacket chunkPacket(int chunkX, int chunkZ) { + return instance.blockHolder.generatePacket(chunkX, chunkZ); + } + + @Override + public void sendPacket(ServerPacket.Play packet) { + Player.this.connection.networkContext.write(packet); + } + }); + + this.entityInteract = new ScratchFeature.EntityInteract(new ScratchFeature.EntityInteract.Mapping() { + @Override + public void left(int id) { + Entity entity = entities.get(id); + if (entity == null) return; + entity.velocity = ScratchVelocityTools.knockback(position, 0.4f, entity.velocity, entity.onGround); + } + + @Override + public void right(int id) { + } + }); + + this.blockInteract = new ScratchFeature.BlockInteract(new ScratchFeature.BlockInteract.Mapping() { + @Override + public boolean creative() { + return true; + } + + @Override + public void breakBlock(Point point) { + instance.blockHolder.setBlock(point, Block.BEDROCK); + instance.synchronizer.signalAt(point, new BlockChangePacket(point, Block.BEDROCK)); + } + + @Override + public void placeBlock(Point point) { + instance.blockHolder.setBlock(point, Block.STONE); + instance.synchronizer.signalAt(point, new BlockChangePacket(point, Block.STONE)); + } + + @Override + public void acknowledge(ServerPacket.Play packet) { + connection.networkContext.write(packet); + } + }); + + this.connection.networkContext.writePlays(initPackets()); + } + + private List initPackets() { + final DimensionType dimensionType = instance.dimensionType; + BlockHolder blockHolder = instance.blockHolder; + List packets = new ArrayList<>(); + + final JoinGamePacket joinGamePacket = new JoinGamePacket( + id, false, List.of(), 0, + 8, 8, + false, true, false, + dimensionType.toString(), "world", + 0, GameMode.CREATIVE, null, false, true, + new WorldPos("dimension", Vec.ZERO), 0); + packets.add(joinGamePacket); + packets.add(new SpawnPositionPacket(position, 0)); + packets.add(new PlayerPositionAndLookPacket(position, (byte) 0, 0)); + packets.add(getAddPlayerToList()); + + packets.add(new UpdateViewDistancePacket(VIEW_DISTANCE)); + packets.add(new UpdateViewPositionPacket(position.chunkX(), position.chunkZ())); + ChunkUtils.forChunksInRange(position.chunkX(), position.chunkZ(), VIEW_DISTANCE, + (x, z) -> packets.add(blockHolder.generatePacket(x, z))); + + packets.add(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.LEVEL_CHUNKS_LOAD_START, 0f)); + + return packets; + } + + boolean tick() { + ClientPacket packet; + while ((packet = connection.packetQueue.poll()) != null) { + this.messaging.accept(packet); + this.movement.accept(packet); + this.chunkLoading.accept(packet); + this.entityInteract.accept(packet); + this.blockInteract.accept(packet); + } + this.oldPosition = this.position; + return connection.online; + } + + private PlayerInfoUpdatePacket getAddPlayerToList() { + final var infoEntry = new PlayerInfoUpdatePacket.Entry(uuid, username, List.of(), + true, 1, GameMode.CREATIVE, null, null); + return new PlayerInfoUpdatePacket(EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED), + List.of(infoEntry)); + } + } +} diff --git a/demo/src/main/java/net/minestom/scratch/ScratchLimbo.java b/demo/src/main/java/net/minestom/scratch/ScratchLimbo.java new file mode 100644 index 000000000..f1e74f3fe --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/ScratchLimbo.java @@ -0,0 +1,311 @@ +package net.minestom.scratch; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.minestom.scratch.tools.ScratchBlockTools.BlockHolder; +import net.minestom.scratch.tools.ScratchFeature; +import net.minestom.scratch.tools.ScratchNetworkTools.NetworkContext; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.GameMode; +import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.packet.client.ClientPacket; +import net.minestom.server.network.packet.client.common.ClientPingRequestPacket; +import net.minestom.server.network.packet.client.configuration.ClientFinishConfigurationPacket; +import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket; +import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; +import net.minestom.server.network.packet.client.status.StatusRequestPacket; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.network.packet.server.common.PingResponsePacket; +import net.minestom.server.network.packet.server.configuration.FinishConfigurationPacket; +import net.minestom.server.network.packet.server.login.LoginSuccessPacket; +import net.minestom.server.network.packet.server.play.*; +import net.minestom.server.network.packet.server.play.data.WorldPos; +import net.minestom.server.network.packet.server.status.ResponsePacket; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.world.DimensionType; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.EnumSet; +import java.util.List; +import java.util.Scanner; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import static net.minestom.scratch.tools.ScratchTools.REGISTRY_DATA_PACKET; + +/** + * Limbo server example. + *

+ * Players are unsynchronized, each in their own world. + */ +public final class ScratchLimbo { + private static final SocketAddress ADDRESS = new InetSocketAddress("0.0.0.0", 25565); + private static final int VIEW_DISTANCE = 8; + + public static void main(String[] args) throws Exception { + new ScratchLimbo(); + } + + private final AtomicBoolean stop = new AtomicBoolean(false); + private final ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.INET); + + private final ReentrantLock stopLock = new ReentrantLock(); + private final Condition stopCondition = stopLock.newCondition(); + + ScratchLimbo() throws Exception { + server.bind(ADDRESS); + System.out.println("Server started on: " + ADDRESS); + Thread.startVirtualThread(this::listenCommands); + Thread.startVirtualThread(this::listenConnections); + // Wait until the server is stopped + stopLock.lock(); + try { + stopCondition.await(); + } finally { + stopLock.unlock(); + } + server.close(); + System.out.println("Server stopped"); + } + + void listenCommands() { + Scanner scanner = new Scanner(System.in); + while (serverRunning()) { + final String line = scanner.nextLine(); + if (line.equals("stop")) { + stop(); + System.out.println("Stopping server..."); + } else if (line.equals("gc")) { + System.gc(); + } + } + } + + void listenConnections() { + while (serverRunning()) { + try { + final SocketChannel client = server.accept(); + System.out.println("Accepted connection from " + client.getRemoteAddress()); + Connection connection = new Connection(client); + Thread.startVirtualThread(connection::networkLoopRead); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + void stop() { + stopLock.lock(); + try { + stopCondition.signal(); + } finally { + stopLock.unlock(); + } + stop.set(true); + } + + boolean serverRunning() { + return !stop.get(); + } + + static final class Connection { + final SocketChannel client; + final NetworkContext networkContext = new NetworkContext.Sync(this::write); + boolean online = true; + + String username; + UUID uuid; + + Connection(SocketChannel client) { + this.client = client; + } + + void networkLoopRead() { + while (online) { + this.online = networkContext.read(buffer -> { + try { + return client.read(buffer); + } catch (IOException e) { + return -1; + } + }, this::handlePacket); + } + } + + boolean write(ByteBuffer buffer) { + try { + final int length = client.write(buffer.flip()); + if (length == -1) online = false; + } catch (IOException e) { + online = false; + } + return online; + } + + void handlePacket(ClientPacket packet) { + if (packet instanceof ClientFinishConfigurationPacket) { + init(); + this.networkContext.flush(); + return; + } + if (networkContext.state() == ConnectionState.PLAY) { + processPacket(packet); + this.networkContext.flush(); + return; + } + switch (packet) { + case StatusRequestPacket ignored -> { + this.networkContext.write(new ResponsePacket(""" + { + "version": { + "name": "1.20.4", + "protocol": 765 + }, + "players": { + "max": 0, + "online": 0 + }, + "description": { + "text": "Awesome Minestom Limbo" + }, + "enforcesSecureChat": false, + "previewsChat": false + } + """)); + } + case ClientPingRequestPacket pingRequestPacket -> { + this.networkContext.write(new PingResponsePacket(pingRequestPacket.number())); + } + case ClientLoginStartPacket startPacket -> { + username = startPacket.username(); + uuid = UUID.randomUUID(); + this.networkContext.write(new LoginSuccessPacket(startPacket.profileId(), startPacket.username(), 0)); + } + case ClientLoginAcknowledgedPacket ignored -> { + this.networkContext.write(REGISTRY_DATA_PACKET); + this.networkContext.write(new FinishConfigurationPacket()); + } + default -> { + } + } + this.networkContext.flush(); + } + + private final int id = 1; + private final BlockHolder blockHolder = new BlockHolder(DimensionType.OVERWORLD); + Pos position; + Pos oldPosition; + + final ScratchFeature.Messaging messaging = new ScratchFeature.Messaging(new ScratchFeature.Messaging.Mapping() { + @Override + public Component formatMessage(String message) { + return Component.text(username).color(TextColor.color(0x00FF00)) + .append(Component.text(" > ")) + .append(Component.text(message)); + } + + @Override + public void signal(ServerPacket.Play packet) { + networkContext.write(packet); + } + }); + + final ScratchFeature.Movement movement = new ScratchFeature.Movement(new ScratchFeature.Movement.Mapping() { + @Override + public int id() { + return id; + } + + @Override + public Pos position() { + return position; + } + + @Override + public void updatePosition(Pos position) { + Connection.this.position = position; + } + + @Override + public void signalMovement(ServerPacket.Play packet) { + // Nothing to update + } + }); + + final ScratchFeature.ChunkLoading chunkLoading = new ScratchFeature.ChunkLoading(new ScratchFeature.ChunkLoading.Mapping() { + @Override + public int viewDistance() { + return VIEW_DISTANCE; + } + + @Override + public Pos oldPosition() { + return oldPosition; + } + + @Override + public ChunkDataPacket chunkPacket(int chunkX, int chunkZ) { + return blockHolder.generatePacket(chunkX, chunkZ); + } + + @Override + public void sendPacket(ServerPacket.Play packet) { + networkContext.write(packet); + } + }); + + void init() { + final Pos position = new Pos(0, 55, 0); + this.position = position; + this.oldPosition = position; + final DimensionType dimensionType = blockHolder.dimensionType(); + + this.networkContext.write(new JoinGamePacket( + id, false, List.of(), 0, + VIEW_DISTANCE, VIEW_DISTANCE, + false, true, false, + dimensionType.toString(), "world", + 0, GameMode.CREATIVE, null, false, true, + new WorldPos("dimension", Vec.ZERO), 0)); + this.networkContext.write(new SpawnPositionPacket(position, 0)); + this.networkContext.write(new PlayerPositionAndLookPacket(position, (byte) 0, 0)); + this.networkContext.write(new PlayerInfoUpdatePacket(EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED), + List.of( + new PlayerInfoUpdatePacket.Entry(uuid, username, List.of(), + true, 1, GameMode.CREATIVE, null, null) + ))); + + this.networkContext.write(new UpdateViewDistancePacket(VIEW_DISTANCE)); + this.networkContext.write(new UpdateViewPositionPacket(position.chunkX(), position.chunkZ())); + + ChunkUtils.forChunksInRange(position.chunkX(), position.chunkZ(), VIEW_DISTANCE, + (x, z) -> networkContext.write(blockHolder.generatePacket(x, z))); + + this.networkContext.write(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.LEVEL_CHUNKS_LOAD_START, 0f)); + } + + void processPacket(ClientPacket packet) { + this.messaging.accept(packet); + this.movement.accept(packet); + this.chunkLoading.accept(packet); + { + final long heapUsage = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final PlayerListHeaderAndFooterPacket listHeaderAndFooterPacket = new PlayerListHeaderAndFooterPacket( + Component.text("Welcome to Minestom Limbo!"), + Component.text("Heap: " + heapUsage / 1024 / 1024 + "MB") + ); + this.networkContext.write(listHeaderAndFooterPacket); + } + this.oldPosition = this.position; + } + } +} diff --git a/demo/src/main/java/net/minestom/scratch/tools/ScratchBlockTools.java b/demo/src/main/java/net/minestom/scratch/tools/ScratchBlockTools.java new file mode 100644 index 000000000..2f846d78b --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/tools/ScratchBlockTools.java @@ -0,0 +1,101 @@ +package net.minestom.scratch.tools; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.palette.Palette; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.network.packet.server.play.ChunkDataPacket; +import net.minestom.server.network.packet.server.play.data.ChunkData; +import net.minestom.server.network.packet.server.play.data.LightData; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.world.DimensionType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; +import org.jglrxavpok.hephaistos.nbt.NBTCompound; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; +import java.util.Map; + +import static net.minestom.server.network.NetworkBuffer.SHORT; + +public final class ScratchBlockTools { + public static final int CHUNK_SIZE_X = 16; + public static final int CHUNK_SIZE_Z = 16; + public static final int CHUNK_SECTION_SIZE = 16; + + /** + * Basic structure to hold blocks and create chunk data packets. + */ + public static final class BlockHolder implements Block.Getter, Block.Setter { + private final DimensionType dimensionType; + private final int minSection; + private final int maxSection; + private final int sectionCount; + private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + + public BlockHolder(DimensionType dimensionType) { + this.dimensionType = dimensionType; + this.minSection = dimensionType.getMinY() / CHUNK_SECTION_SIZE; + this.maxSection = (dimensionType.getMinY() + dimensionType.getHeight()) / CHUNK_SECTION_SIZE; + this.sectionCount = maxSection - minSection; + } + + public ChunkDataPacket generatePacket(int chunkX, int chunkZ) { + final Chunk chunk = chunks.computeIfAbsent(ChunkUtils.getChunkIndex(chunkX, chunkZ), i -> new Chunk()); + final byte[] data = NetworkBuffer.makeArray(networkBuffer -> { + for (Section section : chunk.sections) { + networkBuffer.write(SHORT, (short) section.blocks.count()); + networkBuffer.write(section.blocks); + networkBuffer.write(section.biomes); + } + }); + return new ChunkDataPacket(chunkX, chunkZ, + new ChunkData(NBTCompound.EMPTY, data, Map.of()), + new LightData(new BitSet(), new BitSet(), new BitSet(), new BitSet(), List.of(), List.of()) + ); + } + + @Override + public void setBlock(int x, int y, int z, @NotNull Block block) { + final Chunk chunk = chunks.computeIfAbsent(ChunkUtils.getChunkIndex(x >> 4, z >> 4), i -> new Chunk()); + final Section section = chunk.sections[(y >> 4) - minSection]; + section.blocks.set(x & 0xF, y & 0xF, z & 0xF, block.stateId()); + } + + @Override + public @UnknownNullability Block getBlock(int x, int y, int z, @NotNull Condition condition) { + final Chunk chunk = chunks.computeIfAbsent(ChunkUtils.getChunkIndex(x >> 4, z >> 4), i -> new Chunk()); + final Section section = chunk.sections[(y >> 4) - minSection]; + final int stateId = section.blocks.get(x & 0xF, y & 0xF, z & 0xF); + return Block.fromStateId((short) stateId); + } + + private final class Chunk { + private final Section[] sections = new Section[sectionCount]; + + { + Arrays.setAll(sections, i -> new Section()); + // Generate blocks + for (int i = 0; i < sectionCount; i++) { + final Section section = sections[i]; + final Palette blockPalette = section.blocks; + if (i < 7) { + blockPalette.fill(Block.STONE.stateId()); + } + } + } + } + + private static final class Section { + private final Palette blocks = Palette.blocks(); + private final Palette biomes = Palette.biomes(); + } + + public DimensionType dimensionType() { + return dimensionType; + } + } +} diff --git a/demo/src/main/java/net/minestom/scratch/tools/ScratchFeature.java b/demo/src/main/java/net/minestom/scratch/tools/ScratchFeature.java new file mode 100644 index 000000000..a59cbcf4c --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/tools/ScratchFeature.java @@ -0,0 +1,151 @@ +package net.minestom.scratch.tools; + +import net.kyori.adventure.text.Component; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.instance.block.BlockFace; +import net.minestom.server.network.packet.client.ClientPacket; +import net.minestom.server.network.packet.client.play.*; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.network.packet.server.play.*; +import net.minestom.server.utils.Direction; +import net.minestom.server.utils.chunk.ChunkUtils; + +import java.util.function.Consumer; + +public interface ScratchFeature extends Consumer { + record Messaging(Mapping mapping) implements ScratchFeature { + @Override + public void accept(ClientPacket packet) { + if (packet instanceof ClientChatMessagePacket chatMessagePacket) { + final String message = chatMessagePacket.message(); + final Component formatted = mapping.formatMessage(message); + mapping.signal(new SystemChatPacket(formatted, false)); + } + } + + public interface Mapping { + Component formatMessage(String message); + + void signal(ServerPacket.Play packet); + } + } + + record Movement(Mapping mapping) implements ScratchFeature { + @Override + public void accept(ClientPacket packet) { + final int id = mapping.id(); + if (packet instanceof ClientPlayerPositionAndRotationPacket positionAndRotationPacket) { + final Pos position = positionAndRotationPacket.position(); + mapping.updatePosition(position); + mapping.signalMovement(new EntityTeleportPacket(id, position, positionAndRotationPacket.onGround())); + mapping.signalMovement(new EntityHeadLookPacket(id, position.yaw())); + } else if (packet instanceof ClientPlayerPositionPacket positionPacket) { + final Pos position = mapping.position().withCoord(positionPacket.position()); + mapping.updatePosition(position); + mapping.signalMovement(new EntityTeleportPacket(id, position, positionPacket.onGround())); + } else if (packet instanceof ClientPlayerRotationPacket rotationPacket) { + final Pos position = mapping.position().withView(rotationPacket.yaw(), rotationPacket.pitch()); + mapping.signalMovement(new EntityRotationPacket(id, position.yaw(), position.pitch(), rotationPacket.onGround())); + mapping.signalMovement(new EntityHeadLookPacket(id, position.yaw())); + } + } + + public interface Mapping { + int id(); + + Pos position(); + + void updatePosition(Pos position); + + void signalMovement(ServerPacket.Play packet); + } + } + + record ChunkLoading(Mapping mapping) implements ScratchFeature { + @Override + public void accept(ClientPacket packet) { + final Pos oldPosition = mapping.oldPosition(); + Pos position = null; + if (packet instanceof ClientPlayerPositionAndRotationPacket positionAndRotationPacket) { + position = positionAndRotationPacket.position(); + } else if (packet instanceof ClientPlayerPositionPacket positionPacket) { + position = Pos.fromPoint(positionPacket.position()); + } + if (position == null || position.sameChunk(oldPosition)) return; + final int oldChunkX = oldPosition.chunkX(); + final int oldChunkZ = oldPosition.chunkZ(); + final int newChunkX = position.chunkX(); + final int newChunkZ = position.chunkZ(); + mapping.sendPacket(new UpdateViewPositionPacket(newChunkX, newChunkZ)); + ChunkUtils.forDifferingChunksInRange(newChunkX, newChunkZ, oldChunkX, oldChunkZ, + mapping.viewDistance(), + (x, z) -> mapping.sendPacket(mapping.chunkPacket(x, z)), + (x, z) -> mapping.sendPacket(new UnloadChunkPacket(x, z))); + } + + public interface Mapping { + int viewDistance(); + + Pos oldPosition(); + + ChunkDataPacket chunkPacket(int chunkX, int chunkZ); + + void sendPacket(ServerPacket.Play packet); + } + } + + record EntityInteract(Mapping mapping) implements ScratchFeature { + @Override + public void accept(ClientPacket packet) { + if (packet instanceof ClientInteractEntityPacket interactEntityPacket) { + final int targetId = interactEntityPacket.targetId(); + final ClientInteractEntityPacket.Type type = interactEntityPacket.type(); + if (type instanceof ClientInteractEntityPacket.Interact interact) { + mapping.right(targetId); + } else if (type instanceof ClientInteractEntityPacket.Attack attack) { + mapping.left(targetId); + } + } + } + + public interface Mapping { + void left(int id); + + void right(int id); + } + } + + record BlockInteract(Mapping mapping) implements ScratchFeature { + @Override + public void accept(ClientPacket packet) { + if (packet instanceof ClientPlayerDiggingPacket diggingPacket) { + final ClientPlayerDiggingPacket.Status status = diggingPacket.status(); + final Point blockPosition = diggingPacket.blockPosition(); + mapping.acknowledge(new AcknowledgeBlockChangePacket(diggingPacket.sequence())); + switch (status) { + case STARTED_DIGGING -> { + if (mapping.creative()) { + mapping.breakBlock(blockPosition); + } + } + } + } else if (packet instanceof ClientPlayerBlockPlacementPacket blockPlacementPacket) { + final Point blockPosition = blockPlacementPacket.blockPosition(); + final BlockFace blockFace = blockPlacementPacket.blockFace(); + final Direction direction = blockFace.toDirection(); + mapping.placeBlock(blockPosition.add(direction.normalX(), direction.normalY(), direction.normalZ())); + } + } + + public interface Mapping { + boolean creative(); + + void breakBlock(Point point); + + void placeBlock(Point point); + + void acknowledge(ServerPacket.Play packet); + } + } +} diff --git a/demo/src/main/java/net/minestom/scratch/tools/ScratchNetworkTools.java b/demo/src/main/java/net/minestom/scratch/tools/ScratchNetworkTools.java new file mode 100644 index 000000000..0a471e418 --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/tools/ScratchNetworkTools.java @@ -0,0 +1,257 @@ +package net.minestom.scratch.tools; + +import it.unimi.dsi.fastutil.ints.IntArrays; +import net.minestom.server.ServerFlag; +import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.PacketProcessor; +import net.minestom.server.network.packet.client.ClientPacket; +import net.minestom.server.network.packet.client.configuration.ClientFinishConfigurationPacket; +import net.minestom.server.network.packet.client.handshake.ClientHandshakePacket; +import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.utils.ObjectPool; +import net.minestom.server.utils.PacketUtils; +import net.minestom.server.utils.binary.BinaryBuffer; +import org.jctools.queues.MpmcUnboundedXaddArrayQueue; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.zip.DataFormatException; + +public final class ScratchNetworkTools { + private static final PacketProcessor PACKET_PROCESSOR = new PacketProcessor(null); + private static final ObjectPool PACKET_POOL = new ObjectPool<>(() -> ByteBuffer.allocateDirect(ServerFlag.POOLED_BUFFER_SIZE), ByteBuffer::clear); + + public static void readPackets(ByteBuffer buffer, + AtomicReference stateRef, + Consumer consumer) { + var b = BinaryBuffer.wrap(buffer); + b.readerOffset(buffer.position()); + b.writerOffset(buffer.limit()); + try { + PacketUtils.readPackets(b, false, + (id, payload) -> { + final ConnectionState state = stateRef.get(); + final ClientPacket packet = PACKET_PROCESSOR.create(state, id, payload); + consumer.accept(packet); + }); + buffer.position(b.readerOffset()); + } catch (DataFormatException e) { + throw new RuntimeException(e); + } + } + + public static void write(NetworkContext.Packet packet, ByteBuffer buffer, Predicate fullCallback) { + final int checkLength = buffer.limit() / 2; + if (packet instanceof NetworkContext.Packet.PacketIdPair packetPair) { + final ServerPacket packetLoop = packetPair.packet; + final int idLoop = packetPair.id; + if (buffer.position() >= checkLength) { + if (!fullCallback.test(buffer)) return; + } + PacketUtils.writeFramedPacket(buffer, idLoop, packetLoop, 0); + } else if (packet instanceof NetworkContext.Packet.PlayList playList) { + final List packets = playList.packets(); + final int[] exception = playList.exception(); + int index = 0; + for (ServerPacket.Play packetLoop : packets) { + final int idLoop = packetLoop.playId(); + if (exception.length > 0 && Arrays.binarySearch(exception, index++) >= 0) continue; + if (buffer.position() >= checkLength) { + if (!fullCallback.test(buffer)) return; + } + PacketUtils.writeFramedPacket(buffer, idLoop, packetLoop, 0); + } + } else { + throw new IllegalStateException("Unexpected packet type: " + packet); + } + } + + public static ConnectionState nextState(ClientPacket packet, ConnectionState currentState) { + return switch (packet) { + case ClientHandshakePacket handshakePacket -> switch (handshakePacket.intent()) { + case 1 -> ConnectionState.STATUS; + case 2 -> ConnectionState.LOGIN; + default -> throw new IllegalStateException("Unexpected value: " + handshakePacket.intent()); + }; + case ClientLoginAcknowledgedPacket ignored -> ConnectionState.CONFIGURATION; + case ClientFinishConfigurationPacket ignored -> ConnectionState.PLAY; + default -> currentState; + }; + } + + public interface NetworkContext { + + boolean read(Function reader, Consumer consumer); + + void write(Packet packet); + + void flush(); + + ConnectionState state(); + + default void write(ServerPacket packet) { + write(new Packet.PacketIdPair(packet, packet.getId(state()))); + } + + default void writePlays(List packets) { + write(new NetworkContext.Packet.PlayList(packets)); + } + + sealed interface Packet { + record PacketIdPair(ServerPacket packet, int id) implements Packet { + } + + record PlayList(List packets, int[] exception) implements Packet { + public PlayList { + packets = List.copyOf(packets); + } + + public PlayList(List packets) { + this(packets, IntArrays.EMPTY_ARRAY); + } + } + } + + final class Async implements NetworkContext { + final AtomicReference stateRef = new AtomicReference<>(ConnectionState.HANDSHAKE); + final MpmcUnboundedXaddArrayQueue packetWriteQueue = new MpmcUnboundedXaddArrayQueue<>(1024); + + final ReentrantLock writeLock = new ReentrantLock(); + final Condition writeCondition = writeLock.newCondition(); + + @Override + public boolean read(Function reader, Consumer consumer) { + try (ObjectPool.Holder hold = PACKET_POOL.hold()) { + ByteBuffer buffer = hold.get(); + while (buffer.hasRemaining()) { + final int length = reader.apply(buffer); + if (length == -1) return false; + readPackets(buffer.flip(), stateRef, clientPacket -> { + stateRef.set(nextState(clientPacket, stateRef.get())); + consumer.accept(clientPacket); + }); + } + } + return true; + } + + public boolean write(Function writer) { + try { + this.writeLock.lock(); + this.writeCondition.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + this.writeLock.unlock(); + } + + AtomicBoolean result = new AtomicBoolean(true); + try (ObjectPool.Holder hold = PACKET_POOL.hold()) { + ByteBuffer buffer = hold.get(); + Packet packet; + while ((packet = packetWriteQueue.poll()) != null) { + ScratchNetworkTools.write(packet, buffer, b -> { + final int length = writer.apply(b); + b.compact(); + if (length == -1) { + result.setPlain(false); + return false; + } + return true; + }); + } + while (buffer.hasRemaining()) { + final int length = writer.apply(buffer); + if (length == -1) { + result.setPlain(false); + break; + } + } + } + return result.getPlain(); + } + + @Override + public void write(Packet packet) { + this.packetWriteQueue.add(packet); + } + + @Override + public void flush() { + try { + this.writeLock.lock(); + this.writeCondition.signal(); + } finally { + this.writeLock.unlock(); + } + } + + @Override + public ConnectionState state() { + return stateRef.get(); + } + } + + final class Sync implements NetworkContext { + final AtomicReference stateRef = new AtomicReference<>(ConnectionState.HANDSHAKE); + final Predicate writer; + final ArrayDeque packetWriteQueue = new ArrayDeque<>(); + + public Sync(Predicate writer) { + this.writer = writer; + } + + @Override + public boolean read(Function reader, Consumer consumer) { + try (ObjectPool.Holder hold = PACKET_POOL.hold()) { + ByteBuffer buffer = hold.get(); + while (buffer.hasRemaining()) { + final int length = reader.apply(buffer); + if (length == -1) return false; + readPackets(buffer.flip(), stateRef, clientPacket -> { + stateRef.set(nextState(clientPacket, stateRef.get())); + consumer.accept(clientPacket); + }); + } + } + return true; + } + + @Override + public void write(Packet packet) { + this.packetWriteQueue.add(packet); + } + + @Override + public void flush() { + try (ObjectPool.Holder hold = PACKET_POOL.hold()) { + ByteBuffer buffer = hold.get(); + Packet packet; + while ((packet = packetWriteQueue.poll()) != null) { + ScratchNetworkTools.write(packet, buffer, b -> { + final boolean result = writer.test(b); + b.compact(); + return result; + }); + } + while (buffer.hasRemaining() && writer.test(buffer)) ; + } + } + + @Override + public ConnectionState state() { + return stateRef.get(); + } + } + } +} diff --git a/demo/src/main/java/net/minestom/scratch/tools/ScratchTools.java b/demo/src/main/java/net/minestom/scratch/tools/ScratchTools.java new file mode 100644 index 000000000..53881d135 --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/tools/ScratchTools.java @@ -0,0 +1,29 @@ +package net.minestom.scratch.tools; + +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.item.armor.TrimManager; +import net.minestom.server.message.Messenger; +import net.minestom.server.network.packet.server.configuration.RegistryDataPacket; +import net.minestom.server.world.DimensionTypeManager; +import net.minestom.server.world.biomes.BiomeManager; +import org.jglrxavpok.hephaistos.nbt.NBT; + +import java.util.HashMap; + +public final class ScratchTools { + public static RegistryDataPacket REGISTRY_DATA_PACKET; + + static { + DimensionTypeManager dimensionTypeManager = new DimensionTypeManager(); + BiomeManager biomeManager = new BiomeManager(); + TrimManager trimManager = new TrimManager(); + var registry = new HashMap(); + registry.put("minecraft:chat_type", Messenger.chatRegistry()); + registry.put("minecraft:dimension_type", dimensionTypeManager.toNBT()); + registry.put("minecraft:worldgen/biome", biomeManager.toNBT()); + registry.put("minecraft:damage_type", DamageType.getNBT()); + registry.put("minecraft:trim_material", trimManager.getTrimMaterialNBT()); + registry.put("minecraft:trim_pattern", trimManager.getTrimPatternNBT()); + REGISTRY_DATA_PACKET = new RegistryDataPacket(NBT.Compound(registry)); + } +} diff --git a/demo/src/main/java/net/minestom/scratch/tools/ScratchVelocityTools.java b/demo/src/main/java/net/minestom/scratch/tools/ScratchVelocityTools.java new file mode 100644 index 000000000..ac4da006c --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/tools/ScratchVelocityTools.java @@ -0,0 +1,17 @@ +package net.minestom.scratch.tools; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; + +public final class ScratchVelocityTools { + public static Vec knockback(Pos source, float strength, Vec velocity, boolean onGround) { + final double x = Math.sin(source.yaw() * 0.017453292); + final double z = -Math.cos(source.yaw() * 0.017453292); + final Vec velocityModifier = new Vec(x, z).normalize().mul(strength); + final double verticalLimit = .4d; + return new Vec(velocity.x() / 2d - velocityModifier.x(), + onGround ? Math.min(verticalLimit, velocity.y() / 2d + strength) : velocity.y(), + velocity.z() / 2d - velocityModifier.z() + ); + } +} diff --git a/demo/src/main/java/net/minestom/scratch/tools/ScratchViewTools.java b/demo/src/main/java/net/minestom/scratch/tools/ScratchViewTools.java new file mode 100644 index 000000000..226ff6de2 --- /dev/null +++ b/demo/src/main/java/net/minestom/scratch/tools/ScratchViewTools.java @@ -0,0 +1,251 @@ +package net.minestom.scratch.tools; + +import it.unimi.dsi.fastutil.ints.*; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minestom.server.coordinate.Point; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.utils.function.IntegerBiConsumer; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import static net.minestom.scratch.tools.ScratchNetworkTools.NetworkContext; + +public final class ScratchViewTools { + + /** + * Utils to synchronize packets between close entities. + */ + public static final class Synchronizer { + private final int viewDistance; + private final Map entries = new HashMap<>(); + private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + private final IntSet entriesChanged = new IntOpenHashSet(); + + public Synchronizer(int viewDistance) { + this.viewDistance = viewDistance; + } + + public Entry makeEntry(boolean receiver, int id, Point point, + Supplier> initSupplier, + Supplier> destroySupplier) { + final Entry entry = new Entry(receiver, id, point, initSupplier, destroySupplier); + this.entries.put(id, entry); + this.entriesChanged.add(id); + return entry; + } + + public void signalAt(Point point, ServerPacket.Play packet) { + signalAt(point.chunkX(), point.chunkZ(), packet); + } + + public void signalAt(int chunkX, int chunkZ, ServerPacket.Play packet) { + Chunk chunk = chunks.get(ChunkUtils.getChunkIndex(chunkX, chunkZ)); + if (chunk != null) chunk.broadcaster.append(packet); + } + + public void computePackets(BiConsumer consumer) { + // Update chunks viewers + for (int entryId : entriesChanged) { + Entry entry = entries.get(entryId); + if (entry == null) continue; + if (entry.initialized) { + final long oldChunkIndex = ChunkUtils.getChunkIndex(entry.oldChunkX, entry.oldChunkZ); + Chunk oldChunk = chunks.get(oldChunkIndex); + if (oldChunk != null) { + oldChunk.viewers.remove(entryId); + if (entry.receiver) oldChunk.viewersReceivers.remove(entryId); + } + } + if (entry.alive) { + final long newChunkIndex = ChunkUtils.getChunkIndex(entry.newChunkX, entry.newChunkZ); + Chunk newChunk = chunks.computeIfAbsent(newChunkIndex, Chunk::new); + newChunk.viewers.add(entryId); + if (entry.receiver) newChunk.viewersReceivers.add(entryId); + } + } + // Send init/destroy packets + for (int entryId : entriesChanged) { + Entry entry = entries.get(entryId); + if (entry == null) continue; + IntegerBiConsumer newCallback = (x, z) -> { + Chunk chunk = chunks.computeIfAbsent(ChunkUtils.getChunkIndex(x, z), Chunk::new); + for (int viewerId : entry.receiver ? chunk.viewers : chunk.viewersReceivers) { + if (viewerId == entryId) continue; + final Entry viewer = entries.get(viewerId); + if (viewer == null) continue; + if (entry.receiver) { + final List packets = viewer.initSupplier.get(); + consumer.accept(entryId, new NetworkContext.Packet.PlayList(packets)); + } + if (viewer.receiver) { + final List packets = entry.initSupplier.get(); + consumer.accept(viewerId, new NetworkContext.Packet.PlayList(packets)); + } + } + if (entry.receiver) chunk.receivers.add(entryId); + }; + IntegerBiConsumer oldCallback = (x, z) -> { + final Chunk chunk = chunks.get(ChunkUtils.getChunkIndex(x, z)); + if (chunk == null) return; + for (int viewerId : entry.receiver ? chunk.viewers : chunk.viewersReceivers) { + if (viewerId == entryId) continue; + final Entry viewer = entries.get(viewerId); + if (viewer == null) continue; + if (entry.receiver) { + final List packets = viewer.destroySupplier.get(); + consumer.accept(entryId, new NetworkContext.Packet.PlayList(packets)); + } + if (viewer.receiver) { + final List packets = entry.destroySupplier.get(); + consumer.accept(viewerId, new NetworkContext.Packet.PlayList(packets)); + } + } + if (entry.receiver) chunk.receivers.remove(entryId); + }; + if (entry.initialized) { + if (entry.alive) { + ChunkUtils.forDifferingChunksInRange(entry.newChunkX, entry.newChunkZ, + entry.oldChunkX, entry.oldChunkZ, + viewDistance, newCallback, oldCallback); + } else { + ChunkUtils.forChunksInRange(entry.newChunkX, entry.newChunkZ, viewDistance, oldCallback); + } + } else { + ChunkUtils.forChunksInRange(entry.newChunkX, entry.newChunkZ, viewDistance, newCallback); + entry.initialized = true; + } + entry.oldChunkX = entry.newChunkX; + entry.oldChunkZ = entry.newChunkZ; + } + // Remove dead entries + this.entries.values().removeIf(entry -> !entry.alive); + // Send update packets + for (Chunk chunk : chunks.values()) { + try (Broadcaster.Collector collector = chunk.broadcaster.collector()) { + final List packets = collector.packets(); + if (packets.isEmpty()) continue; + for (int viewerId : chunk.receivers) { + final Entry viewer = entries.get(viewerId); + if (viewer == null) continue; + final int[] exception = collector.exception(viewerId); + consumer.accept(viewerId, new NetworkContext.Packet.PlayList(packets, exception)); + } + } + } + this.entriesChanged.clear(); + } + + public final class Entry { + private final boolean receiver; + private final int id; + private final Supplier> initSupplier; + private final Supplier> destroySupplier; + + private int oldChunkX, oldChunkZ; + private int newChunkX, newChunkZ; + private boolean initialized = false; + private boolean alive = true; + + public Entry(boolean receiver, int id, Point point, + Supplier> initSupplier, + Supplier> destroySupplier) { + this.receiver = receiver; + this.id = id; + this.oldChunkX = point.chunkX(); + this.oldChunkZ = point.chunkZ(); + this.newChunkX = point.chunkX(); + this.newChunkZ = point.chunkZ(); + this.initSupplier = initSupplier; + this.destroySupplier = destroySupplier; + } + + public void move(Point point) { + this.newChunkX = point.chunkX(); + this.newChunkZ = point.chunkZ(); + if (oldChunkX != newChunkX || oldChunkZ != newChunkZ) { + entriesChanged.add(id); + } + } + + public void signal(ServerPacket.Play packet) { + signalAt(newChunkX, newChunkZ, packet); + } + + public void signalAt(int chunkX, int chunkZ, ServerPacket.Play packet) { + Chunk chunk = chunks.get(ChunkUtils.getChunkIndex(chunkX, chunkZ)); + if (chunk != null) chunk.broadcaster.append(packet, id); + } + + public void unmake() { + entriesChanged.add(id); + this.alive = false; + } + } + + private static final class Chunk { + private final int x, z; + private final IntSet viewers = new IntOpenHashSet(); + private final IntSet viewersReceivers = new IntOpenHashSet(); + private final IntSet receivers = new IntOpenHashSet(); + private final Broadcaster broadcaster = new Broadcaster(); + + public Chunk(long index) { + this.x = ChunkUtils.getChunkCoordX(index); + this.z = ChunkUtils.getChunkCoordZ(index); + } + } + } + + /** + * Utils to broadcast packets to multiple players while ignoring the original sender. + *

+ * Useful for interest management. + */ + public static final class Broadcaster { + private final Int2ObjectMap entityIdMap = new Int2ObjectOpenHashMap<>(); + private final List packets = new ArrayList<>(); + + public void append(ServerPacket.Play packet) { + this.packets.add(packet); + } + + public void append(ServerPacket.Play packet, int senderId) { + final int index = packets.size(); + this.packets.add(packet); + IntArrayList list = entityIdMap.computeIfAbsent(senderId, id -> new IntArrayList()); + list.add(index); + } + + public Collector collector() { + return new Collector(); + } + + public final class Collector implements Closeable { + private final List constPackets = List.copyOf(Broadcaster.this.packets); + + public List packets() { + return constPackets; + } + + public int[] exception(int id) { + IntArrayList list = Broadcaster.this.entityIdMap.get(id); + if (list == null) return IntArrays.EMPTY_ARRAY; + return list.toIntArray(); + } + + @Override + public void close() { + Broadcaster.this.packets.clear(); + Broadcaster.this.entityIdMap.clear(); + } + } + } +} diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 3d3595421..6cb843f7f 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -12,7 +12,6 @@ import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.block.BlockIterator; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; final class BlockCollision { /** @@ -24,20 +23,71 @@ final class BlockCollision { static PhysicsResult handlePhysics(@NotNull BoundingBox boundingBox, @NotNull Vec velocity, @NotNull Pos entityPosition, @NotNull Block.Getter getter, - @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { - if (velocity.isZero()) { - // TODO should return a constant - return new PhysicsResult(entityPosition, Vec.ZERO, false, false, false, false, - velocity, new Point[3], new Shape[3], false, SweepResult.NO_COLLISION); + return rayPhysics(boundingBox, velocity, entityPosition, false, getter, singleCollision); + } + + private static PhysicsResult rayPhysics(@NotNull BoundingBox boundingBox, + @NotNull Vec velocity, @NotNull Pos position, boolean onGround, + @NotNull Block.Getter getter, + boolean singleCollision) { + SweepResult finalResult = new SweepResult(1 - Vec.EPSILON, 0, 0, 0, null, null); + final Vec[] allFaces = calculateFaces(velocity, boundingBox); + for (var face : allFaces) { + BlockIterator iterator = new BlockIterator(Vec.fromPoint(face.add(position)), velocity, 0, velocity.length()); + while (iterator.hasNext()) { + Point p = iterator.next(); + final Block block = getter.getBlock(p, Block.Getter.Condition.TYPE); + Shape shape = block.registry().collisionShape(); + final boolean currentShort = shape.relativeEnd().y() < 0.5; + //System.out.println("check " + p + " " + block.name() + " " + shape.relativeEnd()); + if (currentShort) { + // Must check for fence block below + final Point belowPoint = p.sub(0, 1, 0); + final Block belowBlock = getter.getBlock(belowPoint, Block.Getter.Condition.TYPE); + final Shape belowShape = belowBlock.registry().collisionShape(); + //System.out.println("below " + belowBlock.name() + " " + belowShape.relativeEnd()); + if (belowShape.relativeEnd().y() > 1) { + p = belowPoint; + shape = belowShape; + } + } + final boolean collide = shape.intersectBoxSwept(position, velocity, p, boundingBox, finalResult); + final boolean collisionX = finalResult.normalX != 0; + final boolean collisionY = finalResult.normalY != 0; + final boolean collisionZ = finalResult.normalZ != 0; + final boolean hasCollided = collisionX || collisionY || collisionZ; + if (!hasCollided) continue; + double deltaX = finalResult.res * velocity.x(); + double deltaY = finalResult.res * velocity.y(); + double deltaZ = finalResult.res * velocity.z(); + if (Math.abs(deltaX) < Vec.EPSILON) deltaX = 0; + if (Math.abs(deltaY) < Vec.EPSILON) deltaY = 0; + if (Math.abs(deltaZ) < Vec.EPSILON) deltaZ = 0; + final Pos newPosition = position.add(deltaX, deltaY, deltaZ); + final double remainingX = collisionX ? 0 : velocity.x() - deltaX; + final double remainingY = collisionY ? 0 : velocity.y() - deltaY; + final double remainingZ = collisionZ ? 0 : velocity.z() - deltaZ; + final Vec newVelocity = new Vec(remainingX, remainingY, remainingZ); + if (newVelocity.isZero() || singleCollision) { + // Collided on all required axis + return new PhysicsResult(newPosition, newVelocity, + collisionY || onGround, false, false, false, Vec.ZERO, + new Point[3], new Shape[3], false, + SweepResult.NO_COLLISION); + } else { + // Additional axis to check + return rayPhysics(boundingBox, newVelocity, newPosition, + collisionY || onGround, getter, singleCollision); + } + } } - // Fast-exit using cache - final PhysicsResult cachedResult = cachedPhysics(velocity, entityPosition, getter, lastPhysicsResult); - if (cachedResult != null) { - return cachedResult; - } - // Expensive AABB computation - return stepPhysics(boundingBox, velocity, entityPosition, getter, singleCollision); + // No collision + final Pos newPosition = position.add(velocity); + return new PhysicsResult(newPosition, velocity, + onGround, false, false, false, Vec.ZERO, + new Point[3], new Shape[3], false, + SweepResult.NO_COLLISION); } static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) { diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index 00533def4..872ef24ce 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -26,25 +26,20 @@ public final class CollisionUtils { * Works by getting all the full blocks that an entity could interact with. * All bounding boxes inside the full blocks are checked for collisions with the entity. * - * @param entity the entity to move - * @param entityVelocity the velocity of the entity - * @param lastPhysicsResult the last physics result, can be null + * @param entity the entity to move + * @param entityVelocity the velocity of the entity * @param singleCollision if the entity should only collide with one block * @return the result of physics simulation */ - public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, - @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { + public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, boolean singleCollision) { final Instance instance = entity.getInstance(); assert instance != null; - return handlePhysics(instance, entity.getChunk(), - entity.getBoundingBox(), - entity.getPosition(), entityVelocity, - lastPhysicsResult, singleCollision); + return handlePhysics(instance, entity.getChunk(), entity.getBoundingBox(), + entity.getPosition(), entityVelocity, singleCollision); } /** - * - * @param entity the entity to move + * @param entity the entity to move * @param entityVelocity the velocity of the entity * @return the closest entity we collide with */ @@ -55,8 +50,7 @@ public final class CollisionUtils { } /** - * - * @param velocity the velocity of the entity + * @param velocity the velocity of the entity * @param extendRadius the largest entity bounding box we can collide with * Measured from bottom center to top corner * This is used to extend the search radius for entities we collide with @@ -73,19 +67,15 @@ public final class CollisionUtils { * Works by getting all the full blocks that an entity could interact with. * All bounding boxes inside the full blocks are checked for collisions with the entity. * - * @param entity the entity to move + * @param entity the entity to move * @param entityVelocity the velocity of the entity - * @param lastPhysicsResult the last physics result, can be null * @return the result of physics simulation */ - public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, - @Nullable PhysicsResult lastPhysicsResult) { + public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity) { final Instance instance = entity.getInstance(); assert instance != null; - return handlePhysics(instance, entity.getChunk(), - entity.getBoundingBox(), - entity.getPosition(), entityVelocity, - lastPhysicsResult, false); + return handlePhysics(instance, entity.getChunk(), entity.getBoundingBox(), + entity.getPosition(), entityVelocity, false); } /** @@ -99,10 +89,9 @@ public final class CollisionUtils { */ public static PhysicsResult handlePhysics(@NotNull Instance instance, @Nullable Chunk chunk, @NotNull BoundingBox boundingBox, - @NotNull Pos position, @NotNull Vec velocity, - @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { + @NotNull Pos position, @NotNull Vec velocity, boolean singleCollision) { final Block.Getter getter = new ChunkCache(instance, chunk != null ? chunk : instance.getChunkAt(position), Block.STONE); - return handlePhysics(getter, boundingBox, position, velocity, lastPhysicsResult, singleCollision); + return handlePhysics(getter, boundingBox, position, velocity, singleCollision); } /** @@ -117,11 +106,9 @@ public final class CollisionUtils { @ApiStatus.Internal public static PhysicsResult handlePhysics(@NotNull Block.Getter blockGetter, @NotNull BoundingBox boundingBox, - @NotNull Pos position, @NotNull Vec velocity, - @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { + @NotNull Pos position, @NotNull Vec velocity, boolean singleCollision) { return BlockCollision.handlePhysics(boundingBox, - velocity, position, - blockGetter, lastPhysicsResult, singleCollision); + velocity, position, blockGetter, singleCollision); } /** @@ -138,18 +125,12 @@ public final class CollisionUtils { public static boolean isLineOfSightReachingShape(@NotNull Instance instance, @Nullable Chunk chunk, @NotNull Point start, @NotNull Point end, @NotNull Shape shape) { - final PhysicsResult result = handlePhysics(instance, chunk, - BoundingBox.ZERO, - Pos.fromPoint(start), Vec.fromPoint(end.sub(start)), - null, false); + final PhysicsResult result = handlePhysics(instance, chunk, BoundingBox.ZERO, + Pos.fromPoint(start), Vec.fromPoint(end.sub(start)), false); return shape.intersectBox(end.sub(result.newPosition()).sub(Vec.EPSILON), BoundingBox.ZERO); } - public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity) { - return handlePhysics(entity, entityVelocity, null); - } - public static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) { return BlockCollision.canPlaceBlockAt(instance, blockPos, b); } @@ -184,7 +165,7 @@ public final class CollisionUtils { public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) { return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry); } - + /** * Simulate the entity's collision physics as if the world had no blocks * diff --git a/src/main/java/net/minestom/server/collision/PhysicsUtils.java b/src/main/java/net/minestom/server/collision/PhysicsUtils.java index 76089299f..384e2b447 100644 --- a/src/main/java/net/minestom/server/collision/PhysicsUtils.java +++ b/src/main/java/net/minestom/server/collision/PhysicsUtils.java @@ -25,14 +25,13 @@ public final class PhysicsUtils { * @param entityHasPhysics whether the entity has physics * @param entityOnGround whether the entity is on the ground * @param entityFlying whether the entity is flying - * @param previousPhysicsResult the physics result from the previous simulation or null * @return a {@link PhysicsResult} containing the resulting physics state of this simulation */ public static @NotNull PhysicsResult simulateMovement(@NotNull Pos entityPosition, @NotNull Vec entityVelocityPerTick, @NotNull BoundingBox entityBoundingBox, @NotNull WorldBorder worldBorder, @NotNull Block.Getter blockGetter, @NotNull Aerodynamics aerodynamics, boolean entityNoGravity, - boolean entityHasPhysics, boolean entityOnGround, boolean entityFlying, @Nullable PhysicsResult previousPhysicsResult) { + boolean entityHasPhysics, boolean entityOnGround, boolean entityFlying) { final PhysicsResult physicsResult = entityHasPhysics ? - CollisionUtils.handlePhysics(blockGetter, entityBoundingBox, entityPosition, entityVelocityPerTick, previousPhysicsResult, false) : + CollisionUtils.handlePhysics(blockGetter, entityBoundingBox, entityPosition, entityVelocityPerTick, false) : CollisionUtils.blocklessCollision(entityPosition, entityVelocityPerTick); Pos newPosition = physicsResult.newPosition(); diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 6d3c83d7d..c7293e362 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -102,7 +102,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev protected boolean onGround; protected BoundingBox boundingBox; - private PhysicsResult previousPhysicsResult = null; protected Entity vehicle; @@ -593,8 +592,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev boolean entityIsPlayer = this instanceof Player; boolean entityFlying = entityIsPlayer && ((Player) this).isFlying(); PhysicsResult physicsResult = PhysicsUtils.simulateMovement(position, velocity.div(ServerFlag.SERVER_TICKS_PER_SECOND), boundingBox, - instance.getWorldBorder(), instance, aerodynamics, hasNoGravity(), hasPhysics, onGround, entityFlying, previousPhysicsResult); - this.previousPhysicsResult = physicsResult; + instance.getWorldBorder(), instance, aerodynamics, hasNoGravity(), hasPhysics, onGround, entityFlying); Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, physicsResult.newPosition()); if (!ChunkUtils.isLoaded(finalChunk)) return; @@ -805,7 +803,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev this.isActive = true; this.position = spawnPosition; this.previousPosition = spawnPosition; - this.previousPhysicsResult = null; this.instance = instance; return instance.loadOptionalChunk(spawnPosition).thenAccept(chunk -> { try { diff --git a/src/main/java/net/minestom/server/entity/PlayerProjectile.java b/src/main/java/net/minestom/server/entity/PlayerProjectile.java index d62c6e75f..3e0fe126f 100644 --- a/src/main/java/net/minestom/server/entity/PlayerProjectile.java +++ b/src/main/java/net/minestom/server/entity/PlayerProjectile.java @@ -134,8 +134,7 @@ public class PlayerProjectile extends LivingEntity { PhysicsResult result = CollisionUtils.handlePhysics( instance, this.getChunk(), this.getBoundingBox(), - posBefore, diff, - null, true + posBefore, diff, true ); if (cooldown + 500 < System.currentTimeMillis()) { diff --git a/src/main/java/net/minestom/server/instance/WorldBorder.java b/src/main/java/net/minestom/server/instance/WorldBorder.java index 80542a635..3f3727f7d 100644 --- a/src/main/java/net/minestom/server/instance/WorldBorder.java +++ b/src/main/java/net/minestom/server/instance/WorldBorder.java @@ -32,7 +32,7 @@ public class WorldBorder { private int warningTime; private int warningBlocks; - protected WorldBorder(@NotNull Instance instance) { + public WorldBorder(@NotNull Instance instance) { this.instance = instance; this.oldDiameter = Double.MAX_VALUE; diff --git a/src/main/java/net/minestom/server/utils/ObjectPool.java b/src/main/java/net/minestom/server/utils/ObjectPool.java index 932b9621f..f27709e6e 100644 --- a/src/main/java/net/minestom/server/utils/ObjectPool.java +++ b/src/main/java/net/minestom/server/utils/ObjectPool.java @@ -1,7 +1,6 @@ package net.minestom.server.utils; import net.minestom.server.ServerFlag; -import net.minestom.server.network.socket.Server; import net.minestom.server.utils.binary.BinaryBuffer; import org.jctools.queues.MessagePassingQueue; import org.jctools.queues.MpmcUnboundedXaddArrayQueue; @@ -30,7 +29,7 @@ public final class ObjectPool { private final Supplier supplier; private final UnaryOperator sanitizer; - ObjectPool(Supplier supplier, UnaryOperator sanitizer) { + public ObjectPool(Supplier supplier, UnaryOperator sanitizer) { this.supplier = supplier; this.sanitizer = sanitizer; } diff --git a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java index 55dc5ecb2..e98affd82 100644 --- a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java +++ b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java @@ -950,7 +950,7 @@ public class EntityBlockPhysicsIntegrationTest { PhysicsResult res = CollisionUtils.handlePhysics(entity, Vec.ZERO); entity.teleport(res.newPosition()); - res = CollisionUtils.handlePhysics(entity, Vec.ZERO, res); + res = CollisionUtils.handlePhysics(entity, Vec.ZERO); assertEqualsPoint(new Pos(5, 42, 5), res.newPosition()); } @@ -969,7 +969,7 @@ public class EntityBlockPhysicsIntegrationTest { PhysicsResult res = CollisionUtils.handlePhysics(entity, Vec.ZERO); entity.teleport(res.newPosition()); - res = CollisionUtils.handlePhysics(entity, new Vec((distance - 1) * 16, 0, 0), res); + res = CollisionUtils.handlePhysics(entity, new Vec((distance - 1) * 16, 0, 0)); assertEqualsPoint(new Pos(distance * 8 - 0.3, 42, 5), res.newPosition()); } @@ -988,7 +988,7 @@ public class EntityBlockPhysicsIntegrationTest { PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec((distance - 1) * 16, 0, 0)); entity.teleport(res.newPosition()); - res = CollisionUtils.handlePhysics(entity, Vec.ZERO, res); + res = CollisionUtils.handlePhysics(entity, Vec.ZERO); assertEqualsPoint(new Pos(distance * 8 - 0.3, 42, 5), res.newPosition()); } @@ -1005,7 +1005,7 @@ public class EntityBlockPhysicsIntegrationTest { PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -0.4)); entity.teleport(res.newPosition()); - res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -0.4), res); + res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, -0.4)); assertEqualsPoint(new Pos(0.5, 42.5, 0.487), res.newPosition()); } @@ -1025,38 +1025,7 @@ public class EntityBlockPhysicsIntegrationTest { PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, 10)); entity.teleport(res.newPosition()); - res = CollisionUtils.handlePhysics(entity, new Vec(0, -10, 0), res); - - assertEqualsPoint(new Pos(0, 40, 0.7), res.newPosition()); - } - - @Test - public void entityPhysicsCheckGravityCached(Env env) { - var instance = env.createFlatInstance(); - instance.setBlock(0, 43, 1, Block.STONE); - - for (int i = -2; i <= 2; ++i) - for (int j = -2; j <= 2; ++j) - instance.loadChunk(i, j).join(); - - var entity = new Entity(EntityType.ZOMBIE); - entity.setInstance(instance, new Pos(0, 42, 0)).join(); - assertEquals(instance, entity.getInstance()); - - PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0, 0, 10)); - entity.teleport(res.newPosition()); - res = CollisionUtils.handlePhysics(entity, new Vec(0, -10, 0), res); - entity.teleport(res.newPosition()); - - PhysicsResult lastPhysicsResult; - - for (int x = 0; x < 50; ++x) { - lastPhysicsResult = res; - res = CollisionUtils.handlePhysics(entity, new Vec(0, -1.7, 0), res); - entity.teleport(res.newPosition()); - - if (x > 10) assertSame(lastPhysicsResult, res, "Physics result not cached"); - } + res = CollisionUtils.handlePhysics(entity, new Vec(0, -10, 0)); assertEqualsPoint(new Pos(0, 40, 0.7), res.newPosition()); } @@ -1070,7 +1039,7 @@ public class EntityBlockPhysicsIntegrationTest { entity.setInstance(instance, new Pos(0, 43.00001, 0)); var deltaPos = new Vec(0.0, -10, 0.0); - var physicsResult = CollisionUtils.handlePhysics(entity, deltaPos, null); + var physicsResult = CollisionUtils.handlePhysics(entity, deltaPos); var newPos = physicsResult.newPosition(); assertEquals(43, newPos.blockY()); @@ -1085,7 +1054,7 @@ public class EntityBlockPhysicsIntegrationTest { entity.setInstance(instance, new Pos(0, 43.5, 0)); var deltaPos = new Vec(0.0, -10, 0.0); - var physicsResult = CollisionUtils.handlePhysics(entity, deltaPos, null); + var physicsResult = CollisionUtils.handlePhysics(entity, deltaPos); var newPos = physicsResult.newPosition(); assertEquals(43, newPos.blockY());