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