mirror of https://github.com/Minestom/Minestom.git
Barebone API experiment
This commit is contained in:
parent
7daf8d69b7
commit
fa83e51b4e
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue