Minestom/demo/src/main/java/net/minestom/scratch/Scratch.java

541 lines
22 KiB
Java

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.ScratchInventoryUtils;
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.item.ItemStack;
import net.minestom.server.item.Material;
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;
GameMode gameMode = GameMode.SURVIVAL;
Pos position;
Pos oldPosition;
ItemStack[] inventory = new ItemStack[46];
ItemStack cursor = ItemStack.AIR;
final ScratchFeature.Messaging messaging;
final ScratchFeature.Movement movement;
final ScratchFeature.ChunkLoading chunkLoading;
final ScratchFeature.EntityInteract entityInteract;
final ScratchFeature.BlockInteract blockInteract;
final ScratchFeature.InventoryHandling inventoryHandling;
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;
Arrays.fill(inventory, ItemStack.AIR);
inventory[0] = ItemStack.of(Material.STONE, 64);
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 gameMode == GameMode.CREATIVE;
}
@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.inventoryHandling = new ScratchFeature.InventoryHandling(new ScratchFeature.InventoryHandling.Mapping() {
@Override
public void setPlayerItem(int slot, ItemStack itemStack) {
inventory[slot] = itemStack;
}
@Override
public void setCursorItem(ItemStack itemStack) {
cursor = itemStack;
}
});
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, 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));
packets.add(ScratchInventoryUtils.makePlayerPacket(inventory, cursor));
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, null, null);
return new PlayerInfoUpdatePacket(EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED),
List.of(infoEntry));
}
}
}