Barebone API experiment

This commit is contained in:
themode 2024-03-23 20:07:21 +01:00
parent 7daf8d69b7
commit fa83e51b4e
17 changed files with 1728 additions and 100 deletions

View File

@ -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)
}

View File

@ -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.
* <p>
* 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<PlayerInfo> waitingPlayers = new ConcurrentLinkedQueue<>();
private final Instance instance = new Instance(DimensionType.OVERWORLD, new BlockHolder(DimensionType.OVERWORLD));
private final Map<Integer, Player> players = new HashMap<>();
private final Map<Integer, Entity> 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<Player> 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<ServerPacket.Play> 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<ClientPacket> packetQueue = new ConcurrentLinkedQueue<>();
volatile boolean online = true;
final AtomicReference<String> nameRef = new AtomicReference<>();
final AtomicReference<UUID> 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<ServerPacket.Play> initPackets() {
final DimensionType dimensionType = instance.dimensionType;
BlockHolder blockHolder = instance.blockHolder;
List<ServerPacket.Play> 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));
}
}
}

View File

@ -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.
* <p>
* 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;
}
}
}

View File

@ -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<Chunk> 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;
}
}
}

View File

@ -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<ClientPacket> {
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);
}
}
}

View File

@ -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<ByteBuffer> PACKET_POOL = new ObjectPool<>(() -> ByteBuffer.allocateDirect(ServerFlag.POOLED_BUFFER_SIZE), ByteBuffer::clear);
public static void readPackets(ByteBuffer buffer,
AtomicReference<ConnectionState> stateRef,
Consumer<ClientPacket> 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<ByteBuffer> 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<ServerPacket.Play> 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<ByteBuffer, Integer> reader, Consumer<ClientPacket> 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<ServerPacket.Play> packets) {
write(new NetworkContext.Packet.PlayList(packets));
}
sealed interface Packet {
record PacketIdPair(ServerPacket packet, int id) implements Packet {
}
record PlayList(List<ServerPacket.Play> packets, int[] exception) implements Packet {
public PlayList {
packets = List.copyOf(packets);
}
public PlayList(List<ServerPacket.Play> packets) {
this(packets, IntArrays.EMPTY_ARRAY);
}
}
}
final class Async implements NetworkContext {
final AtomicReference<ConnectionState> stateRef = new AtomicReference<>(ConnectionState.HANDSHAKE);
final MpmcUnboundedXaddArrayQueue<Packet> packetWriteQueue = new MpmcUnboundedXaddArrayQueue<>(1024);
final ReentrantLock writeLock = new ReentrantLock();
final Condition writeCondition = writeLock.newCondition();
@Override
public boolean read(Function<ByteBuffer, Integer> reader, Consumer<ClientPacket> consumer) {
try (ObjectPool<ByteBuffer>.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<ByteBuffer, Integer> 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<ByteBuffer>.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<ConnectionState> stateRef = new AtomicReference<>(ConnectionState.HANDSHAKE);
final Predicate<ByteBuffer> writer;
final ArrayDeque<Packet> packetWriteQueue = new ArrayDeque<>();
public Sync(Predicate<ByteBuffer> writer) {
this.writer = writer;
}
@Override
public boolean read(Function<ByteBuffer, Integer> reader, Consumer<ClientPacket> consumer) {
try (ObjectPool<ByteBuffer>.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<ByteBuffer>.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();
}
}
}
}

View File

@ -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<String, NBT>();
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));
}
}

View File

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

View File

@ -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<Integer, Entry> entries = new HashMap<>();
private final Long2ObjectMap<Chunk> 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<List<ServerPacket.Play>> initSupplier,
Supplier<List<ServerPacket.Play>> 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<Integer, NetworkContext.Packet> 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<ServerPacket.Play> packets = viewer.initSupplier.get();
consumer.accept(entryId, new NetworkContext.Packet.PlayList(packets));
}
if (viewer.receiver) {
final List<ServerPacket.Play> 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<ServerPacket.Play> packets = viewer.destroySupplier.get();
consumer.accept(entryId, new NetworkContext.Packet.PlayList(packets));
}
if (viewer.receiver) {
final List<ServerPacket.Play> 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<ServerPacket.Play> 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<List<ServerPacket.Play>> initSupplier;
private final Supplier<List<ServerPacket.Play>> 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<List<ServerPacket.Play>> initSupplier,
Supplier<List<ServerPacket.Play>> 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.
* <p>
* Useful for interest management.
*/
public static final class Broadcaster {
private final Int2ObjectMap<IntArrayList> entityIdMap = new Int2ObjectOpenHashMap<>();
private final List<ServerPacket.Play> 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<ServerPacket.Play> constPackets = List.copyOf(Broadcaster.this.packets);
public List<ServerPacket.Play> 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();
}
}
}
}

View File

@ -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) {

View File

@ -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
*

View File

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

View File

@ -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 {

View File

@ -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()) {

View File

@ -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;

View File

@ -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<T> {
private final Supplier<T> supplier;
private final UnaryOperator<T> sanitizer;
ObjectPool(Supplier<T> supplier, UnaryOperator<T> sanitizer) {
public ObjectPool(Supplier<T> supplier, UnaryOperator<T> sanitizer) {
this.supplier = supplier;
this.sanitizer = sanitizer;
}

View File

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