Merge remote-tracking branch 'upstream/master'

This commit is contained in:
LeoDog896 2020-11-19 19:55:00 -05:00
commit 974372d2bd
135 changed files with 2958 additions and 1796 deletions

2
.github/README.md vendored
View File

@ -53,7 +53,7 @@ Minestom isn't perfect, our choices make it much better for some cases, worse fo
## Disadvantages ## Disadvantages
* Does not work with Bukkit/Spigot plugins * Does not work with Bukkit/Spigot plugins
* Does not work with older clients * Does not work with older clients (using a proxy with ViaBackwards is possible)
* Bad for those who want a vanilla experience * Bad for those who want a vanilla experience
* Longer to develop something playable * Longer to develop something playable
* Multi-threaded environments are prone to complications * Multi-threaded environments are prone to complications

27
.github/workflows/javadoc.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Build and deploy Javadoc
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Build javadoc
run: gradle javadoc
- name: Deploy javadoc to its assigned branch
uses: s0/git-publish-subdir-action@develop
env:
REPO: self
BRANCH: javadoc
FOLDER: docs
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

25
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Build and test Minestom
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Minestom
run: ./gradlew build
- name: Run tests
run: ./gradlew test

View File

@ -8,6 +8,10 @@ plugins {
id 'org.jetbrains.kotlin.jvm' version '1.4.10' id 'org.jetbrains.kotlin.jvm' version '1.4.10'
} }
group 'net.minestom.server'
version '1.0'
sourceCompatibility = 1.11
project.ext.lwjglVersion = "3.2.3" project.ext.lwjglVersion = "3.2.3"
switch (OperatingSystem.current()) { switch (OperatingSystem.current()) {
@ -37,17 +41,12 @@ allprojects {
} }
javadoc { javadoc {
options { options {
destinationDir(file("docs"))
addBooleanOption('html5', true) addBooleanOption('html5', true)
addBooleanOption('-no-module-directories', true)
} }
} }
} }
group 'net.minestom.server'
version '1.0'
sourceCompatibility = 1.11
sourceSets { sourceSets {
main { main {
java { java {
@ -101,12 +100,13 @@ dependencies {
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.6.2') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.6.2')
// Netty // Netty
api 'io.netty:netty-handler:4.1.52.Final' api 'io.netty:netty-handler:4.1.54.Final'
api 'io.netty:netty-codec:4.1.52.Final' api 'io.netty:netty-codec:4.1.54.Final'
implementation 'io.netty:netty-transport-native-epoll:4.1.52.Final:linux-x86_64' api 'io.netty:netty-transport-native-epoll:4.1.54.Final:linux-x86_64'
api 'io.netty:netty-transport-native-kqueue:4.1.54.Final:osx-x86_64'
// https://mvnrepository.com/artifact/it.unimi.dsi/fastutil // https://mvnrepository.com/artifact/it.unimi.dsi/fastutil
api 'it.unimi.dsi:fastutil:8.4.2' api 'it.unimi.dsi:fastutil:8.4.3'
// https://mvnrepository.com/artifact/com.google.code.gson/gson // https://mvnrepository.com/artifact/com.google.code.gson/gson
api 'com.google.code.gson:gson:2.8.6' api 'com.google.code.gson:gson:2.8.6'
@ -116,25 +116,21 @@ dependencies {
api 'com.github.Articdive:Jnoise:1.0.0' api 'com.github.Articdive:Jnoise:1.0.0'
// https://mvnrepository.com/artifact/org.rocksdb/rocksdbjni // https://mvnrepository.com/artifact/org.rocksdb/rocksdbjni
api 'org.rocksdb:rocksdbjni:6.11.4' api 'org.rocksdb:rocksdbjni:6.13.3'
// Logging // Logging
api 'org.apache.logging.log4j:log4j-core:2.13.3' api 'org.apache.logging.log4j:log4j-core:2.14.0'
// SLF4J is the base logger for most libraries, therefore we can hook it into log4j2. // SLF4J is the base logger for most libraries, therefore we can hook it into log4j2.
api 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3' api 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.0'
api 'com.mojang:authlib:1.5.21' api 'com.mojang:authlib:1.5.21'
api 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'
// Code modification // Code modification
api "org.ow2.asm:asm:${asmVersion}" api "org.ow2.asm:asm:${asmVersion}"
api "org.ow2.asm:asm-tree:${asmVersion}" api "org.ow2.asm:asm-tree:${asmVersion}"
api "org.ow2.asm:asm-analysis:${asmVersion}" api "org.ow2.asm:asm-analysis:${asmVersion}"
api "org.ow2.asm:asm-util:${asmVersion}" api "org.ow2.asm:asm-util:${asmVersion}"
api "org.ow2.asm:asm-commons:${asmVersion}" api "org.ow2.asm:asm-commons:${asmVersion}"
implementation 'com.google.guava:guava:21.0'
api "org.spongepowered:mixin:${mixinVersion}" api "org.spongepowered:mixin:${mixinVersion}"
// Path finding // Path finding

View File

@ -1,3 +1,3 @@
asmVersion=8.0.1 asmVersion=9.0
mixinVersion=0.8 mixinVersion=0.8.1
hephaistos_version=v1.1.5 hephaistos_version=v1.1.5

View File

@ -1,23 +1,21 @@
package net.minestom.codegen; package net.minestom.codegen;
import lombok.Getter;
import java.io.File; import java.io.File;
public class PrismarinePaths { public class PrismarinePaths {
@Getter private String blocks; private String blocks;
@Getter private String biomes; private String biomes;
@Getter private String effects; private String effects;
@Getter private String items; private String items;
@Getter private String recipes; private String recipes;
@Getter private String instruments; private String instruments;
@Getter private String materials; private String materials;
@Getter private String entities; private String entities;
@Getter private String protocol; private String protocol;
@Getter private String windows; private String windows;
@Getter private String version; private String version;
@Getter private String language; private String language;
public File getBlockFile() { public File getBlockFile() {
return getFile(blocks, "blocks"); return getFile(blocks, "blocks");
@ -32,6 +30,6 @@ public class PrismarinePaths {
} }
public File getFile(String path, String type) { public File getFile(String path, String type) {
return new File("prismarine-minecraft-data/data/"+path+"/"+type+".json"); return new File("prismarine-minecraft-data/data/" + path + "/" + type + ".json");
} }
} }

View File

@ -1,10 +1,5 @@
package net.minestom.server; package net.minestom.server;
import com.mojang.authlib.AuthenticationService;
import com.mojang.authlib.minecraft.MinecraftSessionService;
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import lombok.Getter;
import lombok.Setter;
import net.minestom.server.advancements.AdvancementManager; import net.minestom.server.advancements.AdvancementManager;
import net.minestom.server.benchmark.BenchmarkManager; import net.minestom.server.benchmark.BenchmarkManager;
import net.minestom.server.command.CommandManager; import net.minestom.server.command.CommandManager;
@ -16,7 +11,6 @@ import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.extensions.Extension; import net.minestom.server.extensions.Extension;
import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.extensions.ExtensionManager;
import net.minestom.server.extras.mojangAuth.MojangCrypt;
import net.minestom.server.fluids.Fluid; import net.minestom.server.fluids.Fluid;
import net.minestom.server.gamedata.loottables.LootTableManager; import net.minestom.server.gamedata.loottables.LootTableManager;
import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.gamedata.tags.TagManager;
@ -31,7 +25,6 @@ import net.minestom.server.item.Material;
import net.minestom.server.listener.manager.PacketListenerManager; import net.minestom.server.listener.manager.PacketListenerManager;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.netty.NettyServer; import net.minestom.server.network.netty.NettyServer;
import net.minestom.server.network.packet.server.play.PluginMessagePacket; import net.minestom.server.network.packet.server.play.PluginMessagePacket;
import net.minestom.server.network.packet.server.play.ServerDifficultyPacket; import net.minestom.server.network.packet.server.play.ServerDifficultyPacket;
@ -49,6 +42,7 @@ import net.minestom.server.storage.StorageLocation;
import net.minestom.server.storage.StorageManager; import net.minestom.server.storage.StorageManager;
import net.minestom.server.timer.SchedulerManager; import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.thread.MinestomThread; import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.Difficulty; import net.minestom.server.world.Difficulty;
@ -60,8 +54,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.Proxy;
import java.security.KeyPair;
import java.util.Collection; import java.util.Collection;
/** /**
@ -72,8 +64,7 @@ import java.util.Collection;
*/ */
public final class MinecraftServer { public final class MinecraftServer {
@Getter public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
private final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
public static final String VERSION_NAME = "1.16.4"; public static final String VERSION_NAME = "1.16.4";
public static final int PROTOCOL_VERSION = 754; public static final int PROTOCOL_VERSION = 754;
@ -85,9 +76,6 @@ public final class MinecraftServer {
public static final String THREAD_NAME_TICK = "Ms-Tick"; public static final String THREAD_NAME_TICK = "Ms-Tick";
public static final String THREAD_NAME_PACKET_WRITER = "Ms-PacketWriterPool";
public static final int THREAD_COUNT_PACKET_WRITER = 2;
public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool"; public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool";
public static final int THREAD_COUNT_BLOCK_BATCH = 2; public static final int THREAD_COUNT_BLOCK_BATCH = 2;
@ -103,21 +91,12 @@ public final class MinecraftServer {
private static final int MS_TO_SEC = 1000; private static final int MS_TO_SEC = 1000;
public static final int TICK_MS = MS_TO_SEC / TICK_PER_SECOND; public static final int TICK_MS = MS_TO_SEC / TICK_PER_SECOND;
@Getter // Network monitoring
@Setter private static int rateLimit = 300;
private static boolean hardcoreLook = false; private static int maxPacketSize = 30_000;
// Network
//Extras
@Getter
@Setter
private static boolean fixLighting = true;
//Rate Limiting
private static int rateLimit = 0;
// TODO
public static final int MAX_PACKET_SIZE = 300_000;
private static PacketListenerManager packetListenerManager; private static PacketListenerManager packetListenerManager;
private static PacketProcessor packetProcessor;
private static NettyServer nettyServer; private static NettyServer nettyServer;
// In-Game Manager // In-Game Manager
@ -154,14 +133,6 @@ public final class MinecraftServer {
private static LootTableManager lootTableManager; private static LootTableManager lootTableManager;
private static TagManager tagManager; private static TagManager tagManager;
//Mojang Auth
@Getter
private static final KeyPair keyPair = MojangCrypt.generateKeyPair();
@Getter
private static final AuthenticationService authService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, "");
@Getter
private static final MinecraftSessionService sessionService = authService.createMinecraftSessionService();
public static MinecraftServer init() { public static MinecraftServer init() {
if (minecraftServer != null) // don't init twice if (minecraftServer != null) // don't init twice
return minecraftServer; return minecraftServer;
@ -185,7 +156,7 @@ public final class MinecraftServer {
connectionManager = new ConnectionManager(); connectionManager = new ConnectionManager();
// Networking // Networking
final PacketProcessor packetProcessor = new PacketProcessor(); packetProcessor = new PacketProcessor();
packetListenerManager = new PacketListenerManager(); packetListenerManager = new PacketListenerManager();
instanceManager = new InstanceManager(); instanceManager = new InstanceManager();
@ -239,19 +210,17 @@ public final class MinecraftServer {
* @param brandName the server brand name * @param brandName the server brand name
* @throws NullPointerException if {@code brandName} is null * @throws NullPointerException if {@code brandName} is null
*/ */
@NotNull public static void setBrandName(@NotNull String brandName) {
public static void setBrandName(String brandName) {
Check.notNull(brandName, "The brand name cannot be null"); Check.notNull(brandName, "The brand name cannot be null");
MinecraftServer.brandName = brandName; MinecraftServer.brandName = brandName;
PluginMessagePacket brandMessage = PluginMessagePacket.getBrandPacket(); PacketUtils.sendGroupedPacket(connectionManager.getOnlinePlayers(), PluginMessagePacket.getBrandPacket());
PacketWriterUtils.writeAndSend(connectionManager.getOnlinePlayers(), brandMessage);
} }
/** /**
* Gets the max number of packets a client can send over 1 second. * Gets the maximum number of packets a client can send over 1 second.
* *
* @return the packet count limit over 1 second * @return the packet count limit over 1 second, 0 if not enabled
*/ */
public static int getRateLimit() { public static int getRateLimit() {
return rateLimit; return rateLimit;
@ -266,6 +235,24 @@ public final class MinecraftServer {
MinecraftServer.rateLimit = rateLimit; MinecraftServer.rateLimit = rateLimit;
} }
/**
* Gets the maximum packet size (in bytes) that a client can send without getting disconnected.
*
* @return the maximum packet size
*/
public static int getMaxPacketSize() {
return maxPacketSize;
}
/**
* Changes the maximum packet size (in bytes) that a client can send without getting disconnected.
*
* @param maxPacketSize the new max packet size
*/
public static void setMaxPacketSize(int maxPacketSize) {
MinecraftServer.maxPacketSize = maxPacketSize;
}
/** /**
* Gets the server difficulty showed in game option. * Gets the server difficulty showed in game option.
* *
@ -281,17 +268,15 @@ public final class MinecraftServer {
* *
* @param difficulty the new server difficulty * @param difficulty the new server difficulty
*/ */
@NotNull
public static void setDifficulty(@NotNull Difficulty difficulty) { public static void setDifficulty(@NotNull Difficulty difficulty) {
Check.notNull(difficulty, "The server difficulty cannot be null."); Check.notNull(difficulty, "The server difficulty cannot be null.");
MinecraftServer.difficulty = difficulty; MinecraftServer.difficulty = difficulty;
// The difficulty packet // Send the packet to all online players
ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket(); ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket();
serverDifficultyPacket.difficulty = difficulty; serverDifficultyPacket.difficulty = difficulty;
serverDifficultyPacket.locked = true; // Can only be modified on single-player serverDifficultyPacket.locked = true; // Can only be modified on single-player
// Send the packet to all online players PacketUtils.sendGroupedPacket(connectionManager.getOnlinePlayers(), serverDifficultyPacket);
PacketWriterUtils.writeAndSend(connectionManager.getOnlinePlayers(), serverDifficultyPacket);
} }
/** /**
@ -424,6 +409,18 @@ public final class MinecraftServer {
return connectionManager; return connectionManager;
} }
/**
* Gets the object handling the client packets processing.
* <p>
* Can be used if you want to convert a buffer to a client packet object.
*
* @return the packet processor
*/
public static PacketProcessor getPacketProcessor() {
checkInitStatus(packetProcessor);
return packetProcessor;
}
/** /**
* Gets if the server is up and running. * Gets if the server is up and running.
* *
@ -453,14 +450,16 @@ public final class MinecraftServer {
"The chunk view distance must be between 2 and 32"); "The chunk view distance must be between 2 and 32");
MinecraftServer.chunkViewDistance = chunkViewDistance; MinecraftServer.chunkViewDistance = chunkViewDistance;
if (started) { if (started) {
UpdateViewDistancePacket updateViewDistancePacket = new UpdateViewDistancePacket();
updateViewDistancePacket.viewDistance = chunkViewDistance;
final Collection<Player> players = connectionManager.getOnlinePlayers(); final Collection<Player> players = connectionManager.getOnlinePlayers();
PacketWriterUtils.writeAndSend(players, updateViewDistancePacket); UpdateViewDistancePacket updateViewDistancePacket = new UpdateViewDistancePacket();
updateViewDistancePacket.viewDistance = chunkViewDistance;
connectionManager.getOnlinePlayers().forEach(player -> { // Send packet to all online players
PacketUtils.sendGroupedPacket(players, updateViewDistancePacket);
players.forEach(player -> {
final Chunk playerChunk = player.getChunk(); final Chunk playerChunk = player.getChunk();
if (playerChunk != null) { if (playerChunk != null) {
player.refreshVisibleChunks(playerChunk); player.refreshVisibleChunks(playerChunk);
@ -625,11 +624,11 @@ public final class MinecraftServer {
extensionManager.getExtensions().forEach(Extension::initialize); extensionManager.getExtensions().forEach(Extension::initialize);
extensionManager.getExtensions().forEach(Extension::postInitialize); extensionManager.getExtensions().forEach(Extension::postInitialize);
MinecraftServer.started = true;
final double loadTime = MathUtils.round((t1 + System.nanoTime()) / 1_000_000D, 2); final double loadTime = MathUtils.round((t1 + System.nanoTime()) / 1_000_000D, 2);
LOGGER.info("Extensions loaded in " + loadTime + "ms"); LOGGER.info("Extensions loaded in " + loadTime + "ms");
LOGGER.info("Minestom server started successfully."); LOGGER.info("Minestom server started successfully.");
MinecraftServer.started = true;
} }
/** /**

View File

@ -1,23 +1,19 @@
package net.minestom.server; package net.minestom.server;
import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.EntityManager; import net.minestom.server.entity.EntityManager;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.InstanceManager;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.packet.server.play.KeepAlivePacket;
import net.minestom.server.thread.PerGroupChunkProvider; import net.minestom.server.thread.PerGroupChunkProvider;
import net.minestom.server.thread.ThreadProvider; import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.thread.MinestomThread; import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.function.DoubleConsumer; import java.util.function.LongConsumer;
/** /**
* Manager responsible for the server ticks. * Manager responsible for the server ticks.
@ -27,37 +23,30 @@ import java.util.function.DoubleConsumer;
*/ */
public final class UpdateManager { public final class UpdateManager {
private static final long KEEP_ALIVE_DELAY = 10_000;
private static final long KEEP_ALIVE_KICK = 30_000;
private static final ColoredText TIMEOUT_TEXT = ColoredText.of(ChatColor.RED + "Timeout");
private final ExecutorService mainUpdate = new MinestomThread(1, MinecraftServer.THREAD_NAME_MAIN_UPDATE); private final ExecutorService mainUpdate = new MinestomThread(1, MinecraftServer.THREAD_NAME_MAIN_UPDATE);
private boolean stopRequested; private boolean stopRequested;
private ThreadProvider threadProvider; private ThreadProvider threadProvider;
private final ConcurrentLinkedQueue<Runnable> tickStartCallbacks = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<DoubleConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
{ {
// DEFAULT THREAD PROVIDER // DEFAULT THREAD PROVIDER
//threadProvider = new PerInstanceThreadProvider();
threadProvider = new PerGroupChunkProvider(); threadProvider = new PerGroupChunkProvider();
} }
/** /**
* Should only be created in MinecraftServer * Should only be created in MinecraftServer.
*/ */
protected UpdateManager() { protected UpdateManager() {
} }
/** /**
* Starts the server loop in the update thread * Starts the server loop in the update thread.
*/ */
protected void start() { protected void start() {
mainUpdate.execute(() -> { mainUpdate.execute(() -> {
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
final EntityManager entityManager = MinecraftServer.getEntityManager(); final EntityManager entityManager = MinecraftServer.getEntityManager();
final long tickDistance = MinecraftServer.TICK_MS * 1000000; final long tickDistance = MinecraftServer.TICK_MS * 1000000;
@ -67,56 +56,25 @@ public final class UpdateManager {
final long tickStart = System.currentTimeMillis(); final long tickStart = System.currentTimeMillis();
// Tick start callbacks // Tick start callbacks
if (!tickStartCallbacks.isEmpty()) { doTickCallback(tickStartCallbacks, tickStart);
Runnable callback;
while ((callback = tickStartCallbacks.poll()) != null) {
callback.run();
}
}
List<Future<?>> futures;
// Server tick (instance/chunk/entity)
// Synchronize with the update manager instance, like the signal for chunk load/unload
synchronized (this) {
futures = threadProvider.update(tickStart);
}
// Waiting players update (newly connected clients waiting to get into the server) // Waiting players update (newly connected clients waiting to get into the server)
entityManager.updateWaitingPlayers(); entityManager.updateWaitingPlayers();
// Keep Alive Handling // Keep Alive Handling
final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(tickStart); entityManager.handleKeepAlive(tickStart);
for (Player player : connectionManager.getOnlinePlayers()) {
final long lastKeepAlive = tickStart - player.getLastKeepAlive();
if (lastKeepAlive > KEEP_ALIVE_DELAY && player.didAnswerKeepAlive()) {
player.refreshKeepAlive(tickStart);
player.getPlayerConnection().sendPacket(keepAlivePacket);
} else if (lastKeepAlive >= KEEP_ALIVE_KICK) {
player.kick(TIMEOUT_TEXT);
}
}
for (final Future<?> future : futures) { // Server tick (chunks/entities)
try { serverTick(tickStart);
future.get();
} catch (Throwable e) {
e.printStackTrace();
}
}
// the time that the tick took in nanoseconds
final long tickTime = System.nanoTime() - currentTime;
// Tick end callbacks // Tick end callbacks
if (!tickEndCallbacks.isEmpty()) { doTickCallback(tickEndCallbacks, tickTime / 1000000L);
final double tickEnd = (System.nanoTime() - currentTime) / 1000000D;
DoubleConsumer callback;
while ((callback = tickEndCallbacks.poll()) != null) {
callback.accept(tickEnd);
}
}
// Sleep until next tick // Sleep until next tick
final long sleepTime = Math.max(1, (tickDistance - (System.nanoTime() - currentTime)) / 1000000); final long sleepTime = Math.max(1, (tickDistance - tickTime) / 1000000L);
try { try {
Thread.sleep(sleepTime); Thread.sleep(sleepTime);
@ -128,6 +86,44 @@ public final class UpdateManager {
}); });
} }
/**
* Executes a server tick and returns only once all the futures are completed.
*
* @param tickStart the time of the tick in milliseconds
*/
private void serverTick(long tickStart) {
List<Future<?>> futures;
// Server tick (instance/chunk/entity)
// Synchronize with the update manager instance, like the signal for chunk load/unload
synchronized (this) {
futures = threadProvider.update(tickStart);
}
for (final Future<?> future : futures) {
try {
future.get();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
/**
* Used to execute tick-related callbacks.
*
* @param callbacks the callbacks to execute
* @param value the value to give to the consumers
*/
private void doTickCallback(ConcurrentLinkedQueue<LongConsumer> callbacks, long value) {
if (!callbacks.isEmpty()) {
LongConsumer callback;
while ((callback = callbacks.poll()) != null) {
callback.accept(value);
}
}
}
/** /**
* Gets the current {@link ThreadProvider}. * Gets the current {@link ThreadProvider}.
* *
@ -206,10 +202,12 @@ public final class UpdateManager {
/** /**
* Adds a callback executed at the start of the next server tick. * Adds a callback executed at the start of the next server tick.
* <p>
* The long in the consumer represents the starting time (in ms) of the tick.
* *
* @param callback the tick start callback * @param callback the tick start callback
*/ */
public void addTickStartCallback(Runnable callback) { public void addTickStartCallback(@NotNull LongConsumer callback) {
this.tickStartCallbacks.add(callback); this.tickStartCallbacks.add(callback);
} }
@ -218,18 +216,18 @@ public final class UpdateManager {
* *
* @param callback the callback to remove * @param callback the callback to remove
*/ */
public void removeTickStartCallback(Runnable callback) { public void removeTickStartCallback(@NotNull LongConsumer callback) {
this.tickStartCallbacks.remove(callback); this.tickStartCallbacks.remove(callback);
} }
/** /**
* Adds a callback executed at the end of the next server tick. * Adds a callback executed at the end of the next server tick.
* <p> * <p>
* The double in the consumer represents the duration (in ms) of the tick. * The long in the consumer represents the duration (in ms) of the tick.
* *
* @param callback the tick end callback * @param callback the tick end callback
*/ */
public void addTickEndCallback(DoubleConsumer callback) { public void addTickEndCallback(@NotNull LongConsumer callback) {
this.tickEndCallbacks.add(callback); this.tickEndCallbacks.add(callback);
} }
@ -238,12 +236,12 @@ public final class UpdateManager {
* *
* @param callback the callback to remove * @param callback the callback to remove
*/ */
public void removeTickEndCallback(DoubleConsumer callback) { public void removeTickEndCallback(@NotNull LongConsumer callback) {
this.tickEndCallbacks.remove(callback); this.tickEndCallbacks.remove(callback);
} }
/** /**
* Stops the server loop * Stops the server loop.
*/ */
public void stop() { public void stop() {
stopRequested = true; stopRequested = true;

View File

@ -1,11 +1,10 @@
package net.minestom.server; package net.minestom.server;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
/** /**
@ -56,7 +55,7 @@ public interface Viewable {
* @param packet the packet to send to all viewers * @param packet the packet to send to all viewers
*/ */
default void sendPacketToViewers(@NotNull ServerPacket packet) { default void sendPacketToViewers(@NotNull ServerPacket packet) {
PacketWriterUtils.writeAndSend(getViewers(), packet); PacketUtils.sendGroupedPacket(getViewers(), packet);
} }
/** /**
@ -69,40 +68,22 @@ public interface Viewable {
*/ */
default void sendPacketsToViewers(@NotNull ServerPacket... packets) { default void sendPacketsToViewers(@NotNull ServerPacket... packets) {
for (ServerPacket packet : packets) { for (ServerPacket packet : packets) {
PacketWriterUtils.writeAndSend(getViewers(), packet); PacketUtils.sendGroupedPacket(getViewers(), packet);
} }
} }
/** /**
* Sends a packet to all viewers and the viewable element if it is a player. * Sends a packet to all viewers and the viewable element if it is a player.
* <p> * <p>
* If 'this' isn't a player, then {@link #sendPacketToViewers(ServerPacket)} is called instead. * If 'this' isn't a player, then {only @link #sendPacketToViewers(ServerPacket)} is called.
* *
* @param packet the packet to send * @param packet the packet to send
*/ */
default void sendPacketToViewersAndSelf(@NotNull ServerPacket packet) { default void sendPacketToViewersAndSelf(@NotNull ServerPacket packet) {
if (this instanceof Player) { if (this instanceof Player) {
if (getViewers().isEmpty()) { ((Player) this).getPlayerConnection().sendPacket(packet);
PacketWriterUtils.writeAndSend((Player) this, packet);
} else {
UNSAFE_sendPacketToViewersAndSelf(packet);
}
} else {
sendPacketToViewers(packet);
} }
} sendPacketToViewers(packet);
/**
* Sends a packet to all the viewers and 'this'.
* <p>
* Unsafe because of a cast to {@link Player} without any check beforehand.
*
* @param packet the packet to send
*/
private void UNSAFE_sendPacketToViewersAndSelf(@NotNull ServerPacket packet) {
Set<Player> recipients = new HashSet<>(getViewers());
recipients.add((Player) this);
PacketWriterUtils.writeAndSend(recipients, packet);
} }
} }

View File

@ -1,15 +1,16 @@
package net.minestom.server.advancements; package net.minestom.server.advancements;
import io.netty.buffer.ByteBuf;
import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.Player;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
import net.minestom.server.network.packet.server.play.AdvancementsPacket; import net.minestom.server.network.packet.server.play.AdvancementsPacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Date; import java.util.Date;
import java.util.Set;
/** /**
* Represents an advancement located in an {@link AdvancementTab}. * Represents an advancement located in an {@link AdvancementTab}.
@ -373,17 +374,11 @@ public class Advancement {
updateCriteria(); updateCriteria();
if (tab != null) { if (tab != null) {
// Update the tab cached packet final Set<Player> viewers = tab.getViewers();
tab.updatePacket(); AdvancementsPacket createPacket = tab.createPacket();
final ByteBuf createBuffer = tab.createBuffer; PacketUtils.sendGroupedPacket(viewers, tab.removePacket);
final ByteBuf removeBuffer = tab.removeBuffer; PacketUtils.sendGroupedPacket(viewers, createPacket);
tab.getViewers().forEach(player -> {
final PlayerConnection playerConnection = player.getPlayerConnection();
// Receive order is important
playerConnection.sendPacket(removeBuffer, true);
playerConnection.sendPacket(createBuffer, true);
});
} }
} }

View File

@ -1,11 +1,9 @@
package net.minestom.server.advancements; package net.minestom.server.advancements;
import io.netty.buffer.ByteBuf;
import net.minestom.server.Viewable; import net.minestom.server.Viewable;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.play.AdvancementsPacket; import net.minestom.server.network.packet.server.play.AdvancementsPacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.advancement.AdvancementUtils; import net.minestom.server.utils.advancement.AdvancementUtils;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -14,7 +12,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
/** /**
* Represents a tab which can be shared between multiple players. * Represents a tab which can be shared between multiple players. Created using {@link AdvancementManager#createTab(String, AdvancementRoot)}.
* <p> * <p>
* Each tab requires a root advancement and all succeeding advancements need to have a parent in the tab. * Each tab requires a root advancement and all succeeding advancements need to have a parent in the tab.
* You can create a new advancement using {@link #createAdvancement(String, Advancement, Advancement)}. * You can create a new advancement using {@link #createAdvancement(String, Advancement, Advancement)}.
@ -33,19 +31,16 @@ public class AdvancementTab implements Viewable {
// Advancement -> its parent // Advancement -> its parent
private final Map<Advancement, Advancement> advancementMap = new HashMap<>(); private final Map<Advancement, Advancement> advancementMap = new HashMap<>();
// Packet cache, updated every time the tab changes
protected ByteBuf createBuffer;
// the packet used to clear the tab (used to remove it and to update an advancement) // the packet used to clear the tab (used to remove it and to update an advancement)
// will never change (since the root identifier is always the same) // will never change (since the root identifier is always the same)
protected final ByteBuf removeBuffer; protected final AdvancementsPacket removePacket;
protected AdvancementTab(@NotNull String rootIdentifier, @NotNull AdvancementRoot root) { protected AdvancementTab(@NotNull String rootIdentifier, @NotNull AdvancementRoot root) {
this.root = root; this.root = root;
cacheAdvancement(rootIdentifier, root, null); cacheAdvancement(rootIdentifier, root, null);
final AdvancementsPacket removePacket = AdvancementUtils.getRemovePacket(new String[]{rootIdentifier}); this.removePacket = AdvancementUtils.getRemovePacket(new String[]{rootIdentifier});
this.removeBuffer = PacketUtils.writePacket(removePacket);
} }
/** /**
@ -88,13 +83,6 @@ public class AdvancementTab implements Viewable {
} }
/**
* Updates the packet buffer.
*/
protected void updatePacket() {
this.createBuffer = PacketUtils.writePacket(createPacket());
}
/** /**
* Builds the packet which build the whole advancement tab. * Builds the packet which build the whole advancement tab.
* *
@ -135,8 +123,6 @@ public class AdvancementTab implements Viewable {
advancement.setParent(parent); advancement.setParent(parent);
advancement.updateCriteria(); advancement.updateCriteria();
this.advancementMap.put(advancement, parent); this.advancementMap.put(advancement, parent);
updatePacket();
} }
@Override @Override
@ -149,7 +135,7 @@ public class AdvancementTab implements Viewable {
final PlayerConnection playerConnection = player.getPlayerConnection(); final PlayerConnection playerConnection = player.getPlayerConnection();
// Send the tab to the player // Send the tab to the player
playerConnection.sendPacket(createBuffer, true); playerConnection.sendPacket(createPacket());
addPlayer(player); addPlayer(player);
@ -166,7 +152,7 @@ public class AdvancementTab implements Viewable {
// Remove the tab // Remove the tab
if (!player.isRemoved()) { if (!player.isRemoved()) {
playerConnection.sendPacket(removeBuffer, true); playerConnection.sendPacket(removePacket);
} }
removePlayer(player); removePlayer(player);

View File

@ -38,7 +38,6 @@ public final class BenchmarkManager {
THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
THREADS.add(THREAD_NAME_MAIN_UPDATE); THREADS.add(THREAD_NAME_MAIN_UPDATE);
THREADS.add(THREAD_NAME_PACKET_WRITER);
THREADS.add(THREAD_NAME_BLOCK_BATCH); THREADS.add(THREAD_NAME_BLOCK_BATCH);
THREADS.add(THREAD_NAME_SCHEDULER); THREADS.add(THREAD_NAME_SCHEDULER);
THREADS.add(THREAD_NAME_TICK); THREADS.add(THREAD_NAME_TICK);
@ -92,7 +91,7 @@ public final class BenchmarkManager {
} }
/** /**
* Gets the memory used by the server in bytes. * Gets the heap memory used by the server in bytes.
* *
* @return the memory used by the server * @return the memory used by the server
*/ */

View File

@ -15,7 +15,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* Represents a boss bar which is displayed on the top of the client screen (max amount of boss bar defined by {@link #MAX_BOSSBAR}). * Represents a boss bar which is displayed on the top of the client screen (max amount of boss bar defined by {@link #MAX_BOSSBAR}).
* <p> * <p>
* To use it, create a new instance and add the {@link Player} you want using {@link #addViewer(Player)} and remove them using {@link #removeViewer(Player)}. * To use it, create a new instance using the constructor
* and add the {@link Player} you want using {@link #addViewer(Player)} and remove them using {@link #removeViewer(Player)}.
* <p> * <p>
* You can retrieve all the boss bars of a {@link Player} with {@link #getBossBars(Player)}. * You can retrieve all the boss bars of a {@link Player} with {@link #getBossBars(Player)}.
*/ */

View File

@ -9,8 +9,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
// TODO format retention
/** /**
* Represents multiple {@link ColoredText} batched together with the possibility to add * Represents multiple {@link ColoredText} batched together with the possibility to add
* click and hover events. * click and hover events.
@ -42,18 +40,30 @@ public class RichMessage extends JsonMessage {
Check.notNull(coloredText, "ColoredText cannot be null"); Check.notNull(coloredText, "ColoredText cannot be null");
RichMessage richMessage = new RichMessage(); RichMessage richMessage = new RichMessage();
appendText(richMessage, coloredText, FormatRetention.ALL); appendText(richMessage, coloredText);
return richMessage; return richMessage;
} }
private static void appendText(@NotNull RichMessage richMessage, @NotNull ColoredText coloredText, private static void appendText(@NotNull RichMessage richMessage, @NotNull ColoredText coloredText) {
@NotNull FormatRetention formatRetention) { RichComponent component = new RichComponent(coloredText);
RichComponent component = new RichComponent(coloredText, formatRetention);
richMessage.components.add(component); richMessage.components.add(component);
richMessage.currentComponent = component; richMessage.currentComponent = component;
} }
/**
* Adds a new rich component to the message.
*
* @param coloredText the text composing the rich component
* @return the rich message
*/
public RichMessage append(@NotNull ColoredText coloredText) {
Check.notNull(coloredText, "ColoredText cannot be null");
appendText(this, coloredText);
return this;
}
/** /**
* Sets the click event of the current rich component. * Sets the click event of the current rich component.
* *
@ -87,31 +97,6 @@ public class RichMessage extends JsonMessage {
return this; return this;
} }
/**
* Adds a new rich component to the message.
*
* @param coloredText the text composing the rich component
* @param formatRetention the format retention of the added component
* @return the rich message
*/
public RichMessage append(@NotNull ColoredText coloredText, @NotNull FormatRetention formatRetention) {
Check.notNull(coloredText, "ColoredText cannot be null");
appendText(this, coloredText, formatRetention);
return this;
}
/**
* Adds a new rich component to the message,
* the format retention is set to {@link FormatRetention#ALL}.
*
* @param coloredText the text composing the rich component
* @return the rich message
*/
public RichMessage append(@NotNull ColoredText coloredText) {
return append(coloredText, FormatRetention.ALL);
}
@NotNull @NotNull
@Override @Override
public JsonObject getJsonObject() { public JsonObject getJsonObject() {
@ -121,18 +106,14 @@ public class RichMessage extends JsonMessage {
if (cacheComponents.isEmpty()) if (cacheComponents.isEmpty())
return new JsonObject(); return new JsonObject();
RichComponent firstComponent = cacheComponents.remove(0); // The main object contains the extra array, with an empty text to do not share its state with the others
List<JsonObject> firstComponentObjects = getComponentObject(firstComponent); JsonObject mainObject = new JsonObject();
JsonObject mainObject = firstComponentObjects.remove(0); mainObject.addProperty("text", "");
if (cacheComponents.isEmpty() && firstComponentObjects.isEmpty())
return mainObject;
// The extra array contains all the components
JsonArray extraArray = new JsonArray(); JsonArray extraArray = new JsonArray();
for (JsonObject firstComponentObject : firstComponentObjects) {
extraArray.add(firstComponentObject);
}
// Add all the components
for (RichComponent component : cacheComponents) { for (RichComponent component : cacheComponents) {
List<JsonObject> componentObjects = getComponentObject(component); List<JsonObject> componentObjects = getComponentObject(component);
for (JsonObject componentObject : componentObjects) { for (JsonObject componentObject : componentObjects) {
@ -205,24 +186,18 @@ public class RichMessage extends JsonMessage {
return eventObject; return eventObject;
} }
public enum FormatRetention {
ALL, CLICK_EVENT, HOVER_EVENT, NONE
}
/** /**
* Represents a {@link ColoredText} with a click and hover event (can be null). * Represents a {@link ColoredText} with a click and hover event (can be null).
*/ */
private static class RichComponent { private static class RichComponent {
private final ColoredText text; private final ColoredText text;
private final FormatRetention formatRetention;
private ChatClickEvent clickEvent; private ChatClickEvent clickEvent;
private ChatHoverEvent hoverEvent; private ChatHoverEvent hoverEvent;
private String insertion; private String insertion;
private RichComponent(@NotNull ColoredText text, @NotNull FormatRetention formatRetention) { private RichComponent(@NotNull ColoredText text) {
this.text = text; this.text = text;
this.formatRetention = formatRetention;
} }
@NotNull @NotNull
@ -230,11 +205,6 @@ public class RichMessage extends JsonMessage {
return text; return text;
} }
@NotNull
public FormatRetention getFormatRetention() {
return formatRetention;
}
@Nullable @Nullable
public ChatClickEvent getClickEvent() { public ChatClickEvent getClickEvent() {
return clickEvent; return clickEvent;

View File

@ -83,8 +83,15 @@ public final class CommandManager {
* Registers a {@link Command}. * Registers a {@link Command}.
* *
* @param command the command to register * @param command the command to register
* @throws IllegalStateException if a command with the same name already exists
*/ */
public void register(@NotNull Command command) { public synchronized void register(@NotNull Command command) {
Check.stateCondition(commandExists(command.getName()),
"A command with the name " + command.getName() + " is already registered!");
for (String alias : command.getAliases()) {
Check.stateCondition(commandExists(alias),
"A command with the name " + alias + " is already registered!");
}
this.dispatcher.register(command); this.dispatcher.register(command);
} }
@ -103,13 +110,20 @@ public final class CommandManager {
* Registers a {@link CommandProcessor}. * Registers a {@link CommandProcessor}.
* *
* @param commandProcessor the command to register * @param commandProcessor the command to register
* @throws IllegalStateException if a command with the same name already exists
*/ */
public void register(@NotNull CommandProcessor commandProcessor) { public synchronized void register(@NotNull CommandProcessor commandProcessor) {
this.commandProcessorMap.put(commandProcessor.getCommandName().toLowerCase(), commandProcessor); final String commandName = commandProcessor.getCommandName().toLowerCase();
Check.stateCondition(commandExists(commandName),
"A command with the name " + commandName + " is already registered!");
this.commandProcessorMap.put(commandName, commandProcessor);
// Register aliases // Register aliases
final String[] aliases = commandProcessor.getAliases(); final String[] aliases = commandProcessor.getAliases();
if (aliases != null && aliases.length > 0) { if (aliases != null && aliases.length > 0) {
for (String alias : aliases) { for (String alias : aliases) {
Check.stateCondition(commandExists(alias),
"A command with the name " + alias + " is already registered!");
this.commandProcessorMap.put(alias.toLowerCase(), commandProcessor); this.commandProcessorMap.put(alias.toLowerCase(), commandProcessor);
} }
} }
@ -126,6 +140,18 @@ public final class CommandManager {
return commandProcessorMap.get(commandName.toLowerCase()); return commandProcessorMap.get(commandName.toLowerCase());
} }
/**
* Gets if a command with the name {@code commandName} already exists or name.
*
* @param commandName the command name to check
* @return true if the command does exist
*/
public boolean commandExists(@NotNull String commandName) {
commandName = commandName.toLowerCase();
return dispatcher.findCommand(commandName) != null ||
commandProcessorMap.get(commandName) != null;
}
/** /**
* Executes a command for a {@link ConsoleSender}. * Executes a command for a {@link ConsoleSender}.
* *

View File

@ -12,6 +12,8 @@ import org.jetbrains.annotations.Nullable;
* <p> * <p>
* Tab-completion can be activated by overriding {@link #enableWritingTracking()} and return true, you should then listen to * Tab-completion can be activated by overriding {@link #enableWritingTracking()} and return true, you should then listen to
* {@link #onWrite(String)} and return the possible completions to suggest. * {@link #onWrite(String)} and return the possible completions to suggest.
* <p>
* Please be sure to check {@link net.minestom.server.command.builder.Command} as it is likely to be better for your use case.
*/ */
public interface CommandProcessor { public interface CommandProcessor {

View File

@ -1,18 +1,15 @@
package net.minestom.server.command; package net.minestom.server.command;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.permission.Permission; import net.minestom.server.permission.PermissionHandler;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
/** /**
* Represents something which can send commands to the server. * Represents something which can send commands to the server.
* <p> * <p>
* Main implementations are {@link Player} and {@link ConsoleSender}. * Main implementations are {@link Player} and {@link ConsoleSender}.
*/ */
public interface CommandSender { public interface CommandSender extends PermissionHandler {
/** /**
* Sends a raw string message. * Sends a raw string message.
@ -32,85 +29,6 @@ public interface CommandSender {
} }
} }
/**
* Returns all permissions associated to this command sender.
* The returned collection should be modified only by subclasses.
*
* @return the permissions of this command sender.
*/
@NotNull
Collection<Permission> getAllPermissions();
/**
* Adds a {@link Permission} to this commandSender
*
* @param permission the permission to add
*/
default void addPermission(@NotNull Permission permission) {
getAllPermissions().add(permission);
}
/**
* Removes a {@link Permission} from this commandSender
*
* @param permission the permission to remove
*/
default void removePermission(@NotNull Permission permission) {
getAllPermissions().remove(permission);
}
/**
* Checks if the given {@link Permission} is possessed by this command sender.
* Simple shortcut to <pre>getAllPermissions().contains(permission) &amp;&amp; permission.isValidFor(this)</pre> for readability.
*
* @param p permission to check against
* @return true if the sender has the permission and validate {@link Permission#isValidFor(CommandSender, Object)}
*/
default boolean hasPermission(@NotNull Permission p) {
return hasPermission(p, null);
}
default <T> boolean hasPermission(@NotNull Permission<T> p, @Nullable T data) {
return getAllPermissions().contains(p) && p.isValidFor(this, data);
}
/**
* Checks if the given {@link Permission} is possessed by this command sender.
* Will call {@link Permission#isValidFor(CommandSender, Object)} on all permissions that are an instance of {@code permissionClass}.
* If no matching permission is found, this result returns false.
*
* @param permissionClass the permission class to check
* @return true if the sender has the permission and validate {@link Permission#isValidFor(CommandSender, Object)}
* @see #getAllPermissions()
*/
default boolean hasPermission(@NotNull Class<? extends Permission> permissionClass) {
boolean result = true;
boolean foundPerm = false;
for (Permission p : getAllPermissions()) {
if (permissionClass.isInstance(p)) {
foundPerm = true;
result &= p.isValidFor(this, null);
}
}
if (!foundPerm)
return false;
return result;
}
default <T> boolean hasPermission(@NotNull Class<? extends Permission<T>> permissionClass, @Nullable T data) {
boolean result = true;
boolean foundPerm = false;
for (Permission p : getAllPermissions()) {
if (permissionClass.isInstance(p)) {
foundPerm = true;
result &= p.isValidFor(this, data);
}
}
if (!foundPerm)
return false;
return result;
}
/** /**
* Gets if the sender is a {@link Player}. * Gets if the sender is a {@link Player}.
* *
@ -133,6 +51,7 @@ public interface CommandSender {
* Casts this object to a {@link Player}. * Casts this object to a {@link Player}.
* No checks are performed, {@link ClassCastException} can very much happen. * No checks are performed, {@link ClassCastException} can very much happen.
* *
* @throws ClassCastException if 'this' is not a player
* @see #isPlayer() * @see #isPlayer()
*/ */
default Player asPlayer() { default Player asPlayer() {
@ -143,6 +62,7 @@ public interface CommandSender {
* Casts this object to a {@link ConsoleSender}. * Casts this object to a {@link ConsoleSender}.
* No checks are performed, {@link ClassCastException} can very much happen. * No checks are performed, {@link ClassCastException} can very much happen.
* *
* @throws ClassCastException if 'this' is not a console sender
* @see #isConsole() * @see #isConsole()
*/ */
default ConsoleSender asConsole() { default ConsoleSender asConsole() {

View File

@ -58,7 +58,7 @@ public class Command {
} }
/** /**
* Creates a {@link Command} with a name without any aliases. * Creates a {@link Command} with a name and no alias.
* *
* @param name the name of the command * @param name the name of the command
* @see #Command(String, String...) * @see #Command(String, String...)
@ -70,8 +70,10 @@ public class Command {
/** /**
* Gets the {@link CommandCondition}. * Gets the {@link CommandCondition}.
* <p> * <p>
* It is called no matter the syntax used and can be used to check permissions or * It is called after the parsing and just before the execution no matter the syntax used and can be used to check permissions or
* the {@link CommandSender} type. * the {@link CommandSender} type.
* <p>
* Worth mentioning that the condition is also used to know if the command known from a player (at connection).
* *
* @return the command condition, null if not any * @return the command condition, null if not any
*/ */
@ -84,6 +86,7 @@ public class Command {
* Sets the {@link CommandCondition}. * Sets the {@link CommandCondition}.
* *
* @param commandCondition the new command condition, null to do not call anything * @param commandCondition the new command condition, null to do not call anything
* @see #getCondition()
*/ */
public void setCondition(@Nullable CommandCondition commandCondition) { public void setCondition(@Nullable CommandCondition commandCondition) {
this.condition = commandCondition; this.condition = commandCondition;
@ -104,7 +107,7 @@ public class Command {
/** /**
* Adds a new syntax in the command. * Adds a new syntax in the command.
* <p> * <p>
* A syntax is simply a list of arguments. * A syntax is simply a list of arguments and an executor called when successfully parsed.
* *
* @param commandCondition the condition to use the syntax * @param commandCondition the condition to use the syntax
* @param executor the executor to call when the syntax is successfully received * @param executor the executor to call when the syntax is successfully received

View File

@ -17,6 +17,12 @@ public class CommandDispatcher {
private final Map<String, Command> commandMap = new HashMap<>(); private final Map<String, Command> commandMap = new HashMap<>();
private final Set<Command> commands = new HashSet<>(); private final Set<Command> commands = new HashSet<>();
/**
* Registers a command,
* be aware that registering a command name or alias will override the previous entry.
*
* @param command the command to register
*/
public void register(@NotNull Command command) { public void register(@NotNull Command command) {
this.commandMap.put(command.getName().toLowerCase(), command); this.commandMap.put(command.getName().toLowerCase(), command);

View File

@ -12,11 +12,11 @@ import org.jetbrains.annotations.NotNull;
public interface CommandExecutor { public interface CommandExecutor {
/** /**
* Executes the command callback once the syntax has been called (or the default executor) * Executes the command callback once the syntax has been called (or the default executor).
* *
* @param source the sender of the command * @param sender the sender of the command
* @param args contains all the parsed arguments, * @param args contains all the parsed arguments,
* the id is the one initialized when creating the argument object * the id is the one initialized when creating the argument object
*/ */
void apply(@NotNull CommandSender source, @NotNull Arguments args); void apply(@NotNull CommandSender sender, @NotNull Arguments args);
} }

View File

@ -3,6 +3,11 @@ package net.minestom.server.command.builder.arguments.relative;
import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.Argument;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/**
* Common interface for all the relative location arguments.
*
* @param <T> the relative location type
*/
public abstract class ArgumentRelative<T> extends Argument<T> { public abstract class ArgumentRelative<T> extends Argument<T> {
public static final String RELATIVE_CHAR = "~"; public static final String RELATIVE_CHAR = "~";

View File

@ -26,13 +26,23 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
// Check if each element is correct // Check if each element is correct
for (String element : split) { for (String element : split) {
if (!element.equals(RELATIVE_CHAR)) { if (!element.startsWith(RELATIVE_CHAR)) {
try { try {
// Will throw the exception if not an integer // Will throw the exception if not an integer
Integer.parseInt(element); Integer.parseInt(element);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return INVALID_NUMBER_ERROR; return INVALID_NUMBER_ERROR;
} }
} else {
if (element.length() > RELATIVE_CHAR.length()) {
try {
final String potentialNumber = element.substring(1);
// Will throw the exception if not an integer
Integer.parseInt(potentialNumber);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
return INVALID_NUMBER_ERROR;
}
}
} }
} }
@ -51,7 +61,8 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
for (int i = 0; i < split.length; i++) { for (int i = 0; i < split.length; i++) {
final String element = split[i]; final String element = split[i];
if (element.equals(RELATIVE_CHAR)) { if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) { if (i == 0) {
relativeX = true; relativeX = true;
} else if (i == 1) { } else if (i == 1) {
@ -59,6 +70,19 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
} else if (i == 2) { } else if (i == 2) {
relativeZ = true; relativeZ = true;
} }
if (element.length() != RELATIVE_CHAR.length()) {
final String potentialNumber = element.substring(1);
final int number = Integer.parseInt(potentialNumber);
if (i == 0) {
blockPosition.setX(number);
} else if (i == 1) {
blockPosition.setY(number);
} else if (i == 2) {
blockPosition.setZ(number);
}
}
} else { } else {
final int number = Integer.parseInt(element); final int number = Integer.parseInt(element);
if (i == 0) { if (i == 0) {

View File

@ -0,0 +1,54 @@
package net.minestom.server.command.builder.arguments.relative;
import net.minestom.server.utils.location.RelativeVec;
import org.jetbrains.annotations.NotNull;
/**
* Common super class for {@link ArgumentRelativeVec2} and {@link ArgumentRelativeVec3}.
*/
public abstract class ArgumentRelativeVec extends ArgumentRelative<RelativeVec> {
public ArgumentRelativeVec(@NotNull String id, int numberCount) {
super(id, numberCount);
}
@Override
public int getCorrectionResult(@NotNull String value) {
final String[] split = value.split(" ");
// Check if the value has enough element to be correct
if (split.length != getNumberCount()) {
return INVALID_NUMBER_COUNT_ERROR;
}
// Check if each element is correct
for (String element : split) {
if (!element.startsWith(RELATIVE_CHAR)) {
try {
// Will throw the exception if not a float
Float.parseFloat(element);
} catch (NumberFormatException e) {
return INVALID_NUMBER_ERROR;
}
} else {
if (element.length() > RELATIVE_CHAR.length()) {
try {
final String potentialNumber = element.substring(1);
// Will throw the exception if not a float
Float.parseFloat(potentialNumber);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
return INVALID_NUMBER_ERROR;
}
}
}
}
return SUCCESS;
}
@Override
public int getConditionResult(@NotNull RelativeVec value) {
return SUCCESS;
}
}

View File

@ -9,36 +9,12 @@ import org.jetbrains.annotations.NotNull;
* <p> * <p>
* Example: -1.2 ~ * Example: -1.2 ~
*/ */
public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> { public class ArgumentRelativeVec2 extends ArgumentRelativeVec {
public ArgumentRelativeVec2(@NotNull String id) { public ArgumentRelativeVec2(@NotNull String id) {
super(id, 2); super(id, 2);
} }
@Override
public int getCorrectionResult(@NotNull String value) {
final String[] split = value.split(" ");
// Check if the value has enough element to be correct
if (split.length != getNumberCount()) {
return INVALID_NUMBER_COUNT_ERROR;
}
// Check if each element is correct
for (String element : split) {
if (!element.equals(RELATIVE_CHAR)) {
try {
// Will throw the exception if not a float
Float.parseFloat(element);
} catch (NumberFormatException e) {
return INVALID_NUMBER_ERROR;
}
}
}
return SUCCESS;
}
@NotNull @NotNull
@Override @Override
public RelativeVec parse(@NotNull String value) { public RelativeVec parse(@NotNull String value) {
@ -50,12 +26,23 @@ public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> {
for (int i = 0; i < split.length; i++) { for (int i = 0; i < split.length; i++) {
final String element = split[i]; final String element = split[i];
if (element.equals(RELATIVE_CHAR)) { if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) { if (i == 0) {
relativeX = true; relativeX = true;
} else if (i == 1) { } else if (i == 1) {
relativeZ = true; relativeZ = true;
} }
if (element.length() != RELATIVE_CHAR.length()) {
final String potentialNumber = element.substring(1);
final float number = Float.parseFloat(potentialNumber);
if (i == 0) {
vector.setX(number);
} else if (i == 1) {
vector.setZ(number);
}
}
} else { } else {
final float number = Float.parseFloat(element); final float number = Float.parseFloat(element);
if (i == 0) { if (i == 0) {
@ -69,8 +56,4 @@ public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> {
return new RelativeVec(vector, relativeX, false, relativeZ); return new RelativeVec(vector, relativeX, false, relativeZ);
} }
@Override
public int getConditionResult(@NotNull RelativeVec value) {
return SUCCESS;
}
} }

View File

@ -9,36 +9,12 @@ import org.jetbrains.annotations.NotNull;
* <p> * <p>
* Example: -1.2 ~ 5 * Example: -1.2 ~ 5
*/ */
public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> { public class ArgumentRelativeVec3 extends ArgumentRelativeVec {
public ArgumentRelativeVec3(@NotNull String id) { public ArgumentRelativeVec3(@NotNull String id) {
super(id, 3); super(id, 3);
} }
@Override
public int getCorrectionResult(@NotNull String value) {
final String[] split = value.split(" ");
// Check if the value has enough element to be correct
if (split.length != getNumberCount()) {
return INVALID_NUMBER_COUNT_ERROR;
}
// Check if each element is correct
for (String element : split) {
if (!element.equals(RELATIVE_CHAR)) {
try {
// Will throw the exception if not a float
Float.parseFloat(element);
} catch (NumberFormatException e) {
return INVALID_NUMBER_ERROR;
}
}
}
return SUCCESS;
}
@NotNull @NotNull
@Override @Override
public RelativeVec parse(@NotNull String value) { public RelativeVec parse(@NotNull String value) {
@ -51,7 +27,8 @@ public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
for (int i = 0; i < split.length; i++) { for (int i = 0; i < split.length; i++) {
final String element = split[i]; final String element = split[i];
if (element.equals(RELATIVE_CHAR)) { if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) { if (i == 0) {
relativeX = true; relativeX = true;
} else if (i == 1) { } else if (i == 1) {
@ -59,6 +36,19 @@ public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
} else if (i == 2) { } else if (i == 2) {
relativeZ = true; relativeZ = true;
} }
if (element.length() != RELATIVE_CHAR.length()) {
final String potentialNumber = element.substring(1);
final float number = Float.parseFloat(potentialNumber);
if (i == 0) {
vector.setX(number);
} else if (i == 1) {
vector.setY(number);
} else if (i == 2) {
vector.setZ(number);
}
}
} else { } else {
final float number = Float.parseFloat(element); final float number = Float.parseFloat(element);
if (i == 0) { if (i == 0) {
@ -73,9 +63,4 @@ public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
return new RelativeVec(vector, relativeX, relativeY, relativeZ); return new RelativeVec(vector, relativeX, relativeY, relativeZ);
} }
@Override
public int getConditionResult(@NotNull RelativeVec value) {
return SUCCESS;
}
} }

View File

@ -4,6 +4,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -66,4 +67,17 @@ public class DataImpl implements Data {
return data; return data;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DataImpl data1 = (DataImpl) o;
return Objects.equals(data, data1.data) &&
Objects.equals(dataType, data1.dataType);
}
@Override
public int hashCode() {
return Objects.hash(data, dataType);
}
} }

View File

@ -14,7 +14,7 @@ import java.util.Map;
*/ */
public class NbtDataImpl extends DataImpl { public class NbtDataImpl extends DataImpl {
// Used to know if a nbt key is from a Data object, should NOT be changed // Used to know if a nbt key is from a Data object, should NOT be changed and used in a key name
public static final String KEY_PREFIX = "nbtdata_"; public static final String KEY_PREFIX = "nbtdata_";
@NotNull @NotNull
@ -45,7 +45,8 @@ public class NbtDataImpl extends DataImpl {
Check.notNull(nbt, Check.notNull(nbt,
"The type '" + type + "' is not supported within NbtDataImpl, if you wish to use a custom type you can encode the value into a byte array using a DataType"); "The type '" + type + "' is not supported within NbtDataImpl, if you wish to use a custom type you can encode the value into a byte array using a DataType");
nbtCompound.set(KEY_PREFIX + key, nbt); final String finalKey = KEY_PREFIX + key;
nbtCompound.set(finalKey, nbt);
} }
} }

View File

@ -18,7 +18,7 @@ public interface SerializableData extends Data {
DataManager DATA_MANAGER = MinecraftServer.getDataManager(); DataManager DATA_MANAGER = MinecraftServer.getDataManager();
@Override @Override
<T> void set(@NotNull String key, @Nullable T value, @NotNull Class<T> type); <T> void set(@NotNull String key, @Nullable T value, @Nullable Class<T> type);
/** /**
* Serializes the data into an array of bytes. * Serializes the data into an array of bytes.
@ -109,7 +109,7 @@ public interface SerializableData extends Data {
{ {
final int dataIndexSize = binaryReader.readVarInt(); final int dataIndexSize = binaryReader.readVarInt();
for (int i = 0; i < dataIndexSize; i++) { for (int i = 0; i < dataIndexSize; i++) {
final String className = binaryReader.readSizedString(); final String className = binaryReader.readSizedString(Integer.MAX_VALUE);
final short classIndex = binaryReader.readShort(); final short classIndex = binaryReader.readShort();
typeToIndexMap.put(className, classIndex); typeToIndexMap.put(className, classIndex);
} }

View File

@ -92,6 +92,7 @@ public class SerializableDataImpl extends DataImpl implements SerializableData {
// Write the data (no length) // Write the data (no length)
final DataType dataType = DATA_MANAGER.getDataType(type); final DataType dataType = DATA_MANAGER.getDataType(type);
Check.notNull(dataType, "Tried to encode a type not registered in DataManager: " + type);
dataType.encode(binaryWriter, value); dataType.encode(binaryWriter, value);
} }
@ -149,7 +150,7 @@ public class SerializableDataImpl extends DataImpl implements SerializableData {
} }
// Get the key // Get the key
final String name = reader.readSizedString(); final String name = reader.readSizedString(Integer.MAX_VALUE);
// Get the data // Get the data
final Object value; final Object value;

View File

@ -27,8 +27,8 @@ public class InventoryData extends DataType<Inventory> {
@NotNull @NotNull
@Override @Override
public Inventory decode(@NotNull BinaryReader reader) { public Inventory decode(@NotNull BinaryReader reader) {
final String title = reader.readSizedString(); final String title = reader.readSizedString(Integer.MAX_VALUE);
final InventoryType inventoryType = InventoryType.valueOf(reader.readSizedString()); final InventoryType inventoryType = InventoryType.valueOf(reader.readSizedString(Integer.MAX_VALUE));
final int size = inventoryType.getAdditionalSlot(); final int size = inventoryType.getAdditionalSlot();
Inventory inventory = new Inventory(inventoryType, title); Inventory inventory = new Inventory(inventoryType, title);

View File

@ -15,6 +15,6 @@ public class StringData extends DataType<String> {
@NotNull @NotNull
@Override @Override
public String decode(@NotNull BinaryReader reader) { public String decode(@NotNull BinaryReader reader) {
return reader.readSizedString(); return reader.readSizedString(Integer.MAX_VALUE);
} }
} }

View File

@ -9,19 +9,12 @@ public class StringArrayData extends DataType<String[]> {
@Override @Override
public void encode(@NotNull BinaryWriter writer, @NotNull String[] value) { public void encode(@NotNull BinaryWriter writer, @NotNull String[] value) {
writer.writeVarInt(value.length); writer.writeStringArray(value);
for (String val : value) {
writer.writeSizedString(val);
}
} }
@NotNull @NotNull
@Override @Override
public String[] decode(@NotNull BinaryReader reader) { public String[] decode(@NotNull BinaryReader reader) {
String[] array = new String[reader.readVarInt()]; return reader.readSizedStringArray(Integer.MAX_VALUE);
for (int i = 0; i < array.length; i++) {
array[i] = reader.readSizedString();
}
return array;
} }
} }

View File

@ -20,6 +20,8 @@ import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.WorldBorder;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.packet.server.play.*;
import net.minestom.server.permission.Permission;
import net.minestom.server.permission.PermissionHandler;
import net.minestom.server.thread.ThreadProvider; import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position; import net.minestom.server.utils.Position;
@ -41,7 +43,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
public abstract class Entity implements Viewable, EventHandler, DataContainer { /**
* Could be a player, a monster, or an object.
* <p>
* To create your own entity you probably want to extends {@link ObjectEntity} or {@link EntityCreature} instead.
*/
public abstract class Entity implements Viewable, EventHandler, DataContainer, PermissionHandler {
private static final Map<Integer, Entity> entityById = new ConcurrentHashMap<>(); private static final Map<Integer, Entity> entityById = new ConcurrentHashMap<>();
private static final AtomicInteger lastEntityId = new AtomicInteger(); private static final AtomicInteger lastEntityId = new AtomicInteger();
@ -82,8 +89,9 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
private boolean autoViewable; private boolean autoViewable;
private final int id; private final int id;
private Data data;
protected final Set<Player> viewers = new CopyOnWriteArraySet<>(); protected final Set<Player> viewers = new CopyOnWriteArraySet<>();
private Data data;
private final List<Permission> permissions = new LinkedList<>();
protected UUID uuid; protected UUID uuid;
private boolean isActive; // False if entity has only been instanced without being added somewhere private boolean isActive; // False if entity has only been instanced without being added somewhere
@ -139,6 +147,10 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
setVelocityUpdatePeriod(5); setVelocityUpdatePeriod(5);
} }
public Entity(@NotNull EntityType entityType) {
this(entityType, new Position());
}
/** /**
* Schedules a task to be run during the next entity tick. * Schedules a task to be run during the next entity tick.
* It ensures that the task will be executed in the same thread as the entity (depending of the {@link ThreadProvider}). * It ensures that the task will be executed in the same thread as the entity (depending of the {@link ThreadProvider}).
@ -149,12 +161,12 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
this.nextTick.add(callback); this.nextTick.add(callback);
} }
public Entity(@NotNull EntityType entityType) {
this(entityType, new Position());
}
/** /**
* @param id the entity unique id ({@link #getEntityId()}) * Gets an entity based on its id (from {@link #getEntityId()}).
* <p>
* Entity id are unique server-wide.
*
* @param id the entity unique id
* @return the entity having the specified id, null if not found * @return the entity having the specified id, null if not found
*/ */
@Nullable @Nullable
@ -169,7 +181,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
/** /**
* Called each tick. * Called each tick.
* *
* @param time the time of update in milliseconds * @param time time of the update in milliseconds
*/ */
public abstract void update(long time); public abstract void update(long time);
@ -341,6 +353,12 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
this.data = data; this.data = data;
} }
@NotNull
@Override
public Collection<Permission> getAllPermissions() {
return permissions;
}
/** /**
* Updates the entity, called every tick. * Updates the entity, called every tick.
* <p> * <p>
@ -365,8 +383,9 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
return; return;
} }
BlockPosition blockPosition = position.toBlockPosition(); final Chunk currentChunk = getChunk(); // current entity chunk
if (!ChunkUtils.isLoaded(instance, position.getX(), position.getZ()) || !ChunkUtils.isLoaded(instance, blockPosition.getX(), blockPosition.getZ())) {
if (!ChunkUtils.isLoaded(currentChunk)) {
// No update for entities in unloaded chunk // No update for entities in unloaded chunk
return; return;
} }
@ -454,8 +473,8 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
float drag; float drag;
if (onGround) { if (onGround) {
final CustomBlock customBlock = final BlockPosition blockPosition = position.toBlockPosition();
instance.getCustomBlock(blockPosition); final CustomBlock customBlock = instance.getCustomBlock(blockPosition);
if (customBlock != null) { if (customBlock != null) {
// Custom drag // Custom drag
drag = customBlock.getDrag(instance, blockPosition); drag = customBlock.getDrag(instance, blockPosition);
@ -484,6 +503,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
} }
// handle block contacts // handle block contacts
// TODO do not call every tick (it is pretty expensive)
final int minX = (int) Math.floor(boundingBox.getMinX()); final int minX = (int) Math.floor(boundingBox.getMinX());
final int maxX = (int) Math.ceil(boundingBox.getMaxX()); final int maxX = (int) Math.ceil(boundingBox.getMaxX());
final int minY = (int) Math.floor(boundingBox.getMinY()); final int minY = (int) Math.floor(boundingBox.getMinY());
@ -576,11 +596,10 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
} }
/** /**
* Each entity has an unique id which will change after a restart. * Each entity has an unique id (server-wide) which will change after a restart.
* <p>
* All entities can be retrieved by calling {@link Entity#getEntity(int)}.
* *
* @return the unique entity id * @return the unique entity id
* @see Entity#getEntity(int) to retrive an entity based on its id
*/ */
public int getEntityId() { public int getEntityId() {
return id; return id;

View File

@ -1,10 +1,15 @@
package net.minestom.server.entity; package net.minestom.server.entity;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.event.player.PlayerLoginEvent; import net.minestom.server.event.player.PlayerLoginEvent;
import net.minestom.server.event.player.PlayerPreLoginEvent; import net.minestom.server.event.player.PlayerPreLoginEvent;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.packet.server.play.KeepAlivePacket;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@ -12,10 +17,16 @@ import java.util.function.Consumer;
public final class EntityManager { public final class EntityManager {
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private static final long KEEP_ALIVE_DELAY = 10_000;
private static final long KEEP_ALIVE_KICK = 30_000;
private static final ColoredText TIMEOUT_TEXT = ColoredText.of(ChatColor.RED + "Timeout");
private final ConcurrentLinkedQueue<Player> waitingPlayers = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<Player> waitingPlayers = new ConcurrentLinkedQueue<>();
/** /**
* Connect waiting players * Connects waiting players.
*/ */
public void updateWaitingPlayers() { public void updateWaitingPlayers() {
// Connect waiting players // Connect waiting players
@ -23,7 +34,25 @@ public final class EntityManager {
} }
/** /**
* Add connected clients after the handshake (used to free the networking threads) * Updates keep alive by checking the last keep alive packet and send a new one if needed.
*
* @param tickStart the time of the update in milliseconds, forwarded to the packet
*/
public void handleKeepAlive(long tickStart) {
final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(tickStart);
for (Player player : CONNECTION_MANAGER.getOnlinePlayers()) {
final long lastKeepAlive = tickStart - player.getLastKeepAlive();
if (lastKeepAlive > KEEP_ALIVE_DELAY && player.didAnswerKeepAlive()) {
player.refreshKeepAlive(tickStart);
player.getPlayerConnection().sendPacket(keepAlivePacket);
} else if (lastKeepAlive >= KEEP_ALIVE_KICK) {
player.kick(TIMEOUT_TEXT);
}
}
}
/**
* Adds connected clients after the handshake (used to free the networking threads).
*/ */
private void waitingPlayersTick() { private void waitingPlayersTick() {
Player waitingPlayer; Player waitingPlayer;
@ -41,14 +70,14 @@ public final class EntityManager {
} }
/** /**
* Call the player initialization callbacks and the event {@link PlayerPreLoginEvent}. * Calls the player initialization callbacks and the event {@link PlayerPreLoginEvent}.
* If the {@link Player} hasn't been kicked, add him to the waiting list. * If the {@link Player} hasn't been kicked, add him to the waiting list.
* <p> * <p>
* Can be considered as a pre-init thing. * Can be considered as a pre-init thing.
* *
* @param player the {@link Player} to add * @param player the {@link Player} to add
*/ */
public void addWaitingPlayer(Player player) { public void addWaitingPlayer(@NotNull Player player) {
// Init player (register events) // Init player (register events)
for (Consumer<Player> playerInitialization : MinecraftServer.getConnectionManager().getPlayerInitializations()) { for (Consumer<Player> playerInitialization : MinecraftServer.getConnectionManager().getPlayerInitializations()) {

View File

@ -19,7 +19,9 @@ import net.minestom.server.sound.Sound;
import net.minestom.server.sound.SoundCategory; import net.minestom.server.sound.SoundCategory;
import net.minestom.server.utils.Position; import net.minestom.server.utils.Position;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.time.CooldownUtils;
import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -29,7 +31,11 @@ import java.util.function.Consumer;
public abstract class LivingEntity extends Entity implements EquipmentHandler { public abstract class LivingEntity extends Entity implements EquipmentHandler {
// Item pickup
protected boolean canPickupItem; protected boolean canPickupItem;
protected UpdateOption itemPickupCooldown = new UpdateOption(5, TimeUnit.TICK);
private long lastItemPickupCheckTime;
protected boolean isDead; protected boolean isDead;
private float health; private float health;
@ -90,8 +96,10 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
} }
// Items picking // Items picking
if (canPickupItem()) { if (canPickupItem() && !CooldownUtils.hasCooldown(time, lastItemPickupCheckTime, itemPickupCooldown)) {
final Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks this.lastItemPickupCheckTime = time;
final Chunk chunk = getChunk(); // TODO check surrounding chunks
final Set<Entity> entities = instance.getChunkEntities(chunk); final Set<Entity> entities = instance.getChunkEntities(chunk);
for (Entity entity : entities) { for (Entity entity : entities) {
if (entity instanceof ItemEntity) { if (entity instanceof ItemEntity) {
@ -305,7 +313,10 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
soundCategory = SoundCategory.HOSTILE; soundCategory = SoundCategory.HOSTILE;
} }
SoundEffectPacket damageSoundPacket = SoundEffectPacket.create(soundCategory, sound, getPosition().getX(), getPosition().getY(), getPosition().getZ(), 1.0f, 1.0f); SoundEffectPacket damageSoundPacket =
SoundEffectPacket.create(soundCategory, sound,
getPosition().getX(), getPosition().getY(), getPosition().getZ(),
1.0f, 1.0f);
sendPacketToViewersAndSelf(damageSoundPacket); sendPacketToViewersAndSelf(damageSoundPacket);
} }
}); });

View File

@ -37,7 +37,6 @@ import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.packet.server.play.*;
import net.minestom.server.network.player.NettyPlayerConnection; import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.permission.Permission;
import net.minestom.server.recipe.Recipe; import net.minestom.server.recipe.Recipe;
import net.minestom.server.recipe.RecipeManager; import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.resourcepack.ResourcePack; import net.minestom.server.resourcepack.ResourcePack;
@ -55,6 +54,10 @@ import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.chunk.ChunkCallback; import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.instance.InstanceUtils; import net.minestom.server.utils.instance.InstanceUtils;
import net.minestom.server.utils.player.PlayerUtils;
import net.minestom.server.utils.time.CooldownUtils;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.DimensionType; import net.minestom.server.world.DimensionType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -126,6 +129,10 @@ public class Player extends LivingEntity implements CommandSender {
private byte targetStage; // The current stage of the target block, only if multi player breaking is disabled private byte targetStage; // The current stage of the target block, only if multi player breaking is disabled
private final Set<Player> targetBreakers = new HashSet<>(1); // Only used if multi player breaking is disabled, contains only this player private final Set<Player> targetBreakers = new HashSet<>(1); // Only used if multi player breaking is disabled, contains only this player
// Experience orb pickup
protected UpdateOption experiencePickupCooldown = new UpdateOption(10, TimeUnit.TICK);
private long lastExperiencePickupCheckTime;
private BelowNameTag belowNameTag; private BelowNameTag belowNameTag;
private int permissionLevel; private int permissionLevel;
@ -148,8 +155,6 @@ public class Player extends LivingEntity implements CommandSender {
// Tick related // Tick related
private final PlayerTickEvent playerTickEvent = new PlayerTickEvent(this); private final PlayerTickEvent playerTickEvent = new PlayerTickEvent(this);
private final List<Permission> permissions = new LinkedList<>();
public Player(UUID uuid, String username, PlayerConnection playerConnection) { public Player(UUID uuid, String username, PlayerConnection playerConnection) {
super(EntityType.PLAYER); super(EntityType.PLAYER);
this.uuid = uuid; // Override Entity#uuid defined in the constructor this.uuid = uuid; // Override Entity#uuid defined in the constructor
@ -210,13 +215,13 @@ public class Player extends LivingEntity implements CommandSender {
playerConnection.sendPacket(serverDifficultyPacket); playerConnection.sendPacket(serverDifficultyPacket);
SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket(); SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket();
spawnPositionPacket.x = 0; spawnPositionPacket.x = (int) respawnPoint.getX();
spawnPositionPacket.y = 0; spawnPositionPacket.y = (int) respawnPoint.getY();
spawnPositionPacket.z = 0; spawnPositionPacket.z = (int) respawnPoint.getZ();
playerConnection.sendPacket(spawnPositionPacket); playerConnection.sendPacket(spawnPositionPacket);
// Add player to list with spawning skin // Add player to list with spawning skin
PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this); PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this, skin);
callEvent(PlayerSkinInitEvent.class, skinInitEvent); callEvent(PlayerSkinInitEvent.class, skinInitEvent);
this.skin = skinInitEvent.getSkin(); this.skin = skinInitEvent.getSkin();
playerConnection.sendPacket(getAddPlayerToList()); playerConnection.sendPacket(getAddPlayerToList());
@ -291,9 +296,13 @@ public class Player extends LivingEntity implements CommandSender {
@Override @Override
public void update(long time) { public void update(long time) {
// Flush all pending packets
playerConnection.flush();
// Flush all pending packets
if (PlayerUtils.isNettyClient(this)) {
((NettyPlayerConnection) playerConnection).getChannel().flush();
}
// Network tick verification
playerConnection.updateStats(); playerConnection.updateStats();
// Process received packets // Process received packets
@ -338,7 +347,8 @@ public class Player extends LivingEntity implements CommandSender {
final Chunk chunk = instance.getChunkAt(targetBlockPosition); final Chunk chunk = instance.getChunkAt(targetBlockPosition);
final int entityId = targetCustomBlock.getBreakEntityId(this); final int entityId = targetCustomBlock.getBreakEntityId(this);
final BlockBreakAnimationPacket blockBreakAnimationPacket = new BlockBreakAnimationPacket(entityId, targetBlockPosition, targetStage); final BlockBreakAnimationPacket blockBreakAnimationPacket =
new BlockBreakAnimationPacket(entityId, targetBlockPosition, targetStage);
Check.notNull(chunk, "Tried to interact with an unloaded chunk."); Check.notNull(chunk, "Tried to interact with an unloaded chunk.");
chunk.sendPacketToViewers(blockBreakAnimationPacket); chunk.sendPacketToViewers(blockBreakAnimationPacket);
@ -350,20 +360,24 @@ public class Player extends LivingEntity implements CommandSender {
} }
// Experience orb pickup // Experience orb pickup
final Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks if (!CooldownUtils.hasCooldown(time, lastExperiencePickupCheckTime, experiencePickupCooldown)) {
final Set<Entity> entities = instance.getChunkEntities(chunk); this.lastExperiencePickupCheckTime = time;
for (Entity entity : entities) {
if (entity instanceof ExperienceOrb) { final Chunk chunk = getChunk(); // TODO check surrounding chunks
final ExperienceOrb experienceOrb = (ExperienceOrb) entity; final Set<Entity> entities = instance.getChunkEntities(chunk);
final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox(); for (Entity entity : entities) {
if (expandedBoundingBox.intersect(itemBoundingBox)) { if (entity instanceof ExperienceOrb) {
if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled()) final ExperienceOrb experienceOrb = (ExperienceOrb) entity;
continue; final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox();
PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(experienceOrb); if (expandedBoundingBox.intersect(itemBoundingBox)) {
callCancellableEvent(PickupExperienceEvent.class, pickupExperienceEvent, () -> { if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled())
short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player continue;
entity.remove(); PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(experienceOrb);
}); callCancellableEvent(PickupExperienceEvent.class, pickupExperienceEvent, () -> {
short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player
entity.remove();
});
}
} }
} }
} }
@ -414,11 +428,6 @@ public class Player extends LivingEntity implements CommandSender {
entityPositionAndRotationPacket.pitch = position.getPitch(); entityPositionAndRotationPacket.pitch = position.getPitch();
entityPositionAndRotationPacket.onGround = onGround; entityPositionAndRotationPacket.onGround = onGround;
lastX = position.getX();
lastY = position.getY();
lastZ = position.getZ();
lastYaw = position.getYaw();
lastPitch = position.getPitch();
updatePacket = entityPositionAndRotationPacket; updatePacket = entityPositionAndRotationPacket;
} else if (positionChanged) { } else if (positionChanged) {
EntityPositionPacket entityPositionPacket = new EntityPositionPacket(); EntityPositionPacket entityPositionPacket = new EntityPositionPacket();
@ -427,9 +436,7 @@ public class Player extends LivingEntity implements CommandSender {
entityPositionPacket.deltaY = (short) ((position.getY() * 32 - lastY * 32) * 128); entityPositionPacket.deltaY = (short) ((position.getY() * 32 - lastY * 32) * 128);
entityPositionPacket.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128); entityPositionPacket.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128);
entityPositionPacket.onGround = onGround; entityPositionPacket.onGround = onGround;
lastX = position.getX();
lastY = position.getY();
lastZ = position.getZ();
updatePacket = entityPositionPacket; updatePacket = entityPositionPacket;
} else { } else {
// View changed // View changed
@ -439,12 +446,11 @@ public class Player extends LivingEntity implements CommandSender {
entityRotationPacket.pitch = position.getPitch(); entityRotationPacket.pitch = position.getPitch();
entityRotationPacket.onGround = onGround; entityRotationPacket.onGround = onGround;
lastYaw = position.getYaw();
lastPitch = position.getPitch();
updatePacket = entityRotationPacket; updatePacket = entityRotationPacket;
} }
if (viewChanged) { if (viewChanged) {
// Yaw from the rotation packet seems to be ignored, which is why this is required
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket(); EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId(); entityHeadLookPacket.entityId = getEntityId();
entityHeadLookPacket.yaw = position.getYaw(); entityHeadLookPacket.yaw = position.getYaw();
@ -466,11 +472,22 @@ public class Player extends LivingEntity implements CommandSender {
} }
} }
if (positionChanged) {
lastX = position.getX();
lastY = position.getY();
lastZ = position.getZ();
}
if (viewChanged) {
lastYaw = position.getYaw();
lastPitch = position.getPitch();
}
} }
@Override @Override
public void kill() { public void kill() {
if (!isDead()) { if (!isDead()) {
// send death screen text to the killed player // send death screen text to the killed player
{ {
ColoredText deathText; ColoredText deathText;
@ -608,6 +625,16 @@ public class Player extends LivingEntity implements CommandSender {
return result; return result;
} }
/**
* Changes the player instance and load surrounding chunks if needed.
* <p>
* Be aware that because chunk operations are expensive,
* it is possible for this method to be non-blocking when retrieving chunks is required.
* <p>
* When this method is called for the first time (during player login), the player will be teleport at {@link #getRespawnPoint()}.
*
* @param instance the new instance of the player
*/
@Override @Override
public void setInstance(@NotNull Instance instance) { public void setInstance(@NotNull Instance instance) {
Check.notNull(instance, "instance cannot be null!"); Check.notNull(instance, "instance cannot be null!");
@ -641,7 +668,8 @@ public class Player extends LivingEntity implements CommandSender {
final ChunkCallback callback = (chunk) -> { final ChunkCallback callback = (chunk) -> {
if (chunk != null) { if (chunk != null) {
chunk.addViewer(this); chunk.addViewer(this);
if (chunk.getChunkX() == Math.floorDiv((int) getPosition().getX(), 16) && chunk.getChunkZ() == Math.floorDiv((int) getPosition().getZ(), 16)) if (chunk.getChunkX() == ChunkUtils.getChunkCoordinate((int) getPosition().getX()) &&
chunk.getChunkZ() == ChunkUtils.getChunkCoordinate((int) getPosition().getZ()))
updateViewPosition(chunk); updateViewPosition(chunk);
} }
final boolean isLast = counter.get() == length - 1; final boolean isLast = counter.get() == length - 1;
@ -674,6 +702,11 @@ public class Player extends LivingEntity implements CommandSender {
private void spawnPlayer(Instance instance, boolean firstSpawn) { private void spawnPlayer(Instance instance, boolean firstSpawn) {
this.viewableEntities.forEach(entity -> entity.removeViewer(this)); this.viewableEntities.forEach(entity -> entity.removeViewer(this));
super.setInstance(instance); super.setInstance(instance);
if (firstSpawn) {
teleport(getRespawnPoint());
}
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn); PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
callEvent(PlayerSpawnEvent.class, spawnEvent); callEvent(PlayerSpawnEvent.class, spawnEvent);
} }
@ -736,12 +769,6 @@ public class Player extends LivingEntity implements CommandSender {
sendMessage(ColoredText.of(message)); sendMessage(ColoredText.of(message));
} }
@NotNull
@Override
public Collection<Permission> getAllPermissions() {
return permissions;
}
/** /**
* Sends a message to the player. * Sends a message to the player.
* *
@ -1136,6 +1163,9 @@ public class Player extends LivingEntity implements CommandSender {
public synchronized void setSkin(@Nullable PlayerSkin skin) { public synchronized void setSkin(@Nullable PlayerSkin skin) {
this.skin = skin; this.skin = skin;
if (instance == null)
return;
DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket(); DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket();
destroyEntitiesPacket.entityIds = new int[]{getEntityId()}; destroyEntitiesPacket.entityIds = new int[]{getEntityId()};
@ -1260,7 +1290,8 @@ public class Player extends LivingEntity implements CommandSender {
facePosition(facePoint, entity.getPosition(), entity, targetPoint); facePosition(facePoint, entity.getPosition(), entity, targetPoint);
} }
private void facePosition(@NotNull FacePoint facePoint, @NotNull Position targetPosition, @Nullable Entity entity, @Nullable FacePoint targetPoint) { private void facePosition(@NotNull FacePoint facePoint, @NotNull Position targetPosition,
@Nullable Entity entity, @Nullable FacePoint targetPoint) {
FacePlayerPacket facePlayerPacket = new FacePlayerPacket(); FacePlayerPacket facePlayerPacket = new FacePlayerPacket();
facePlayerPacket.entityFacePosition = facePoint == FacePoint.EYE ? facePlayerPacket.entityFacePosition = facePoint == FacePoint.EYE ?
FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET; FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET;
@ -1687,7 +1718,7 @@ public class Player extends LivingEntity implements CommandSender {
public void setTeam(Team team) { public void setTeam(Team team) {
super.setTeam(team); super.setTeam(team);
if (team != null) if (team != null)
getPlayerConnection().sendPacket(team.getTeamsCreationPacket()); getPlayerConnection().sendPacket(team.createTeamsCreationPacket());
} }
/** /**
@ -2194,7 +2225,8 @@ public class Player extends LivingEntity implements CommandSender {
* @param targetBlockPosition the custom block position * @param targetBlockPosition the custom block position
* @param breakers the breakers of the block, can be null if {@code this} is the only breaker * @param breakers the breakers of the block, can be null if {@code this} is the only breaker
*/ */
public void setTargetBlock(@NotNull CustomBlock targetCustomBlock, @NotNull BlockPosition targetBlockPosition, @Nullable Set<Player> breakers) { public void setTargetBlock(@NotNull CustomBlock targetCustomBlock, @NotNull BlockPosition targetBlockPosition,
@Nullable Set<Player> breakers) {
this.targetCustomBlock = targetCustomBlock; this.targetCustomBlock = targetCustomBlock;
this.targetBlockPosition = targetBlockPosition; this.targetBlockPosition = targetBlockPosition;
@ -2338,7 +2370,7 @@ public class Player extends LivingEntity implements CommandSender {
// Team // Team
if (this.getTeam() != null) if (this.getTeam() != null)
connection.sendPacket(this.getTeam().getTeamsCreationPacket()); connection.sendPacket(this.getTeam().createTeamsCreationPacket());
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket(); EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId(); entityHeadLookPacket.entityId = getEntityId();
@ -2513,7 +2545,8 @@ public class Player extends LivingEntity implements CommandSender {
* @param displayedSkinParts the player displayed skin parts * @param displayedSkinParts the player displayed skin parts
* @param mainHand the player main hand * @param mainHand the player main hand
*/ */
public void refresh(String locale, byte viewDistance, ChatMode chatMode, boolean chatColors, byte displayedSkinParts, MainHand mainHand) { public void refresh(String locale, byte viewDistance, ChatMode chatMode, boolean chatColors,
byte displayedSkinParts, MainHand mainHand) {
final boolean viewDistanceChanged = !firstRefresh && this.viewDistance != viewDistance; final boolean viewDistanceChanged = !firstRefresh && this.viewDistance != viewDistance;

View File

@ -60,6 +60,9 @@ public class FakePlayer extends Player {
/** /**
* Inits a new {@link FakePlayer} without adding it in cache. * Inits a new {@link FakePlayer} without adding it in cache.
* <p>
* If you want the fake player to be obtainable with the {@link net.minestom.server.network.ConnectionManager}
* you need to specify it in a {@link FakePlayerOption} and use {@link #initPlayer(UUID, String, FakePlayerOption, Consumer)}.
* *
* @param uuid the FakePlayer uuid * @param uuid the FakePlayer uuid
* @param username the FakePlayer username * @param username the FakePlayer username

View File

@ -35,7 +35,7 @@ public class PlayerLoginEvent extends Event {
* <p> * <p>
* WARNING: this must NOT be null, otherwise the player cannot spawn. * WARNING: this must NOT be null, otherwise the player cannot spawn.
* *
* @return the spawning instance * @return the spawning instance, null if not already defined
*/ */
@Nullable @Nullable
public Instance getSpawningInstance() { public Instance getSpawningInstance() {

View File

@ -14,8 +14,9 @@ public class PlayerSkinInitEvent extends Event {
private final Player player; private final Player player;
private PlayerSkin skin; private PlayerSkin skin;
public PlayerSkinInitEvent(@NotNull Player player) { public PlayerSkinInitEvent(@NotNull Player player, @Nullable PlayerSkin currentSkin) {
this.player = player; this.player = player;
this.skin = currentSkin;
} }
/** /**

View File

@ -1,11 +1,11 @@
package net.minestom.server.extensions; package net.minestom.server.extensions;
import com.google.gson.*; import com.google.gson.*;
import lombok.extern.slf4j.Slf4j;
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixins; import org.spongepowered.asm.mixin.Mixins;
@ -19,9 +19,10 @@ import java.net.URLClassLoader;
import java.util.*; import java.util.*;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
@Slf4j
public final class ExtensionManager { public final class ExtensionManager {
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes"; private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources"; private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
private final static Gson GSON = new Gson(); private final static Gson GSON = new Gson();
@ -40,7 +41,7 @@ public final class ExtensionManager {
if (!extensionFolder.exists()) { if (!extensionFolder.exists()) {
if (!extensionFolder.mkdirs()) { if (!extensionFolder.mkdirs()) {
log.error("Could not find or create the extension folder, extensions will not be loaded!"); LOGGER.error("Could not find or create the extension folder, extensions will not be loaded!");
return; return;
} }
} }
@ -57,7 +58,7 @@ public final class ExtensionManager {
} }
loader = newClassLoader(urls); loader = newClassLoader(urls);
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
log.error("Failed to get URL.", e); LOGGER.error("Failed to get URL.", e);
continue; continue;
} }
// TODO: Can't we use discoveredExtension.description here? Someone should test that. // TODO: Can't we use discoveredExtension.description here? Someone should test that.
@ -71,7 +72,7 @@ public final class ExtensionManager {
} }
urlsString.append("'").append(url.toString()).append("'"); urlsString.append("'").append(url.toString()).append("'");
} }
log.error("Failed to find extension.json in the urls '{}'.", urlsString); LOGGER.error("Failed to find extension.json in the urls '{}'.", urlsString);
continue; continue;
} }
JsonObject extensionDescriptionJson = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject(); JsonObject extensionDescriptionJson = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject();
@ -80,8 +81,8 @@ public final class ExtensionManager {
final String extensionName = extensionDescriptionJson.get("name").getAsString(); final String extensionName = extensionDescriptionJson.get("name").getAsString();
// Check the validity of the extension's name. // Check the validity of the extension's name.
if (!extensionName.matches("[A-Za-z]+")) { if (!extensionName.matches("[A-Za-z]+")) {
log.error("Extension '{}' specified an invalid name.", extensionName); LOGGER.error("Extension '{}' specified an invalid name.", extensionName);
log.error("Extension '{}' will not be loaded.", extensionName); LOGGER.error("Extension '{}' will not be loaded.", extensionName);
continue; continue;
} }
@ -90,8 +91,8 @@ public final class ExtensionManager {
{ {
String version; String version;
if (!extensionDescriptionJson.has("version")) { if (!extensionDescriptionJson.has("version")) {
log.warn("Extension '{}' did not specify a version.", extensionName); LOGGER.warn("Extension '{}' did not specify a version.", extensionName);
log.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName); LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName);
version = "Not Specified"; version = "Not Specified";
} else { } else {
version = extensionDescriptionJson.get("version").getAsString(); version = extensionDescriptionJson.get("version").getAsString();
@ -109,7 +110,7 @@ public final class ExtensionManager {
extensionLoaders.put(extensionName.toLowerCase(), loader); extensionLoaders.put(extensionName.toLowerCase(), loader);
if (extensions.containsKey(extensionName.toLowerCase())) { if (extensions.containsKey(extensionName.toLowerCase())) {
log.error("An extension called '{}' has already been registered.", extensionName); LOGGER.error("An extension called '{}' has already been registered.", extensionName);
continue; continue;
} }
@ -117,7 +118,7 @@ public final class ExtensionManager {
try { try {
jarClass = Class.forName(mainClass, true, loader); jarClass = Class.forName(mainClass, true, loader);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
log.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e); LOGGER.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e);
continue; continue;
} }
@ -125,7 +126,7 @@ public final class ExtensionManager {
try { try {
extensionClass = jarClass.asSubclass(Extension.class); extensionClass = jarClass.asSubclass(Extension.class);
} catch (ClassCastException e) { } catch (ClassCastException e) {
log.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e); LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e);
continue; continue;
} }
@ -135,19 +136,19 @@ public final class ExtensionManager {
// Let's just make it accessible, plugin creators don't have to make this public. // Let's just make it accessible, plugin creators don't have to make this public.
constructor.setAccessible(true); constructor.setAccessible(true);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
log.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e); LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e);
continue; continue;
} }
Extension extension = null; Extension extension = null;
try { try {
extension = constructor.newInstance(); extension = constructor.newInstance();
} catch (InstantiationException e) { } catch (InstantiationException e) {
log.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e); LOGGER.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e);
continue; continue;
} catch (IllegalAccessException ignored) { } catch (IllegalAccessException ignored) {
// We made it accessible, should not occur // We made it accessible, should not occur
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
log.error( LOGGER.error(
"While instantiating the main class '{}' in '{}' an exception was thrown.", "While instantiating the main class '{}' in '{}' an exception was thrown.",
mainClass, mainClass,
extensionName, extensionName,
@ -164,7 +165,7 @@ public final class ExtensionManager {
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
// We made it accessible, should not occur // We made it accessible, should not occur
} catch (NoSuchFieldException e) { } catch (NoSuchFieldException e) {
log.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e); LOGGER.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e);
continue; continue;
} }
@ -178,7 +179,7 @@ public final class ExtensionManager {
e.printStackTrace(); e.printStackTrace();
} catch (NoSuchFieldException e) { } catch (NoSuchFieldException e) {
// This should also not occur (unless someone changed the logger in Extension superclass). // This should also not occur (unless someone changed the logger in Extension superclass).
log.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e); LOGGER.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e);
} }
extensions.put(extensionName.toLowerCase(), extension); extensions.put(extensionName.toLowerCase(), extension);
@ -209,7 +210,7 @@ public final class ExtensionManager {
// this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder // this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder
if (System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) { if (System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) {
log.info("Found indev folders for extension. Adding to list of discovered extensions."); LOGGER.info("Found indev folders for extension. Adding to list of discovered extensions.");
final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER); final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER);
final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER); final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER);
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) {
@ -260,11 +261,11 @@ public final class ExtensionManager {
private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions) { private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions) {
final ClassLoader cl = getClass().getClassLoader(); final ClassLoader cl = getClass().getClassLoader();
if (!(cl instanceof MinestomOverwriteClassLoader)) { if (!(cl instanceof MinestomOverwriteClassLoader)) {
log.warn("Current class loader is not a MinestomOverwriteClassLoader, but " + cl + ". This disables code modifiers (Mixin support is therefore disabled)"); LOGGER.warn("Current class loader is not a MinestomOverwriteClassLoader, but " + cl + ". This disables code modifiers (Mixin support is therefore disabled)");
return; return;
} }
MinestomOverwriteClassLoader modifiableClassLoader = (MinestomOverwriteClassLoader) cl; MinestomOverwriteClassLoader modifiableClassLoader = (MinestomOverwriteClassLoader) cl;
log.info("Start loading code modifiers..."); LOGGER.info("Start loading code modifiers...");
for (DiscoveredExtension extension : extensions) { for (DiscoveredExtension extension : extensions) {
try { try {
if (extension.description.has("codeModifiers")) { if (extension.description.has("codeModifiers")) {
@ -276,14 +277,14 @@ public final class ExtensionManager {
if (extension.description.has("mixinConfig")) { if (extension.description.has("mixinConfig")) {
final String mixinConfigFile = extension.description.get("mixinConfig").getAsString(); final String mixinConfigFile = extension.description.get("mixinConfig").getAsString();
Mixins.addConfiguration(mixinConfigFile); Mixins.addConfiguration(mixinConfigFile);
log.info("Found mixin in extension " + extension.description.get("name").getAsString() + ": " + mixinConfigFile); LOGGER.info("Found mixin in extension " + extension.description.get("name").getAsString() + ": " + mixinConfigFile);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
log.error("Failed to load code modifier for extension in files: " + Arrays.toString(extension.files), e); LOGGER.error("Failed to load code modifier for extension in files: " + Arrays.toString(extension.files), e);
} }
} }
log.info("Done loading code modifiers."); LOGGER.info("Done loading code modifiers.");
} }
private static class DiscoveredExtension { private static class DiscoveredExtension {

View File

@ -1,12 +1,21 @@
package net.minestom.server.extras; package net.minestom.server.extras;
import lombok.Getter; import com.mojang.authlib.AuthenticationService;
import com.mojang.authlib.minecraft.MinecraftSessionService;
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.extras.mojangAuth.MojangCrypt;
import java.net.Proxy;
import java.security.KeyPair;
public final class MojangAuth { public final class MojangAuth {
@Getter private static boolean enabled = false;
private static boolean usingMojangAuth = false;
private static final KeyPair keyPair = MojangCrypt.generateKeyPair();
private static final AuthenticationService authService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, "");
private static final MinecraftSessionService sessionService = authService.createMinecraftSessionService();
/** /**
* Enables mojang authentication on the server. * Enables mojang authentication on the server.
@ -15,9 +24,25 @@ public final class MojangAuth {
*/ */
public static void init() { public static void init() {
if (MinecraftServer.getNettyServer().getAddress() == null) { if (MinecraftServer.getNettyServer().getAddress() == null) {
usingMojangAuth = true; enabled = true;
} else { } else {
throw new IllegalStateException("The server has already been started"); throw new IllegalStateException("The server has already been started");
} }
} }
public static boolean isEnabled() {
return enabled;
}
public static KeyPair getKeyPair() {
return keyPair;
}
public static AuthenticationService getAuthService() {
return authService;
}
public static MinecraftSessionService getSessionService() {
return sessionService;
}
} }

View File

@ -1,5 +1,12 @@
package net.minestom.server.extras.bungee; package net.minestom.server.extras.bungee;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.minestom.server.entity.PlayerSkin;
import org.jetbrains.annotations.NotNull;
/** /**
* BungeeCord forwarding support. This does not count as a security feature and you will still be required to manage your firewall. * BungeeCord forwarding support. This does not count as a security feature and you will still be required to manage your firewall.
* <p> * <p>
@ -7,7 +14,7 @@ package net.minestom.server.extras.bungee;
*/ */
public final class BungeeCordProxy { public final class BungeeCordProxy {
private static boolean enabled; private static volatile boolean enabled;
/** /**
* Enables bungee IP forwarding. * Enables bungee IP forwarding.
@ -24,4 +31,29 @@ public final class BungeeCordProxy {
public static boolean isEnabled() { public static boolean isEnabled() {
return enabled; return enabled;
} }
public static PlayerSkin readSkin(@NotNull String json) {
JsonArray array = JsonParser.parseString(json).getAsJsonArray();
String skinTexture = null;
String skinSignature = null;
for (JsonElement element : array) {
JsonObject jsonObject = element.getAsJsonObject();
final String name = jsonObject.get("name").getAsString();
final String value = jsonObject.get("value").getAsString();
final String signature = jsonObject.get("signature").getAsString();
if (name.equals("textures")) {
skinTexture = value;
skinSignature = signature;
}
}
if (skinTexture != null && skinSignature != null) {
return new PlayerSkin(skinTexture, skinSignature);
} else {
return null;
}
}
} }

View File

@ -0,0 +1,33 @@
package net.minestom.server.extras.optifine;
import net.minestom.server.MinecraftServer;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.biomes.Biome;
import net.minestom.server.world.biomes.BiomeManager;
/**
* Hacky class for Optifine because of an issue making the client crash if biomes 'swamp' or 'swamp_hills'
* are not registered.
* <p>
* Can be removed anytime, hope that it will be fixed.
*/
public final class OptifineSupport {
private static volatile boolean enabled;
/**
* Enables optifine support by registering the required biomes.
*
* @throws IllegalStateException if optifine support is already enabled
*/
public static void enable() {
Check.stateCondition(enabled, "Optifine support is already enabled!");
OptifineSupport.enabled = true;
BiomeManager biomeManager = MinecraftServer.getBiomeManager();
biomeManager.addBiome(Biome.builder().name(NamespaceID.from("minecraft:swamp")).build());
biomeManager.addBiome(Biome.builder().name(NamespaceID.from("minecraft:swamp_hills")).build());
}
}

View File

@ -1,10 +1,12 @@
package net.minestom.server.extras.selfmodification; package net.minestom.server.extras.selfmodification;
import lombok.extern.slf4j.Slf4j; import net.minestom.server.MinecraftServer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -20,9 +22,10 @@ import java.util.Set;
/** /**
* Class Loader that can modify class bytecode when they are loaded * Class Loader that can modify class bytecode when they are loaded
*/ */
@Slf4j
public class MinestomOverwriteClassLoader extends URLClassLoader { public class MinestomOverwriteClassLoader extends URLClassLoader {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
private static MinestomOverwriteClassLoader INSTANCE; private static MinestomOverwriteClassLoader INSTANCE;
/** /**
@ -105,18 +108,18 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
try { try {
// we do not load system classes by ourselves // we do not load system classes by ourselves
Class<?> systemClass = ClassLoader.getPlatformClassLoader().loadClass(name); Class<?> systemClass = ClassLoader.getPlatformClassLoader().loadClass(name);
log.trace("System class: " + systemClass); LOGGER.trace("System class: " + systemClass);
return systemClass; return systemClass;
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
try { try {
if (isProtected(name)) { if (isProtected(name)) {
log.trace("Protected: " + name); LOGGER.trace("Protected: " + name);
return super.loadClass(name, resolve); return super.loadClass(name, resolve);
} }
return define(name, loadBytes(name, true), resolve); return define(name, loadBytes(name, true), resolve);
} catch (Exception ex) { } catch (Exception ex) {
log.trace("Fail to load class, resorting to parent loader: " + name, ex); LOGGER.trace("Fail to load class, resorting to parent loader: " + name, ex);
// fail to load class, let parent load // fail to load class, let parent load
// this forbids code modification, but at least it will load // this forbids code modification, but at least it will load
return super.loadClass(name, resolve); return super.loadClass(name, resolve);
@ -137,7 +140,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
private Class<?> define(String name, byte[] bytes, boolean resolve) { private Class<?> define(String name, byte[] bytes, boolean resolve) {
Class<?> defined = defineClass(name, bytes, 0, bytes.length); Class<?> defined = defineClass(name, bytes, 0, bytes.length);
log.trace("Loaded with code modifiers: " + name); LOGGER.trace("Loaded with code modifiers: " + name);
if (resolve) { if (resolve) {
resolveClass(defined); resolveClass(defined);
} }
@ -180,7 +183,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
}; };
node.accept(writer); node.accept(writer);
bytes = writer.toByteArray(); bytes = writer.toByteArray();
log.trace("Modified " + name); LOGGER.trace("Modified " + name);
} }
} }
return bytes; return bytes;
@ -208,7 +211,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
if (CodeModifier.class.isAssignableFrom(modifierClass)) { if (CodeModifier.class.isAssignableFrom(modifierClass)) {
CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance(); CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance();
synchronized (modifiers) { synchronized (modifiers) {
log.warn("Added Code modifier: " + modifier); LOGGER.warn("Added Code modifier: " + modifier);
addCodeModifier(modifier); addCodeModifier(modifier);
} }
} }

View File

@ -1,25 +1,29 @@
package net.minestom.server.extras.selfmodification.mixins; package net.minestom.server.extras.selfmodification.mixins;
import lombok.extern.slf4j.Slf4j; import net.minestom.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.service.IMixinAuditTrail; import org.spongepowered.asm.service.IMixinAuditTrail;
/** /**
* Takes care of logging mixin operations * Takes care of logging mixin operations
*/ */
@Slf4j
public class MixinAuditTrailMinestom implements IMixinAuditTrail { public class MixinAuditTrailMinestom implements IMixinAuditTrail {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
@Override @Override
public void onApply(String className, String mixinName) { public void onApply(String className, String mixinName) {
log.trace("Applied mixin "+mixinName+" to class "+className); LOGGER.trace("Applied mixin " + mixinName + " to class " + className);
} }
@Override @Override
public void onPostProcess(String className) { public void onPostProcess(String className) {
log.trace("Post processing "+className); LOGGER.trace("Post processing " + className);
} }
@Override @Override
public void onGenerate(String className, String generatorName) { public void onGenerate(String className, String generatorName) {
log.trace("Generating class "+className+" via generator "+generatorName); LOGGER.trace("Generating class " + className + " via generator " + generatorName);
} }
} }

View File

@ -2,6 +2,7 @@ package net.minestom.server.extras.velocity;
import com.google.common.net.InetAddresses; import com.google.common.net.InetAddresses;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.utils.binary.BinaryReader; import net.minestom.server.utils.binary.BinaryReader;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -22,7 +23,7 @@ public final class VelocityProxy {
public static final String PLAYER_INFO_CHANNEL = "velocity:player_info"; public static final String PLAYER_INFO_CHANNEL = "velocity:player_info";
private static final int SUPPORTED_FORWARDING_VERSION = 1; private static final int SUPPORTED_FORWARDING_VERSION = 1;
private static boolean enabled; private static volatile boolean enabled;
private static byte[] secret; private static byte[] secret;
/** /**
@ -72,7 +73,30 @@ public final class VelocityProxy {
} }
public static InetAddress readAddress(@NotNull BinaryReader reader) { public static InetAddress readAddress(@NotNull BinaryReader reader) {
return InetAddresses.forString(reader.readSizedString()); return InetAddresses.forString(reader.readSizedString(Integer.MAX_VALUE));
}
public static PlayerSkin readSkin(@NotNull BinaryReader reader) {
String skinTexture = null;
String skinSignature = null;
final int properties = reader.readVarInt();
for (int i1 = 0; i1 < properties; i1++) {
final String name = reader.readSizedString(Short.MAX_VALUE);
final String value = reader.readSizedString(Short.MAX_VALUE);
final String signature = reader.readBoolean() ? reader.readSizedString(Short.MAX_VALUE) : null;
if (name.equals("textures")) {
skinTexture = value;
skinSignature = signature;
}
}
if (skinTexture != null && skinSignature != null) {
return new PlayerSkin(skinTexture, skinSignature);
} else {
return null;
}
} }
} }

View File

@ -1,6 +1,5 @@
package net.minestom.server.instance; package net.minestom.server.instance;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.Viewable; import net.minestom.server.Viewable;
import net.minestom.server.data.Data; import net.minestom.server.data.Data;
@ -14,7 +13,6 @@ import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockManager; import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.network.packet.server.play.UpdateLightPacket; import net.minestom.server.network.packet.server.play.UpdateLightPacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
@ -33,7 +31,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
// TODO light data & API // TODO light data & API
@ -44,7 +41,11 @@ import java.util.function.Consumer;
* Chunks can be serialized using {@link #getSerializedData()} and deserialized back with {@link #readChunk(BinaryReader, ChunkCallback)}, * Chunks can be serialized using {@link #getSerializedData()} and deserialized back with {@link #readChunk(BinaryReader, ChunkCallback)},
* allowing you to implement your own storage solution if needed. * allowing you to implement your own storage solution if needed.
* <p> * <p>
* You can create your own implementation of this class by extending it and create the objects in {@link InstanceContainer#setChunkSupplier(ChunkSupplier)}. * You can create your own implementation of this class by extending it
* and create the objects in {@link InstanceContainer#setChunkSupplier(ChunkSupplier)}.
* <p>
* You generally want to avoid storing references of this object as this could lead to a huge memory leak,
* you should store the chunk coordinates instead.
*/ */
public abstract class Chunk implements Viewable, DataContainer { public abstract class Chunk implements Viewable, DataContainer {
@ -68,11 +69,6 @@ public abstract class Chunk implements Viewable, DataContainer {
private final boolean shouldGenerate; private final boolean shouldGenerate;
private boolean readOnly; private boolean readOnly;
// Packet cache
private volatile boolean enableCachePacket;
protected volatile boolean packetUpdated;
private ByteBuf fullDataPacket;
protected volatile boolean loaded = true; protected volatile boolean loaded = true;
protected final Set<Player> viewers = new CopyOnWriteArraySet<>(); protected final Set<Player> viewers = new CopyOnWriteArraySet<>();
@ -87,9 +83,6 @@ public abstract class Chunk implements Viewable, DataContainer {
this.chunkZ = chunkZ; this.chunkZ = chunkZ;
this.shouldGenerate = shouldGenerate; this.shouldGenerate = shouldGenerate;
// true by default
this.enableCachePacket = true;
if (biomes != null && biomes.length == BIOME_COUNT) { if (biomes != null && biomes.length == BIOME_COUNT) {
this.biomes = biomes; this.biomes = biomes;
} else { } else {
@ -332,51 +325,6 @@ public abstract class Chunk implements Viewable, DataContainer {
this.readOnly = readOnly; this.readOnly = readOnly;
} }
/**
* Gets if this chunk automatically cache the latest {@link ChunkDataPacket} version.
* <p>
* Retrieved with {@link #retrieveDataBuffer(Consumer)}.
*
* @return true if the chunk automatically cache the chunk packet
*/
public boolean enableCachePacket() {
return enableCachePacket;
}
/**
* Enables or disable the automatic {@link ChunkDataPacket} caching.
*
* @param enableCachePacket true to enable to chunk packet caching
*/
public synchronized void setEnableCachePacket(boolean enableCachePacket) {
this.enableCachePacket = enableCachePacket;
if (enableCachePacket && fullDataPacket != null) {
this.fullDataPacket.release();
this.fullDataPacket = null;
}
}
/**
* Gets the cached data packet.
* <p>
* Use {@link #retrieveDataBuffer(Consumer)} to be sure to get the updated version.
*
* @return the last cached data packet, can be null or outdated
*/
public ByteBuf getFullDataPacket() {
return fullDataPacket;
}
/**
* Sets the cached {@link ChunkDataPacket} of this chunk.
*
* @param fullDataPacket the new cached chunk packet
*/
public void setFullDataPacket(ByteBuf fullDataPacket) {
this.fullDataPacket = fullDataPacket;
this.packetUpdated = true;
}
/** /**
* Changes this chunk columnar space. * Changes this chunk columnar space.
* *
@ -386,27 +334,6 @@ public abstract class Chunk implements Viewable, DataContainer {
this.columnarSpace = columnarSpace; this.columnarSpace = columnarSpace;
} }
/**
* Retrieves (and cache if needed) the updated data packet.
*
* @param consumer the consumer called once the packet is sure to be up-to-date
*/
public void retrieveDataBuffer(Consumer<ByteBuf> consumer) {
final ByteBuf data = getFullDataPacket();
if (data == null || !packetUpdated) {
// Packet has never been wrote or is outdated, write it
PacketWriterUtils.writeCallbackPacket(getFreshFullDataPacket(), packet -> {
if (enableCachePacket) {
setFullDataPacket(packet);
}
consumer.accept(packet);
});
} else {
// Packet is up-to-date
consumer.accept(data);
}
}
/** /**
* Gets a {@link ChunkDataPacket} which should contain the full chunk. * Gets a {@link ChunkDataPacket} which should contain the full chunk.
* *
@ -517,10 +444,10 @@ public abstract class Chunk implements Viewable, DataContainer {
final PlayerConnection playerConnection = player.getPlayerConnection(); final PlayerConnection playerConnection = player.getPlayerConnection();
// Retrieve & send the buffer to the connection // Retrieve & send the buffer to the connection
retrieveDataBuffer(buf -> playerConnection.sendPacket(buf, true)); playerConnection.sendPacket(getFreshFullDataPacket());
// TODO do not hardcode // TODO do not hardcode light
if (MinecraftServer.isFixLighting()) { {
UpdateLightPacket updateLightPacket = new UpdateLightPacket(); UpdateLightPacket updateLightPacket = new UpdateLightPacket();
updateLightPacket.chunkX = getChunkX(); updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ(); updateLightPacket.chunkZ = getChunkZ();
@ -540,7 +467,8 @@ public abstract class Chunk implements Viewable, DataContainer {
} }
updateLightPacket.skyLight = temp; updateLightPacket.skyLight = temp;
updateLightPacket.blockLight = temp2; updateLightPacket.blockLight = temp2;
PacketWriterUtils.writeAndSend(player, updateLightPacket);
playerConnection.sendPacket(updateLightPacket);
} }
} }
@ -550,10 +478,8 @@ public abstract class Chunk implements Viewable, DataContainer {
* @param player the player to update the chunk to * @param player the player to update the chunk to
*/ */
public void sendChunkUpdate(@NotNull Player player) { public void sendChunkUpdate(@NotNull Player player) {
retrieveDataBuffer(buf -> { final PlayerConnection playerConnection = player.getPlayerConnection();
final PlayerConnection playerConnection = player.getPlayerConnection(); playerConnection.sendPacket(getFreshFullDataPacket());
playerConnection.sendPacket(buf, true);
});
} }
/** /**
@ -562,14 +488,10 @@ public abstract class Chunk implements Viewable, DataContainer {
public void sendChunkUpdate() { public void sendChunkUpdate() {
final Set<Player> chunkViewers = getViewers(); final Set<Player> chunkViewers = getViewers();
if (!chunkViewers.isEmpty()) { if (!chunkViewers.isEmpty()) {
retrieveDataBuffer(buf -> chunkViewers.forEach(player -> { chunkViewers.forEach(player -> {
final PlayerConnection playerConnection = player.getPlayerConnection(); final PlayerConnection playerConnection = player.getPlayerConnection();
if (!PlayerUtils.isNettyClient(playerConnection)) playerConnection.sendPacket(getFreshFullDataPacket());
return; });
playerConnection.sendPacket(buf, true);
}));
} }
} }
@ -578,6 +500,7 @@ public abstract class Chunk implements Viewable, DataContainer {
* *
* @param section the section to update * @param section the section to update
* @param player the player to send the packet to * @param player the player to send the packet to
* @throws IllegalArgumentException if {@code section} is not a valid section
*/ */
public void sendChunkSectionUpdate(int section, @NotNull Player player) { public void sendChunkSectionUpdate(int section, @NotNull Player player) {
if (!PlayerUtils.isNettyClient(player)) if (!PlayerUtils.isNettyClient(player))
@ -585,7 +508,7 @@ public abstract class Chunk implements Viewable, DataContainer {
Check.argCondition(!MathUtils.isBetween(section, 0, CHUNK_SECTION_COUNT), Check.argCondition(!MathUtils.isBetween(section, 0, CHUNK_SECTION_COUNT),
"The chunk section " + section + " does not exist"); "The chunk section " + section + " does not exist");
PacketWriterUtils.writeAndSend(player, getChunkSectionUpdatePacket(section)); player.getPlayerConnection().sendPacket(createChunkSectionUpdatePacket(section));
} }
/** /**
@ -595,7 +518,7 @@ public abstract class Chunk implements Viewable, DataContainer {
* @return the {@link ChunkDataPacket} to update a single chunk section * @return the {@link ChunkDataPacket} to update a single chunk section
*/ */
@NotNull @NotNull
protected ChunkDataPacket getChunkSectionUpdatePacket(int section) { protected ChunkDataPacket createChunkSectionUpdatePacket(int section) {
ChunkDataPacket chunkDataPacket = getFreshPartialDataPacket(); ChunkDataPacket chunkDataPacket = getFreshPartialDataPacket();
chunkDataPacket.fullChunk = false; chunkDataPacket.fullChunk = false;
int[] sections = new int[CHUNK_SECTION_COUNT]; int[] sections = new int[CHUNK_SECTION_COUNT];

View File

@ -10,10 +10,9 @@ import net.minestom.server.data.SerializableData;
import net.minestom.server.data.SerializableDataImpl; import net.minestom.server.data.SerializableDataImpl;
import net.minestom.server.entity.pathfinding.PFBlockDescription; import net.minestom.server.entity.pathfinding.PFBlockDescription;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.instance.palette.PaletteStorage;
import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.binary.BinaryReader; import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.block.CustomBlockUtils; import net.minestom.server.utils.block.CustomBlockUtils;
@ -42,11 +41,9 @@ public class DynamicChunk extends Chunk {
*/ */
private static final int DATA_FORMAT_VERSION = 1; private static final int DATA_FORMAT_VERSION = 1;
// blocks id based on coordinate, see Chunk#getBlockIndex // WARNING: not thread-safe and should not be changed
// WARNING: those arrays are NOT thread-safe protected PaletteStorage blockPalette;
// and modifying them can cause issue with block data, update, block entity and the cached chunk packet protected PaletteStorage customBlockPalette;
protected final short[] blocksStateId = new short[CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z];
protected final short[] customBlocksId = new short[CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z];
// Used to get all blocks with data (no null) // Used to get all blocks with data (no null)
// Key is still chunk coordinates (see #getBlockIndex) // Key is still chunk coordinates (see #getBlockIndex)
@ -60,8 +57,17 @@ public class DynamicChunk extends Chunk {
// Block entities // Block entities
protected final Set<Integer> blockEntities = new CopyOnWriteArraySet<>(); protected final Set<Integer> blockEntities = new CopyOnWriteArraySet<>();
public DynamicChunk(@Nullable Biome[] biomes, int chunkX, int chunkZ) { public DynamicChunk(@Nullable Biome[] biomes, int chunkX, int chunkZ,
@NotNull PaletteStorage blockPalette, @NotNull PaletteStorage customBlockPalette) {
super(biomes, chunkX, chunkZ, true); super(biomes, chunkX, chunkZ, true);
this.blockPalette = blockPalette;
this.customBlockPalette = customBlockPalette;
}
public DynamicChunk(@Nullable Biome[] biomes, int chunkX, int chunkZ) {
this(biomes, chunkX, chunkZ,
new PaletteStorage(6, 2),
new PaletteStorage(6, 2));
} }
@Override @Override
@ -79,14 +85,12 @@ public class DynamicChunk extends Chunk {
final int index = getBlockIndex(x, y, z); final int index = getBlockIndex(x, y, z);
// True if the block is not complete air without any custom block capabilities // True if the block is not complete air without any custom block capabilities
final boolean hasBlock = blockStateId != 0 || customBlockId != 0; final boolean hasBlock = blockStateId != 0 || customBlockId != 0;
if (hasBlock) {
this.blocksStateId[index] = blockStateId;
this.customBlocksId[index] = customBlockId;
} else {
// Block has been deleted, clear cache and return
this.blocksStateId[index] = 0; // Set to air this.blockPalette.setBlockAt(x, y, z, blockStateId);
this.customBlocksId[index] = 0; // Remove custom block this.customBlockPalette.setBlockAt(x, y, z, customBlockId);
if (!hasBlock) {
// Block has been deleted, clear cache and return
this.blocksData.remove(index); this.blocksData.remove(index);
@ -94,8 +98,6 @@ public class DynamicChunk extends Chunk {
this.updatableBlocksLastUpdate.remove(index); this.updatableBlocksLastUpdate.remove(index);
this.blockEntities.remove(index); this.blockEntities.remove(index);
this.packetUpdated = false;
return; return;
} }
@ -121,8 +123,6 @@ public class DynamicChunk extends Chunk {
} else { } else {
this.blockEntities.remove(index); this.blockEntities.remove(index);
} }
this.packetUpdated = false;
} }
@Override @Override
@ -155,41 +155,23 @@ public class DynamicChunk extends Chunk {
@Override @Override
public short getBlockStateId(int x, int y, int z) { public short getBlockStateId(int x, int y, int z) {
final int index = getBlockIndex(x, y, z); return this.blockPalette.getBlockAt(x, y, z);
if (!MathUtils.isBetween(index, 0, blocksStateId.length)) {
return 0; // TODO: custom invalid block
}
return blocksStateId[index];
} }
@Override @Override
public short getCustomBlockId(int x, int y, int z) { public short getCustomBlockId(int x, int y, int z) {
final int index = getBlockIndex(x, y, z); return customBlockPalette.getBlockAt(x, y, z);
if (!MathUtils.isBetween(index, 0, blocksStateId.length)) {
return 0; // TODO: custom invalid block
}
return customBlocksId[index];
} }
@Override @Override
protected void refreshBlockValue(int x, int y, int z, short blockStateId, short customBlockId) { protected void refreshBlockValue(int x, int y, int z, short blockStateId, short customBlockId) {
final int blockIndex = getBlockIndex(x, y, z); this.blockPalette.setBlockAt(x, y, z, blockStateId);
if (!MathUtils.isBetween(blockIndex, 0, blocksStateId.length)) { this.customBlockPalette.setBlockAt(x, y, z, customBlockId);
return;
}
this.blocksStateId[blockIndex] = blockStateId;
this.customBlocksId[blockIndex] = customBlockId;
} }
@Override @Override
protected void refreshBlockStateId(int x, int y, int z, short blockStateId) { protected void refreshBlockStateId(int x, int y, int z, short blockStateId) {
final int blockIndex = getBlockIndex(x, y, z); this.blockPalette.setBlockAt(x, y, z, blockStateId);
if (!MathUtils.isBetween(blockIndex, 0, blocksStateId.length)) {
return;
}
this.blocksStateId[blockIndex] = blockStateId;
} }
@Override @Override
@ -260,8 +242,8 @@ public class DynamicChunk extends Chunk {
for (byte z = 0; z < CHUNK_SIZE_Z; z++) { for (byte z = 0; z < CHUNK_SIZE_Z; z++) {
final int index = getBlockIndex(x, y, z); final int index = getBlockIndex(x, y, z);
final short blockStateId = blocksStateId[index]; final short blockStateId = blockPalette.getBlockAt(x, y, z);
final short customBlockId = customBlocksId[index]; final short customBlockId = customBlockPalette.getBlockAt(x, y, z);
// No block at the position // No block at the position
if (blockStateId == 0 && customBlockId == 0) if (blockStateId == 0 && customBlockId == 0)
@ -398,8 +380,8 @@ public class DynamicChunk extends Chunk {
fullDataPacket.biomes = biomes.clone(); fullDataPacket.biomes = biomes.clone();
fullDataPacket.chunkX = chunkX; fullDataPacket.chunkX = chunkX;
fullDataPacket.chunkZ = chunkZ; fullDataPacket.chunkZ = chunkZ;
fullDataPacket.blocksStateId = blocksStateId.clone(); fullDataPacket.paletteStorage = blockPalette.copy();
fullDataPacket.customBlocksId = customBlocksId.clone(); fullDataPacket.customBlockPaletteStorage = customBlockPalette.copy();
fullDataPacket.blockEntities = new HashSet<>(blockEntities); fullDataPacket.blockEntities = new HashSet<>(blockEntities);
fullDataPacket.blocksData = new Int2ObjectOpenHashMap<>(blocksData); fullDataPacket.blocksData = new Int2ObjectOpenHashMap<>(blocksData);
return fullDataPacket; return fullDataPacket;
@ -409,8 +391,8 @@ public class DynamicChunk extends Chunk {
@Override @Override
public Chunk copy(int chunkX, int chunkZ) { public Chunk copy(int chunkX, int chunkZ) {
DynamicChunk dynamicChunk = new DynamicChunk(biomes.clone(), chunkX, chunkZ); DynamicChunk dynamicChunk = new DynamicChunk(biomes.clone(), chunkX, chunkZ);
ArrayUtils.copyToDestination(blocksStateId, dynamicChunk.blocksStateId); dynamicChunk.blockPalette = blockPalette.copy();
ArrayUtils.copyToDestination(customBlocksId, dynamicChunk.customBlocksId); dynamicChunk.customBlockPalette = customBlockPalette.copy();
dynamicChunk.blocksData.putAll(blocksData); dynamicChunk.blocksData.putAll(blocksData);
dynamicChunk.updatableBlocks.addAll(updatableBlocks); dynamicChunk.updatableBlocks.addAll(updatableBlocks);
dynamicChunk.updatableBlocksLastUpdate.putAll(updatableBlocksLastUpdate); dynamicChunk.updatableBlocksLastUpdate.putAll(updatableBlocksLastUpdate);

View File

@ -1,9 +1,10 @@
package net.minestom.server.instance; package net.minestom.server.instance;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.play.ExplosionPacket; import net.minestom.server.network.packet.server.play.ExplosionPacket;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@ -54,7 +55,7 @@ public abstract class Explosion {
* *
* @param instance instance to perform this explosion in * @param instance instance to perform this explosion in
*/ */
public void apply(Instance instance) { public void apply(@NotNull Instance instance) {
List<BlockPosition> blocks = prepare(instance); List<BlockPosition> blocks = prepare(instance);
ExplosionPacket packet = new ExplosionPacket(); ExplosionPacket packet = new ExplosionPacket();
packet.x = getCenterX(); packet.x = getCenterX();
@ -80,7 +81,7 @@ public abstract class Explosion {
postExplosion(instance, blocks, packet); postExplosion(instance, blocks, packet);
// TODO send only to close players // TODO send only to close players
PacketWriterUtils.writeAndSend(instance.getPlayers(), packet); PacketUtils.sendGroupedPacket(instance.getPlayers(), packet);
postSend(instance, blocks); postSend(instance, blocks);
} }

View File

@ -16,12 +16,12 @@ import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockManager; import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.play.BlockActionPacket; import net.minestom.server.network.packet.server.play.BlockActionPacket;
import net.minestom.server.network.packet.server.play.TimeUpdatePacket; import net.minestom.server.network.packet.server.play.TimeUpdatePacket;
import net.minestom.server.storage.StorageLocation; import net.minestom.server.storage.StorageLocation;
import net.minestom.server.thread.ThreadProvider; import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Position; import net.minestom.server.utils.Position;
import net.minestom.server.utils.chunk.ChunkCallback; import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
@ -67,7 +67,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
// The time of the instance // The time of the instance
private long time; private long time;
private int timeRate = 1; private int timeRate = 1;
private UpdateOption timeUpdate = new UpdateOption(1, TimeUnit.TICK); private UpdateOption timeUpdate = new UpdateOption(1, TimeUnit.SECOND);
private long lastTimeUpdate; private long lastTimeUpdate;
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>(); private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
@ -111,7 +111,8 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
/** /**
* Schedules a task to be run during the next instance tick. * Schedules a task to be run during the next instance tick.
* It ensures that the task will be executed in the same thread as the instance and its chunks/entities (depending of the {@link ThreadProvider}). * It ensures that the task will be executed in the same thread as the instance
* and its chunks/entities (depending of the {@link ThreadProvider}).
* *
* @param callback the task to execute during the next instance tick * @param callback the task to execute during the next instance tick
*/ */
@ -369,7 +370,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
*/ */
public void setTime(long time) { public void setTime(long time) {
this.time = time; this.time = time;
PacketWriterUtils.writeAndSend(getPlayers(), getTimePacket()); PacketUtils.sendGroupedPacket(getPlayers(), createTimePacket());
} }
/** /**
@ -417,12 +418,12 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
} }
/** /**
* Gets a {@link TimeUpdatePacket} with the current age and time of this instance * Creates a {@link TimeUpdatePacket} with the current age and time of this instance
* *
* @return the {@link TimeUpdatePacket} with this instance data * @return the {@link TimeUpdatePacket} with this instance data
*/ */
@NotNull @NotNull
private TimeUpdatePacket getTimePacket() { private TimeUpdatePacket createTimePacket() {
TimeUpdatePacket timeUpdatePacket = new TimeUpdatePacket(); TimeUpdatePacket timeUpdatePacket = new TimeUpdatePacket();
timeUpdatePacket.worldAge = worldAge; timeUpdatePacket.worldAge = worldAge;
timeUpdatePacket.timeOfDay = time; timeUpdatePacket.timeOfDay = time;
@ -996,7 +997,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
// time needs to be send to players // time needs to be send to players
if (timeUpdate != null && !CooldownUtils.hasCooldown(time, lastTimeUpdate, timeUpdate)) { if (timeUpdate != null && !CooldownUtils.hasCooldown(time, lastTimeUpdate, timeUpdate)) {
PacketWriterUtils.writeAndSend(getPlayers(), getTimePacket()); PacketUtils.sendGroupedPacket(getPlayers(), createTimePacket());
this.lastTimeUpdate = time; this.lastTimeUpdate = time;
} }

View File

@ -581,7 +581,7 @@ public class InstanceContainer extends Instance {
* @throws NullPointerException if {@code chunkSupplier} is null * @throws NullPointerException if {@code chunkSupplier} is null
*/ */
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) { public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
Check.notNull(chunkSupplier, "The chunk supplier cannot be null, you can use a StaticChunk for a lightweight implementation"); Check.notNull(chunkSupplier, "The chunk supplier cannot be null!");
this.chunkSupplier = chunkSupplier; this.chunkSupplier = chunkSupplier;
} }
@ -605,6 +605,15 @@ public class InstanceContainer extends Instance {
return Collections.unmodifiableList(sharedInstances); return Collections.unmodifiableList(sharedInstances);
} }
/**
* Gets if this instance has {@link SharedInstance} linked to it.
*
* @return true if {@link #getSharedInstances()} is not empty
*/
public boolean hasSharedInstances() {
return !sharedInstances.isEmpty();
}
/** /**
* Assigns a {@link SharedInstance} to this container. * Assigns a {@link SharedInstance} to this container.
* <p> * <p>
@ -744,7 +753,7 @@ public class InstanceContainer extends Instance {
* @param blockPosition the block position * @param blockPosition the block position
* @param blockStateId the new state of the block * @param blockStateId the new state of the block
*/ */
private void sendBlockChange(Chunk chunk, BlockPosition blockPosition, short blockStateId) { private void sendBlockChange(@NotNull Chunk chunk, @NotNull BlockPosition blockPosition, short blockStateId) {
BlockChangePacket blockChangePacket = new BlockChangePacket(); BlockChangePacket blockChangePacket = new BlockChangePacket();
blockChangePacket.blockPosition = blockPosition; blockChangePacket.blockPosition = blockPosition;
blockChangePacket.blockStateId = blockStateId; blockChangePacket.blockStateId = blockStateId;

View File

@ -1,128 +0,0 @@
package net.minestom.server.instance;
import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.data.Data;
import net.minestom.server.instance.block.BlockProvider;
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
/**
* Represents a {@link Chunk} which does not store any block, it makes use of a {@link BlockProvider}
* instead to use less memory.
* <p>
* Can be used for very simple chunks such as flat or others with not any random factor.
* <p>
* WARNING: adding blocks or anything to this chunk would not work, it is static.
*/
public class StaticChunk extends Chunk {
protected final BlockProvider blockProvider;
public StaticChunk(@Nullable Biome[] biomes, int chunkX, int chunkZ,
@NotNull BlockProvider blockProvider) {
super(biomes, chunkX, chunkZ, false);
this.blockProvider = blockProvider;
setReadOnly(true);
}
@Override
public void UNSAFE_setBlock(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data, boolean updatable) {
//noop
}
@Override
public void tick(long time, @NotNull Instance instance) {
//noop
}
@Override
public short getBlockStateId(int x, int y, int z) {
return blockProvider.getBlockStateId(x, y, z);
}
@Override
public short getCustomBlockId(int x, int y, int z) {
//noop
return 0;
}
@Override
protected void refreshBlockValue(int x, int y, int z, short blockStateId, short customId) {
//noop
}
@Override
protected void refreshBlockStateId(int x, int y, int z, short blockStateId) {
//noop
}
@Override
public Data getBlockData(int index) {
return null;
}
@Override
public void setBlockData(int x, int y, int z, Data data) {
//noop
}
@NotNull
@Override
public Set<Integer> getBlockEntities() {
return new HashSet<>();
}
@Override
public byte[] getSerializedData() {
return null;
}
@Override
public void readChunk(@NotNull BinaryReader reader, @Nullable ChunkCallback callback) {
OptionalCallback.execute(callback, this);
}
@NotNull
@Override
protected ChunkDataPacket createFreshPacket() {
ChunkDataPacket fullDataPacket = new ChunkDataPacket();
fullDataPacket.biomes = biomes.clone();
fullDataPacket.chunkX = chunkX;
fullDataPacket.chunkZ = chunkZ;
short[] blocksStateId = new short[CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z];
for (int i = 0; i < blocksStateId.length; i++) {
final int x = ChunkUtils.blockIndexToChunkPositionX(i);
final int y = ChunkUtils.blockIndexToChunkPositionY(i);
final int z = ChunkUtils.blockIndexToChunkPositionZ(i);
blocksStateId[i] = blockProvider.getBlockStateId(x, y, z);
}
fullDataPacket.blocksStateId = blocksStateId;
fullDataPacket.customBlocksId = new short[0];
fullDataPacket.blockEntities = new HashSet<>();
fullDataPacket.blocksData = new Int2ObjectOpenHashMap<>();
return fullDataPacket;
}
@NotNull
@Override
public Chunk copy(int chunkX, int chunkZ) {
StaticChunk staticChunk = new StaticChunk(biomes.clone(), chunkX, chunkZ, blockProvider);
// Prevent re-writing the whole packet since it is static anyway
final ByteBuf packetBuffer = getFullDataPacket();
if (packetBuffer != null) {
staticChunk.setFullDataPacket(packetBuffer);
}
return staticChunk;
}
}

View File

@ -2,8 +2,8 @@ package net.minestom.server.instance;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.play.WorldBorderPacket; import net.minestom.server.network.packet.server.play.WorldBorderPacket;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Position; import net.minestom.server.utils.Position;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -100,6 +100,7 @@ public class WorldBorder {
*/ */
public void setWarningTime(int warningTime) { public void setWarningTime(int warningTime) {
this.warningTime = warningTime; this.warningTime = warningTime;
WorldBorderPacket worldBorderPacket = new WorldBorderPacket(); WorldBorderPacket worldBorderPacket = new WorldBorderPacket();
worldBorderPacket.action = WorldBorderPacket.Action.SET_WARNING_TIME; worldBorderPacket.action = WorldBorderPacket.Action.SET_WARNING_TIME;
worldBorderPacket.wbAction = new WorldBorderPacket.WBSetWarningTime(warningTime); worldBorderPacket.wbAction = new WorldBorderPacket.WBSetWarningTime(warningTime);
@ -115,6 +116,7 @@ public class WorldBorder {
*/ */
public void setWarningBlocks(int warningBlocks) { public void setWarningBlocks(int warningBlocks) {
this.warningBlocks = warningBlocks; this.warningBlocks = warningBlocks;
WorldBorderPacket worldBorderPacket = new WorldBorderPacket(); WorldBorderPacket worldBorderPacket = new WorldBorderPacket();
worldBorderPacket.action = WorldBorderPacket.Action.SET_WARNING_BLOCKS; worldBorderPacket.action = WorldBorderPacket.Action.SET_WARNING_BLOCKS;
worldBorderPacket.wbAction = new WorldBorderPacket.WBSetWarningBlocks(warningBlocks); worldBorderPacket.wbAction = new WorldBorderPacket.WBSetWarningBlocks(warningBlocks);
@ -137,6 +139,7 @@ public class WorldBorder {
this.speed = speed; this.speed = speed;
this.lerpStartTime = System.currentTimeMillis(); this.lerpStartTime = System.currentTimeMillis();
WorldBorderPacket worldBorderPacket = new WorldBorderPacket(); WorldBorderPacket worldBorderPacket = new WorldBorderPacket();
worldBorderPacket.action = WorldBorderPacket.Action.LERP_SIZE; worldBorderPacket.action = WorldBorderPacket.Action.LERP_SIZE;
worldBorderPacket.wbAction = new WorldBorderPacket.WBLerpSize(oldDiameter, newDiameter, speed); worldBorderPacket.wbAction = new WorldBorderPacket.WBLerpSize(oldDiameter, newDiameter, speed);
@ -270,10 +273,10 @@ public class WorldBorder {
/** /**
* Sends a {@link WorldBorderPacket} to all the instance players. * Sends a {@link WorldBorderPacket} to all the instance players.
* *
* @param worldBorderPacket the packet to send * @param packet the packet to send
*/ */
private void sendPacket(@NotNull WorldBorderPacket worldBorderPacket) { private void sendPacket(@NotNull WorldBorderPacket packet) {
PacketWriterUtils.writeAndSend(instance.getPlayers(), worldBorderPacket); PacketUtils.sendGroupedPacket(instance.getPlayers(), packet);
} }
public enum CollisionAxis { public enum CollisionAxis {

View File

@ -1,11 +1,18 @@
package net.minestom.server.instance.batch; package net.minestom.server.instance.batch;
import kotlin.collections.ArrayDeque; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import net.minestom.server.data.Data; import net.minestom.server.data.Data;
import net.minestom.server.instance.*; import net.minestom.server.instance.*;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.utils.block.CustomBlockUtils; import net.minestom.server.utils.block.CustomBlockUtils;
import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.chunk.ChunkCallback; import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
@ -21,49 +28,76 @@ import java.util.List;
*/ */
public class ChunkBatch implements InstanceBatch { public class ChunkBatch implements InstanceBatch {
private static final int INITIAL_SIZE = (Chunk.CHUNK_SIZE_X * Chunk.CHUNK_SIZE_Y * Chunk.CHUNK_SIZE_Z) / 2;
private final InstanceContainer instance; private final InstanceContainer instance;
private final Chunk chunk; private final Chunk chunk;
// Need to be synchronized manually // Need to be synchronized manually
private final ArrayDeque<BlockData> dataList = new ArrayDeque<>(INITIAL_SIZE); // Format: blockIndex/blockStateId/customBlockId (32/16/16 bits)
private final LongList blocks = new LongArrayList();
public ChunkBatch(InstanceContainer instance, Chunk chunk) { // Need to be synchronized manually
// block index - data
private final Int2ObjectMap<Data> blockDataMap = new Int2ObjectOpenHashMap<>();
public ChunkBatch(@NotNull InstanceContainer instance, @NotNull Chunk chunk) {
this.instance = instance; this.instance = instance;
this.chunk = chunk; this.chunk = chunk;
} }
@Override @Override
public void setBlockStateId(int x, int y, int z, short blockStateId, Data data) { public void setBlockStateId(int x, int y, int z, short blockStateId, @Nullable Data data) {
addBlockData((byte) x, y, (byte) z, blockStateId, (short) 0, data); addBlockData((byte) x, y, (byte) z, blockStateId, (short) 0, data);
} }
@Override @Override
public void setCustomBlock(int x, int y, int z, short customBlockId, Data data) { public void setCustomBlock(int x, int y, int z, short customBlockId, @Nullable Data data) {
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId); final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
Check.notNull(customBlock, "The custom block with the id " + customBlockId + " does not exist!");
addBlockData((byte) x, y, (byte) z, customBlock.getDefaultBlockStateId(), customBlockId, data); addBlockData((byte) x, y, (byte) z, customBlock.getDefaultBlockStateId(), customBlockId, data);
} }
@Override @Override
public void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, Data data) { public void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
addBlockData((byte) x, y, (byte) z, blockStateId, customBlockId, data); addBlockData((byte) x, y, (byte) z, blockStateId, customBlockId, data);
} }
private void addBlockData(byte x, int y, byte z, short blockStateId, short customBlockId, Data data) { private void addBlockData(byte x, int y, byte z, short blockStateId, short customBlockId, @Nullable Data data) {
// TODO store a single long with bitwise operators (xyz;boolean,short,short,boolean) with the data in a map final int index = ChunkUtils.getBlockIndex(x, y, z);
final BlockData blockData = new BlockData(x, y, z, blockStateId, customBlockId, data);
synchronized (dataList) { if (data != null) {
this.dataList.add(blockData); synchronized (blockDataMap) {
this.blockDataMap.put(index, data);
}
}
long value = index;
value = (value << 16) | blockStateId;
value = (value << 16) | customBlockId;
synchronized (blocks) {
this.blocks.add(value);
} }
} }
public void flushChunkGenerator(ChunkGenerator chunkGenerator, @Nullable ChunkCallback callback) { /**
* Called to fill the chunk batch.
*
* @param chunkGenerator the chunk generator
* @param callback the optional callback executed once the batch is done
*/
public void flushChunkGenerator(@NotNull ChunkGenerator chunkGenerator, @Nullable ChunkCallback callback) {
BLOCK_BATCH_POOL.execute(() -> { BLOCK_BATCH_POOL.execute(() -> {
final List<ChunkPopulator> populators = chunkGenerator.getPopulators(); final List<ChunkPopulator> populators = chunkGenerator.getPopulators();
final boolean hasPopulator = populators != null && !populators.isEmpty(); final boolean hasPopulator = populators != null && !populators.isEmpty();
chunkGenerator.generateChunkData(this, chunk.getChunkX(), chunk.getChunkZ()); chunkGenerator.generateChunkData(this, chunk.getChunkX(), chunk.getChunkZ());
// Check if there is anything to process
if (blocks.isEmpty() && !hasPopulator) {
OptionalCallback.execute(callback, chunk);
return;
}
singleThreadFlush(hasPopulator ? null : callback, true); singleThreadFlush(hasPopulator ? null : callback, true);
clearData(); // So the populators won't place those blocks again clearData(); // So the populators won't place those blocks again
@ -104,8 +138,8 @@ public class ChunkBatch implements InstanceBatch {
* Resets the chunk batch by removing all the entries. * Resets the chunk batch by removing all the entries.
*/ */
public void clearData() { public void clearData() {
synchronized (dataList) { synchronized (blocks) {
this.dataList.clear(); this.blocks.clear();
} }
} }
@ -116,13 +150,18 @@ public class ChunkBatch implements InstanceBatch {
* @param safeCallback true to run the callback in the instance update thread, otherwise run in the current one * @param safeCallback true to run the callback in the instance update thread, otherwise run in the current one
*/ */
private void singleThreadFlush(@Nullable ChunkCallback callback, boolean safeCallback) { private void singleThreadFlush(@Nullable ChunkCallback callback, boolean safeCallback) {
if (blocks.isEmpty()) {
OptionalCallback.execute(callback, chunk);
return;
}
synchronized (chunk) { synchronized (chunk) {
if (!chunk.isLoaded()) if (!chunk.isLoaded())
return; return;
synchronized (dataList) { synchronized (blocks) {
for (BlockData data : dataList) { for (long block : blocks) {
data.apply(chunk); apply(chunk, block);
} }
} }
@ -141,25 +180,29 @@ public class ChunkBatch implements InstanceBatch {
} }
} }
private static class BlockData { /**
* Places a block which is encoded in a long.
*
* @param chunk the chunk to place the block on
* @param value the block data
*/
private void apply(@NotNull Chunk chunk, long value) {
final short customBlockId = (short) (value & 0xFF);
final short blockId = (short) (value >> 16 & 0xFF);
final int index = (int) (value >> 32 & 0xFFFF);
private final int x, y, z; Data data = null;
private final short blockStateId; if (!blockDataMap.isEmpty()) {
private final short customBlockId; synchronized (blockDataMap) {
private final Data data; data = blockDataMap.get(index);
}
private BlockData(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
this.x = x;
this.y = y;
this.z = z;
this.blockStateId = blockStateId;
this.customBlockId = customBlockId;
this.data = data;
} }
public void apply(Chunk chunk) { chunk.UNSAFE_setBlock(ChunkUtils.blockIndexToChunkPositionX(index),
chunk.UNSAFE_setBlock(x, y, z, blockStateId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId)); ChunkUtils.blockIndexToChunkPositionY(index),
} ChunkUtils.blockIndexToChunkPositionZ(index),
blockId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId));
} }

View File

@ -25,7 +25,7 @@ public class BlockManager {
* @throws IllegalArgumentException if <code>customBlock</code> block id is not greater than 0 * @throws IllegalArgumentException if <code>customBlock</code> block id is not greater than 0
* @throws IllegalStateException if the id of <code>customBlock</code> is already registered * @throws IllegalStateException if the id of <code>customBlock</code> is already registered
*/ */
public void registerCustomBlock(@NotNull CustomBlock customBlock) { public synchronized void registerCustomBlock(@NotNull CustomBlock customBlock) {
final short id = customBlock.getCustomBlockId(); final short id = customBlock.getCustomBlockId();
Check.argCondition(id <= 0, "Custom block ID must be greater than 0, got: " + id); Check.argCondition(id <= 0, "Custom block ID must be greater than 0, got: " + id);
Check.stateCondition(customBlocksInternalId[id] != null, "a CustomBlock with the id " + id + " already exists"); Check.stateCondition(customBlocksInternalId[id] != null, "a CustomBlock with the id " + id + " already exists");
@ -41,7 +41,7 @@ public class BlockManager {
* @param blockPlacementRule the block placement rule to register * @param blockPlacementRule the block placement rule to register
* @throws IllegalArgumentException if <code>blockPlacementRule</code> block id is negative * @throws IllegalArgumentException if <code>blockPlacementRule</code> block id is negative
*/ */
public void registerBlockPlacementRule(@NotNull BlockPlacementRule blockPlacementRule) { public synchronized void registerBlockPlacementRule(@NotNull BlockPlacementRule blockPlacementRule) {
final short id = blockPlacementRule.getBlockId(); final short id = blockPlacementRule.getBlockId();
Check.argCondition(id < 0, "Block ID must be >= 0, got: " + id); Check.argCondition(id < 0, "Block ID must be >= 0, got: " + id);

View File

@ -1,6 +0,0 @@
package net.minestom.server.instance.block;
@FunctionalInterface
public interface BlockProvider {
short getBlockStateId(int x, int y, int z);
}

View File

@ -0,0 +1,373 @@
package net.minestom.server.instance.palette;
import it.unimi.dsi.fastutil.shorts.Short2ShortLinkedOpenHashMap;
import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
import net.minestom.server.instance.Chunk;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_COUNT;
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
/**
* Used to efficiently store blocks with an optional palette.
* <p>
* The format used is the one described in the {@link net.minestom.server.network.packet.server.play.ChunkDataPacket},
* the reason is that it allows us to write the packet much faster.
*/
public class PaletteStorage {
/**
* The maximum bits per entry value.
*/
private final static int MAXIMUM_BITS_PER_ENTRY = 15;
/**
* The minimum bits per entry value.
*/
private final static int MINIMUM_BITS_PER_ENTRY = 4;
/**
* The maximum bits per entry value which allow for a data palette.
*/
private final static int PALETTE_MAXIMUM_BITS = 8;
/**
* The number of blocks that should be in one chunk section.
*/
private final static int BLOCK_COUNT = CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE;
private int bitsPerEntry;
private final int bitsIncrement;
private int valuesPerLong;
private boolean hasPalette;
private long[][] sectionBlocks = new long[CHUNK_SECTION_COUNT][0];
// chunk section - palette index = block id
private Short2ShortLinkedOpenHashMap[] paletteBlockMaps = new Short2ShortLinkedOpenHashMap[CHUNK_SECTION_COUNT];
// chunk section - block id = palette index
private Short2ShortOpenHashMap[] blockPaletteMaps = new Short2ShortOpenHashMap[CHUNK_SECTION_COUNT];
/**
* Creates a new palette storage.
*
* @param bitsPerEntry the number of bits used for one entry (block)
* @param bitsIncrement the number of bits to add per-block once the palette array is filled
*/
public PaletteStorage(int bitsPerEntry, int bitsIncrement) {
Check.argCondition(bitsPerEntry > MAXIMUM_BITS_PER_ENTRY, "The maximum bits per entry is 15");
// Change the bitsPerEntry to be valid
if (bitsPerEntry < MINIMUM_BITS_PER_ENTRY) {
bitsPerEntry = MINIMUM_BITS_PER_ENTRY;
} else if (MathUtils.isBetween(bitsPerEntry, 9, 14)) {
bitsPerEntry = MAXIMUM_BITS_PER_ENTRY;
}
this.bitsPerEntry = bitsPerEntry;
this.bitsIncrement = bitsIncrement;
this.valuesPerLong = Long.SIZE / bitsPerEntry;
this.hasPalette = bitsPerEntry <= PALETTE_MAXIMUM_BITS;
}
public void setBlockAt(int x, int y, int z, short blockId) {
PaletteStorage.setBlockAt(this, x, y, z, blockId);
}
public short getBlockAt(int x, int y, int z) {
return PaletteStorage.getBlockAt(this, x, y, z);
}
/**
* Gets the number of bits that the palette currently take per block.
*
* @return the bits per entry
*/
public int getBitsPerEntry() {
return bitsPerEntry;
}
/**
* Gets the palette with the index and the block id as the value.
*
* @param section the chunk section to get the palette from
* @return the palette
*/
public short[] getPalette(int section) {
Short2ShortLinkedOpenHashMap paletteBlockMap = paletteBlockMaps[section];
return paletteBlockMap != null ? paletteBlockMap.values().toShortArray() : null;
}
/**
* Gets the sections of this object,
* the first array representing the chunk section and the second the block position from {@link #getSectionIndex(int, int, int)}.
*
* @return the section blocks
*/
public long[][] getSectionBlocks() {
return sectionBlocks;
}
/**
* Loops through all the sections and blocks to find unused array (empty chunk section)
* <p>
* Useful after clearing one or multiple sections of a chunk. Can be unnecessarily expensive if the chunk
* is composed of almost-empty sections since the loop will not stop until a non-air block is discovered.
*/
public synchronized void clean() {
for (int i = 0; i < sectionBlocks.length; i++) {
long[] section = sectionBlocks[i];
if (section.length != 0) {
boolean canClear = true;
for (long blockGroup : section) {
if (blockGroup != 0) {
canClear = false;
break;
}
}
if (canClear) {
sectionBlocks[i] = new long[0];
}
}
}
}
public PaletteStorage copy() {
PaletteStorage paletteStorage = new PaletteStorage(bitsPerEntry, bitsIncrement);
paletteStorage.sectionBlocks = sectionBlocks.clone();
paletteStorage.paletteBlockMaps = paletteBlockMaps.clone();
paletteStorage.blockPaletteMaps = blockPaletteMaps.clone();
return paletteStorage;
}
/**
* Retrieves the palette index for the specified block id.
* <p>
* Also responsible for resizing the palette when full.
*
* @param section the chunk section
* @param blockId the block id to convert
* @return the palette index of {@code blockId}
*/
private short getPaletteIndex(int section, short blockId) {
if (!hasPalette) {
return blockId;
}
Short2ShortOpenHashMap blockPaletteMap = blockPaletteMaps[section];
if (blockPaletteMap == null) {
blockPaletteMap = createBlockPaletteMap();
blockPaletteMaps[section] = blockPaletteMap;
}
if (!blockPaletteMap.containsKey(blockId)) {
Short2ShortLinkedOpenHashMap paletteBlockMap = paletteBlockMaps[section];
if (paletteBlockMap == null) {
paletteBlockMap = createPaletteBlockMap();
paletteBlockMaps[section] = paletteBlockMap;
}
// Resize the palette if full
if (paletteBlockMap.size() >= getMaxPaletteSize()) {
resize(bitsPerEntry + bitsIncrement);
}
final short paletteIndex = (short) (paletteBlockMap.lastShortKey() + 1);
paletteBlockMap.put(paletteIndex, blockId);
blockPaletteMap.put(blockId, paletteIndex);
return paletteIndex;
}
return blockPaletteMap.get(blockId);
}
/**
* Resizes the array.
* <p>
* Will create a new palette storage to set all the current blocks, and the data will be transferred to 'this'.
*
* @param newBitsPerEntry the new bits per entry count
*/
private synchronized void resize(int newBitsPerEntry) {
PaletteStorage paletteStorageCache = new PaletteStorage(newBitsPerEntry, bitsIncrement);
paletteStorageCache.paletteBlockMaps = paletteBlockMaps;
paletteStorageCache.blockPaletteMaps = blockPaletteMaps;
for (int y = 0; y < Chunk.CHUNK_SIZE_Y; y++) {
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
final short blockId = getBlockAt(x, y, z);
paletteStorageCache.setBlockAt(x, y, z, blockId);
}
}
}
this.bitsPerEntry = newBitsPerEntry;
this.valuesPerLong = paletteStorageCache.valuesPerLong;
this.hasPalette = paletteStorageCache.hasPalette;
this.sectionBlocks = paletteStorageCache.sectionBlocks;
}
/**
* Gets the maximum number of blocks that the current palette (could be the global one) can take.
*
* @return the number of blocks possible in the palette
*/
private int getMaxPaletteSize() {
return 1 << bitsPerEntry;
}
// Magic values generated with "Integer.MAX_VALUE >> (31 - bitsPerIndex)" for bitsPerIndex between 4 and 15
private static final int[] MAGIC_MASKS =
{0, 0, 0, 0,
15, 31, 63, 127, 255,
511, 1023, 2047, 4095,
8191, 16383, 32767};
private static void setBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z, short blockId) {
if (y < 0 || y >= Chunk.CHUNK_SIZE_Y) {
return;
}
final int section = ChunkUtils.getSectionAt(y);
final int valuesPerLong = paletteStorage.valuesPerLong;
final int bitsPerEntry = paletteStorage.bitsPerEntry;
if (paletteStorage.sectionBlocks[section].length == 0) {
if (blockId == 0) {
// Section is empty and method is trying to place an air block, stop unnecessary computation
return;
}
// Initialize the section
paletteStorage.sectionBlocks[section] = new long[getSize(valuesPerLong)];
}
x = toChunkCoordinate(x);
z = toChunkCoordinate(z);
// Change to palette value
blockId = paletteStorage.getPaletteIndex(section, blockId);
final int sectionIndex = getSectionIndex(x, y, z);
final int index = sectionIndex / valuesPerLong;
final int bitIndex = (sectionIndex % valuesPerLong) * bitsPerEntry;
final long[] sectionBlock = paletteStorage.sectionBlocks[section];
long block = sectionBlock[index];
{
final long clear = MAGIC_MASKS[bitsPerEntry];
block |= clear << bitIndex;
block ^= clear << bitIndex;
block |= (long) blockId << bitIndex;
sectionBlock[index] = block;
}
}
private static short getBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z) {
if (y < 0 || y >= Chunk.CHUNK_SIZE_Y) {
return 0;
}
final int section = ChunkUtils.getSectionAt(y);
final long[] blocks;
// Retrieve the longs and check if the section is empty
{
blocks = paletteStorage.sectionBlocks[section];
if (blocks.length == 0) {
// Section is not loaded, can only be air
return 0;
}
}
x = toChunkCoordinate(x);
z = toChunkCoordinate(z);
final int sectionIndex = getSectionIndex(x, y, z);
final int valuesPerLong = paletteStorage.valuesPerLong;
final int bitsPerEntry = paletteStorage.bitsPerEntry;
final int index = sectionIndex / valuesPerLong;
final int bitIndex = sectionIndex % valuesPerLong * bitsPerEntry;
final long value = blocks[index] >> bitIndex & MAGIC_MASKS[bitsPerEntry];
// Change to palette value and return
return paletteStorage.hasPalette ?
paletteStorage.paletteBlockMaps[section].get((short) value) :
(short) value;
}
private static Short2ShortLinkedOpenHashMap createPaletteBlockMap() {
Short2ShortLinkedOpenHashMap map = new Short2ShortLinkedOpenHashMap(CHUNK_SECTION_SIZE);
map.put((short) 0, (short) 0);
return map;
}
private static Short2ShortOpenHashMap createBlockPaletteMap() {
Short2ShortOpenHashMap map = new Short2ShortOpenHashMap(CHUNK_SECTION_SIZE);
map.put((short) 0, (short) 0);
return map;
}
/**
* Gets the array length of one section based on the number of values which can be stored in one long.
*
* @param valuesPerLong the number of values per long
* @return the array length based on {@code valuesPerLong}
*/
private static int getSize(int valuesPerLong) {
return (BLOCK_COUNT + valuesPerLong - 1) / valuesPerLong;
}
/**
* Converts a world coordinate to a chunk one.
*
* @param xz the world coordinate
* @return the chunk coordinate of {@code xz}
*/
private static int toChunkCoordinate(int xz) {
xz %= 16;
if (xz < 0) {
xz += CHUNK_SECTION_SIZE;
}
return xz;
}
/**
* Gets the index of the block on the section array based on the block position.
*
* @param x the chunk X
* @param y the chunk Y
* @param z the chunk Z
* @return the section index of the position
*/
public static int getSectionIndex(int x, int y, int z) {
y %= CHUNK_SECTION_SIZE;
return y << 8 | z << 4 | x;
}
}

View File

@ -186,7 +186,6 @@ public class Inventory implements InventoryModifier, InventoryClickHandler, View
setItemStackInternal(i, ItemStack.getAirItem()); setItemStackInternal(i, ItemStack.getAirItem());
} }
// Send the cleared inventory to viewers // Send the cleared inventory to viewers
// TODO cached packet with empty content
update(); update();
} }

View File

@ -13,7 +13,6 @@ import net.minestom.server.inventory.click.InventoryClickResult;
import net.minestom.server.inventory.condition.InventoryCondition; import net.minestom.server.inventory.condition.InventoryCondition;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.StackingRule; import net.minestom.server.item.StackingRule;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.play.EntityEquipmentPacket; import net.minestom.server.network.packet.server.play.EntityEquipmentPacket;
import net.minestom.server.network.packet.server.play.SetSlotPacket; import net.minestom.server.network.packet.server.play.SetSlotPacket;
import net.minestom.server.network.packet.server.play.WindowItemsPacket; import net.minestom.server.network.packet.server.play.WindowItemsPacket;
@ -214,7 +213,7 @@ public class PlayerInventory implements InventoryModifier, InventoryClickHandler
* the inventory items * the inventory items
*/ */
public void update() { public void update() {
PacketWriterUtils.writeAndSend(player, createWindowItemsPacket()); player.getPlayerConnection().sendPacket(createWindowItemsPacket());
} }
/** /**
@ -336,7 +335,7 @@ public class PlayerInventory implements InventoryModifier, InventoryClickHandler
/** /**
* Refreshes an inventory slot. * Refreshes an inventory slot.
* *
* @param slot the packet slot * @param slot the packet slot,
* see {@link net.minestom.server.utils.inventory.PlayerInventoryUtils#convertToPacketSlot(int)} * see {@link net.minestom.server.utils.inventory.PlayerInventoryUtils#convertToPacketSlot(int)}
* @param itemStack the item stack in the slot * @param itemStack the item stack in the slot
*/ */

View File

@ -37,7 +37,7 @@ import java.util.*;
*/ */
public class ItemStack implements DataContainer { public class ItemStack implements DataContainer {
private static final StackingRule DEFAULT_STACKING_RULE = new VanillaStackingRule(64); private static final StackingRule VANILLA_STACKING_RULE = new VanillaStackingRule(64);
private Material material; private Material material;
@ -76,7 +76,7 @@ public class ItemStack implements DataContainer {
{ {
if (defaultStackingRule == null) if (defaultStackingRule == null)
defaultStackingRule = DEFAULT_STACKING_RULE; defaultStackingRule = VANILLA_STACKING_RULE;
this.stackingRule = defaultStackingRule; this.stackingRule = defaultStackingRule;
} }
@ -767,4 +767,4 @@ public class ItemStack implements DataContainer {
public void onInventoryClick(@NotNull Player player, @NotNull ClickType clickType, int slot, boolean playerInventory) { public void onInventoryClick(@NotNull Player player, @NotNull ClickType clickType, int slot, boolean playerInventory) {
} }
} }

View File

@ -2,6 +2,7 @@ package net.minestom.server.item.rule;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.StackingRule; import net.minestom.server.item.StackingRule;
import net.minestom.server.utils.MathUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class VanillaStackingRule extends StackingRule { public class VanillaStackingRule extends StackingRule {
@ -17,7 +18,7 @@ public class VanillaStackingRule extends StackingRule {
@Override @Override
public boolean canApply(@NotNull ItemStack item, int newAmount) { public boolean canApply(@NotNull ItemStack item, int newAmount) {
return newAmount > 0 && newAmount <= getMaxSize(); return MathUtils.isBetween(newAmount, 0, getMaxSize());
} }
@NotNull @NotNull

View File

@ -9,9 +9,9 @@ import net.minestom.server.command.CommandManager;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerChatEvent; import net.minestom.server.event.player.PlayerChatEvent;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.client.play.ClientChatMessagePacket; import net.minestom.server.network.packet.client.play.ClientChatMessagePacket;
import net.minestom.server.network.packet.server.play.ChatMessagePacket; import net.minestom.server.network.packet.server.play.ChatMessagePacket;
import net.minestom.server.utils.PacketUtils;
import java.util.Collection; import java.util.Collection;
import java.util.function.Function; import java.util.function.Function;
@ -62,7 +62,7 @@ public class ChatMessageListener {
ChatMessagePacket chatMessagePacket = ChatMessagePacket chatMessagePacket =
new ChatMessagePacket(jsonMessage, ChatMessagePacket.Position.CHAT, player.getUuid()); new ChatMessagePacket(jsonMessage, ChatMessagePacket.Position.CHAT, player.getUuid());
PacketWriterUtils.writeAndSend(recipients, chatMessagePacket); PacketUtils.sendGroupedPacket(recipients, chatMessagePacket);
} }
}); });

View File

@ -13,8 +13,9 @@ public class CreativeInventoryActionListener {
if (player.getGameMode() != GameMode.CREATIVE) if (player.getGameMode() != GameMode.CREATIVE)
return; return;
final ItemStack item = packet.item;
short slot = packet.slot; short slot = packet.slot;
final ItemStack item = packet.item;
if (slot != -1) { if (slot != -1) {
// Set item // Set item
slot = (short) PlayerInventoryUtils.convertSlot(slot, PlayerInventoryUtils.OFFSET); slot = (short) PlayerInventoryUtils.convertSlot(slot, PlayerInventoryUtils.OFFSET);

View File

@ -8,6 +8,7 @@ import net.minestom.server.network.packet.client.play.ClientPlayerPositionPacket
import net.minestom.server.network.packet.client.play.ClientPlayerRotationPacket; import net.minestom.server.network.packet.client.play.ClientPlayerRotationPacket;
import net.minestom.server.utils.Position; import net.minestom.server.utils.Position;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
public class PlayerPositionListener { public class PlayerPositionListener {
@ -47,7 +48,7 @@ public class PlayerPositionListener {
processMovement(player, x, y, z, yaw, pitch, onGround); processMovement(player, x, y, z, yaw, pitch, onGround);
} }
private static void processMovement(Player player, float x, float y, float z, private static void processMovement(@NotNull Player player, float x, float y, float z,
float yaw, float pitch, boolean onGround) { float yaw, float pitch, boolean onGround) {
// Try to move in an unloaded chunk, prevent it // Try to move in an unloaded chunk, prevent it

View File

@ -1,21 +1,24 @@
package net.minestom.server.listener.manager; package net.minestom.server.listener.manager;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.network.ConnectionManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Interface used to add a listener for incoming packets with {@link net.minestom.server.network.ConnectionManager#onPacketReceive(PacketConsumer)}. * Interface used to add a listener for incoming/outgoing packets with
* {@link ConnectionManager#onPacketReceive(PacketConsumer)} and {@link ConnectionManager#onPacketSend(PacketConsumer)}.
*
* @param <T> the packet type
*/ */
@FunctionalInterface @FunctionalInterface
public interface PacketConsumer { public interface PacketConsumer<T> {
/** /**
* Called when a packet is received from the client. * Called when a packet is received/sent from/to a client.
* *
* @param player the player who sent the packet * @param player the player concerned by the packet
* @param packetController the packet controller, used to cancel or control which listener will be called * @param packetController the packet controller, can be used to cancel the packet
* @param packet the received packet * @param packet the packet
*/ */
void accept(@NotNull Player player, @NotNull PacketController packetController, @NotNull ClientPlayPacket packet); void accept(@NotNull Player player, @NotNull PacketController packetController, @NotNull T packet);
} }

View File

@ -1,19 +1,15 @@
package net.minestom.server.listener.manager; package net.minestom.server.listener.manager;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.client.ClientPlayPacket;
import org.jetbrains.annotations.Nullable;
/** /**
* Used to control the output of a packet in {@link PacketConsumer#accept(Player, PacketController, ClientPlayPacket)}. * Used to control the output of a packet in {@link PacketConsumer#accept(Player, PacketController, Object)}.
*/ */
public class PacketController { public class PacketController {
private boolean cancel; private boolean cancel;
private PacketListenerConsumer packetListenerConsumer;
protected PacketController(@Nullable PacketListenerConsumer packetListenerConsumer) { protected PacketController() {
this.packetListenerConsumer = packetListenerConsumer;
} }
/** /**
@ -33,25 +29,4 @@ public class PacketController {
public void setCancel(boolean cancel) { public void setCancel(boolean cancel) {
this.cancel = cancel; this.cancel = cancel;
} }
/**
* Gets the listener associated with the packet.
*
* @return the packet's listener, null if not present
*/
@Nullable
public PacketListenerConsumer getPacketListenerConsumer() {
return packetListenerConsumer;
}
/**
* Changes the packet listener, setting it to null cancel the listener.
* <p>
* WARNING: this will overwrite the default minestom listener, be sure to know what you are doing.
*
* @param packetListenerConsumer the new packet listener, can be null
*/
public void setPacketListenerConsumer(@Nullable PacketListenerConsumer packetListenerConsumer) {
this.packetListenerConsumer = packetListenerConsumer;
}
} }

View File

@ -6,13 +6,17 @@ import net.minestom.server.listener.*;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.network.packet.client.ClientPlayPacket;
import net.minestom.server.network.packet.client.play.*; import net.minestom.server.network.packet.client.play.*;
import net.minestom.server.network.packet.server.ServerPacket;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public final class PacketListenerManager { public final class PacketListenerManager {
public final static Logger LOGGER = LoggerFactory.getLogger(PacketListenerManager.class);
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager(); private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private final Map<Class<? extends ClientPlayPacket>, PacketListenerConsumer> listeners = new ConcurrentHashMap<>(); private final Map<Class<? extends ClientPlayPacket>, PacketListenerConsumer> listeners = new ConcurrentHashMap<>();
@ -56,7 +60,7 @@ public final class PacketListenerManager {
* @param player the player who sent the packet * @param player the player who sent the packet
* @param <T> the packet type * @param <T> the packet type
*/ */
public <T extends ClientPlayPacket> void process(@NotNull T packet, @NotNull Player player) { public <T extends ClientPlayPacket> void processClientPacket(@NotNull T packet, @NotNull Player player) {
final Class clazz = packet.getClass(); final Class clazz = packet.getClass();
@ -64,26 +68,37 @@ public final class PacketListenerManager {
// Listener can be null if none has been set before, call PacketConsumer anyway // Listener can be null if none has been set before, call PacketConsumer anyway
if (packetListenerConsumer == null) { if (packetListenerConsumer == null) {
System.err.println("Packet " + clazz + " does not have any default listener! (The issue comes from Minestom)"); LOGGER.error("Packet " + clazz + " does not have any default listener! (The issue comes from Minestom)");
return;
} }
final PacketController packetController = new PacketController();
final PacketController packetController = new PacketController(packetListenerConsumer); for (PacketConsumer<ClientPlayPacket> packetConsumer : CONNECTION_MANAGER.getReceivePacketConsumers()) {
for (PacketConsumer packetConsumer : CONNECTION_MANAGER.getReceivePacketConsumers()) {
packetConsumer.accept(player, packetController, packet); packetConsumer.accept(player, packetController, packet);
} }
if (packetController.isCancel()) if (packetController.isCancel())
return; return;
// Get the new listener (or the same) from the packet controller // Finally execute the listener
packetListenerConsumer = packetController.getPacketListenerConsumer(); packetListenerConsumer.accept(packet, player);
}
// Call the listener if not null /**
// (can be null because no listener is set, or because it has been changed by the controller) * Executes the consumers from {@link ConnectionManager#onPacketSend(PacketConsumer)}.
if (packetListenerConsumer != null) { *
packetListenerConsumer.accept(packet, player); * @param packet the packet to process
* @param player the player which should receive the packet
* @param <T> the packet type
* @return true if the packet is not cancelled, false otherwise
*/
public <T extends ServerPacket> boolean processServerPacket(@NotNull T packet, @NotNull Player player) {
final PacketController packetController = new PacketController();
for (PacketConsumer<ServerPacket> packetConsumer : CONNECTION_MANAGER.getSendPacketConsumers()) {
packetConsumer.accept(player, packetController, packet);
} }
return !packetController.isCancel();
} }
/** /**

View File

@ -4,10 +4,13 @@ import net.minestom.server.chat.JsonMessage;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.entity.fakeplayer.FakePlayer; import net.minestom.server.entity.fakeplayer.FakePlayer;
import net.minestom.server.listener.manager.PacketConsumer; import net.minestom.server.listener.manager.PacketConsumer;
import net.minestom.server.network.packet.client.ClientPlayPacket;
import net.minestom.server.network.packet.client.login.LoginStartPacket; import net.minestom.server.network.packet.client.login.LoginStartPacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.LoginSuccessPacket; import net.minestom.server.network.packet.server.login.LoginSuccessPacket;
import net.minestom.server.network.packet.server.play.ChatMessagePacket; import net.minestom.server.network.packet.server.play.ChatMessagePacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.callback.validator.PlayerValidator; import net.minestom.server.utils.callback.validator.PlayerValidator;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -26,7 +29,9 @@ public final class ConnectionManager {
private final Map<PlayerConnection, Player> connectionPlayerMap = Collections.synchronizedMap(new HashMap<>()); private final Map<PlayerConnection, Player> connectionPlayerMap = Collections.synchronizedMap(new HashMap<>());
// All the consumers to call once a packet is received // All the consumers to call once a packet is received
private final List<PacketConsumer> receivePacketConsumers = new CopyOnWriteArrayList<>(); private final List<PacketConsumer<ClientPlayPacket>> receivePacketConsumers = new CopyOnWriteArrayList<>();
// All the consumers to call once a packet is sent
private final List<PacketConsumer<ServerPacket>> sendPacketConsumers = new CopyOnWriteArrayList<>();
// The uuid provider once a player login // The uuid provider once a player login
private UuidProvider uuidProvider; private UuidProvider uuidProvider;
// The player provider to have your own Player implementation // The player provider to have your own Player implementation
@ -115,7 +120,8 @@ public final class ConnectionManager {
private void broadcastJson(@NotNull String json, @NotNull Collection<Player> recipients) { private void broadcastJson(@NotNull String json, @NotNull Collection<Player> recipients) {
ChatMessagePacket chatMessagePacket = ChatMessagePacket chatMessagePacket =
new ChatMessagePacket(json, ChatMessagePacket.Position.SYSTEM_MESSAGE); new ChatMessagePacket(json, ChatMessagePacket.Position.SYSTEM_MESSAGE);
PacketWriterUtils.writeAndSend(recipients, chatMessagePacket);
PacketUtils.sendGroupedPacket(recipients, chatMessagePacket);
} }
private Collection<Player> getRecipients(@Nullable PlayerValidator condition) { private Collection<Player> getRecipients(@Nullable PlayerValidator condition) {
@ -142,7 +148,7 @@ public final class ConnectionManager {
* @return an unmodifiable list of packet's consumers * @return an unmodifiable list of packet's consumers
*/ */
@NotNull @NotNull
public List<PacketConsumer> getReceivePacketConsumers() { public List<PacketConsumer<ClientPlayPacket>> getReceivePacketConsumers() {
return Collections.unmodifiableList(receivePacketConsumers); return Collections.unmodifiableList(receivePacketConsumers);
} }
@ -151,14 +157,39 @@ public final class ConnectionManager {
* *
* @param packetConsumer the packet consumer * @param packetConsumer the packet consumer
*/ */
public void onPacketReceive(@NotNull PacketConsumer packetConsumer) { public void onPacketReceive(@NotNull PacketConsumer<ClientPlayPacket> packetConsumer) {
this.receivePacketConsumers.add(packetConsumer); this.receivePacketConsumers.add(packetConsumer);
} }
/**
* Gets all the listeners which are called for each packet sent.
*
* @return an unmodifiable list of packet's consumers
*/
@NotNull
public List<PacketConsumer<ServerPacket>> getSendPacketConsumers() {
return Collections.unmodifiableList(sendPacketConsumers);
}
/**
* Adds a consumer to call once a packet is sent.
* <p>
* Be aware that it is possible for the same packet instance to be used multiple time,
* changing the object fields could lead to issues.
* (consider canceling the packet instead and send your own)
*
* @param packetConsumer the packet consumer
*/
public void onPacketSend(@NotNull PacketConsumer<ServerPacket> packetConsumer) {
this.sendPacketConsumers.add(packetConsumer);
}
/** /**
* Changes how {@link UUID} are attributed to players. * Changes how {@link UUID} are attributed to players.
* <p> * <p>
* Shouldn't be override if already defined. * Shouldn't be override if already defined.
* <p>
* Be aware that it is possible for an UUID provider to be ignored, for example in the case of a proxy (eg: velocity).
* *
* @param uuidProvider the new player connection uuid provider, * @param uuidProvider the new player connection uuid provider,
* setting it to null would apply a random UUID for each player connection * setting it to null would apply a random UUID for each player connection
@ -248,10 +279,13 @@ public final class ConnectionManager {
* @param uuid the new player uuid * @param uuid the new player uuid
* @param username the new player username * @param username the new player username
* @param connection the new player connection * @param connection the new player connection
* @return the newly created player object
*/ */
public void createPlayer(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection connection) { @NotNull
public Player createPlayer(@NotNull UUID uuid, @NotNull String username, @NotNull PlayerConnection connection) {
final Player player = getPlayerProvider().createPlayer(uuid, username, connection); final Player player = getPlayerProvider().createPlayer(uuid, username, connection);
createPlayer(player); createPlayer(player);
return player;
} }
/** /**
@ -277,12 +311,14 @@ public final class ConnectionManager {
* @param connection the player connection * @param connection the player connection
* @param uuid the uuid of the player * @param uuid the uuid of the player
* @param username the username of the player * @param username the username of the player
* @return the newly created player object
*/ */
public void startPlayState(@NotNull PlayerConnection connection, @NotNull UUID uuid, @NotNull String username) { @NotNull
public Player startPlayState(@NotNull PlayerConnection connection, @NotNull UUID uuid, @NotNull String username) {
LoginSuccessPacket loginSuccessPacket = new LoginSuccessPacket(uuid, username); LoginSuccessPacket loginSuccessPacket = new LoginSuccessPacket(uuid, username);
connection.sendPacket(loginSuccessPacket); connection.sendPacket(loginSuccessPacket);
connection.setConnectionState(ConnectionState.PLAY); connection.setConnectionState(ConnectionState.PLAY);
createPlayer(uuid, username, connection); return createPlayer(uuid, username, connection);
} }
} }

View File

@ -23,6 +23,16 @@ import org.slf4j.LoggerFactory;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* Responsible for processing client packets.
* <p>
* You can retrieve the different packet handlers per state (status/login/play)
* from the {@link net.minestom.server.network.packet.client.handler.ClientPacketsHandler} class.
* <p>
* Packet handlers are cached here and can be retrieved with {@link #getStatusPacketsHandler()}, {@link #getLoginPacketsHandler()}
* and {@link #getPlayPacketsHandler()}. The one to use depend on the type of packet you need to retrieve (the packet id 0 does not have
* the same meaning as it is a login or play packet).
*/
public final class PacketProcessor { public final class PacketProcessor {
private final static Logger LOGGER = LoggerFactory.getLogger(PacketProcessor.class); private final static Logger LOGGER = LoggerFactory.getLogger(PacketProcessor.class);
@ -94,10 +104,43 @@ public final class PacketProcessor {
return connectionPlayerConnectionMap.get(channel); return connectionPlayerConnectionMap.get(channel);
} }
public void removePlayerConnection(ChannelHandlerContext channel) { public void removePlayerConnection(@NotNull ChannelHandlerContext channel) {
connectionPlayerConnectionMap.remove(channel); connectionPlayerConnectionMap.remove(channel);
} }
/**
* Gets the handler for client status packets.
*
* @return the status packets handler
* @see <a href="https://wiki.vg/Protocol#Status">Status packets</a>
*/
@NotNull
public ClientStatusPacketsHandler getStatusPacketsHandler() {
return statusPacketsHandler;
}
/**
* Gets the handler for client login packets.
*
* @return the status login handler
* @see <a href="https://wiki.vg/Protocol#Login">Login packets</a>
*/
@NotNull
public ClientLoginPacketsHandler getLoginPacketsHandler() {
return loginPacketsHandler;
}
/**
* Gets the handler for client play packets.
*
* @return the play packets handler
* @see <a href="https://wiki.vg/Protocol#Play">Play packets</a>
*/
@NotNull
public ClientPlayPacketsHandler getPlayPacketsHandler() {
return playPacketsHandler;
}
/** /**
* Calls {@link Readable#read(BinaryReader)} and catch all the exceptions to be printed using the packet processor logger. * Calls {@link Readable#read(BinaryReader)} and catch all the exceptions to be printed using the packet processor logger.
* *
@ -106,12 +149,20 @@ public final class PacketProcessor {
* @param reader the buffer containing the packet * @param reader the buffer containing the packet
*/ */
private void safeRead(@NotNull PlayerConnection connection, @NotNull Readable readable, @NotNull BinaryReader reader) { private void safeRead(@NotNull PlayerConnection connection, @NotNull Readable readable, @NotNull BinaryReader reader) {
final int readableBytes = reader.available();
// Check if there is anything to read
if (readableBytes == 0) {
return;
}
try { try {
readable.read(reader); readable.read(reader);
} catch (Exception e) { } catch (Exception e) {
final Player player = connection.getPlayer(); final Player player = connection.getPlayer();
final String username = player != null ? player.getUsername() : "null"; final String username = player != null ? player.getUsername() : "null";
LOGGER.warn("Connection " + connection.getRemoteAddress() + " (" + username + ") sent an unexpected packet."); LOGGER.warn("Connection " + connection.getRemoteAddress() + " (" + username + ") sent an unexpected packet.");
e.printStackTrace();
} }
} }
} }

View File

@ -1,101 +0,0 @@
package net.minestom.server.network;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.player.PlayerUtils;
import net.minestom.server.utils.thread.MinestomThread;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
/**
* Utils class used to write {@link ServerPacket} in the appropriate thread pool.
* <p>
* WARNING: those methods do not guarantee a receive order.
*/
public final class PacketWriterUtils {
private static final ExecutorService PACKET_WRITER_POOL = new MinestomThread(MinecraftServer.THREAD_COUNT_PACKET_WRITER, MinecraftServer.THREAD_NAME_PACKET_WRITER);
/**
* Writes the {@link ServerPacket} in the writer thread pool.
* <p>
* WARNING: should not be used if the packet receive order is important
*
* @param serverPacket the packet to write
* @param consumer the consumer called once the packet has been written
*/
public static void writeCallbackPacket(@NotNull ServerPacket serverPacket, @NotNull Consumer<ByteBuf> consumer) {
PACKET_WRITER_POOL.execute(() -> {
final ByteBuf buffer = PacketUtils.writePacket(serverPacket);
consumer.accept(buffer);
});
}
/**
* Writes a {@link ServerPacket} in the writer thread pool and send it to every players in {@code players}.
* <p>
* WARNING: should not be used if the packet receive order is important
*
* @param players the players list to send the packet to
* @param serverPacket the packet to write and send
*/
public static void writeAndSend(@NotNull Collection<Player> players, @NotNull ServerPacket serverPacket) {
PACKET_WRITER_POOL.execute(() -> {
if (players.isEmpty())
return;
final ByteBuf buffer = PacketUtils.writePacket(serverPacket);
for (Player player : players) {
final PlayerConnection playerConnection = player.getPlayerConnection();
if (PlayerUtils.isNettyClient(player)) {
playerConnection.writePacket(buffer, true);
} else {
playerConnection.sendPacket(serverPacket);
}
}
buffer.release();
});
}
/**
* Writes a {@link ServerPacket} and send it to a {@link PlayerConnection}.
* <p>
* WARNING: should not be used if the packet receive order is important
*
* @param playerConnection the connection to send the packet to
* @param serverPacket the packet to write and send
*/
public static void writeAndSend(@NotNull PlayerConnection playerConnection, @NotNull ServerPacket serverPacket) {
PACKET_WRITER_POOL.execute(() -> {
if (PlayerUtils.isNettyClient(playerConnection)) {
final ByteBuf buffer = PacketUtils.writePacket(serverPacket);
buffer.retain();
playerConnection.writePacket(buffer, false);
buffer.release();
} else {
playerConnection.sendPacket(serverPacket);
}
});
}
/**
* Writes a {@link ServerPacket} and send it to a {@link Player}.
* <p>
* WARNING: should not be used if the packet receive order is important
*
* @param player the player to send the packet to
* @param serverPacket the packet to write and send
*/
public static void writeAndSend(@NotNull Player player, @NotNull ServerPacket serverPacket) {
final PlayerConnection playerConnection = player.getPlayerConnection();
writeAndSend(playerConnection, serverPacket);
}
}

View File

@ -5,6 +5,9 @@ import io.netty.channel.*;
import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
@ -29,24 +32,29 @@ public class NettyServer {
private String address; private String address;
private int port; private int port;
public NettyServer(PacketProcessor packetProcessor) { public NettyServer(@NotNull PacketProcessor packetProcessor) {
Class<? extends ServerChannel> channel; Class<? extends ServerChannel> channel;
if (Epoll.isAvailable()) { if (Epoll.isAvailable()) {
boss = new EpollEventLoopGroup(2); boss = new EpollEventLoopGroup(2);
worker = new EpollEventLoopGroup(); worker = new EpollEventLoopGroup(); // thread count = core * 2
channel = EpollServerSocketChannel.class; channel = EpollServerSocketChannel.class;
} else if (KQueue.isAvailable()) {
boss = new KQueueEventLoopGroup(2);
worker = new KQueueEventLoopGroup(); // thread count = core * 2
channel = KQueueServerSocketChannel.class;
} else { } else {
boss = new NioEventLoopGroup(2); boss = new NioEventLoopGroup(2);
worker = new NioEventLoopGroup(); worker = new NioEventLoopGroup(); // thread count = core * 2
channel = NioServerSocketChannel.class; channel = NioServerSocketChannel.class;
} }
bootstrap = new ServerBootstrap(); bootstrap = new ServerBootstrap()
bootstrap.group(boss, worker); .group(boss, worker)
bootstrap.channel(channel); .channel(channel);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(@NotNull SocketChannel ch) { protected void initChannel(@NotNull SocketChannel ch) {
@ -55,10 +63,11 @@ public class NettyServer {
ChannelPipeline pipeline = ch.pipeline(); ChannelPipeline pipeline = ch.pipeline();
// First check should verify if the packet is a legacy ping (from 1.6 version and earlier)
pipeline.addLast("legacy-ping", new LegacyPingHandler()); pipeline.addLast("legacy-ping", new LegacyPingHandler());
// Adds packetLength at start | Reads framed bytebuf // Adds packetLength at start | Reads framed bytebuf
pipeline.addLast("framer", new PacketFramer()); pipeline.addLast("framer", new PacketFramer(packetProcessor));
// Reads bytebuf and creating inbound packet // Reads bytebuf and creating inbound packet
pipeline.addLast("decoder", new PacketDecoder()); pipeline.addLast("decoder", new PacketDecoder());

View File

@ -2,7 +2,6 @@ package net.minestom.server.network.netty.channel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
@ -10,14 +9,17 @@ import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.packet.InboundPacket; import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerConnection;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> { public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
private final static Logger LOGGER = LoggerFactory.getLogger(ClientChannel.class);
private final ConnectionManager connectionManager = MinecraftServer.getConnectionManager(); private final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
private final PacketProcessor packetProcessor; private final PacketProcessor packetProcessor;
public ClientChannel(PacketProcessor packetProcessor) { public ClientChannel(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor; this.packetProcessor = packetProcessor;
} }
@ -34,9 +36,10 @@ public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
final int availableBytes = packet.body.readableBytes(); final int availableBytes = packet.body.readableBytes();
if (availableBytes > 0) { if (availableBytes > 0) {
// TODO log4j2 final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
System.err.println("WARNING: Packet 0x" + Integer.toHexString(packet.packetId)
+ " not fully read (" + availableBytes + " bytes left)"); LOGGER.warn("WARNING: Packet 0x" + Integer.toHexString(packet.packetId)
+ " not fully read (" + availableBytes + " bytes left), " + playerConnection);
packet.body.skipBytes(availableBytes); packet.body.skipBytes(availableBytes);
} }
@ -60,7 +63,7 @@ public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
@Override @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.info(cause.getMessage()); LOGGER.info(cause.getMessage());
cause.printStackTrace(); cause.printStackTrace();
ctx.close(); ctx.close();
} }

View File

@ -27,45 +27,38 @@ import java.util.List;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.Inflater; import java.util.zip.Inflater;
// TODO Optimize
public class PacketCompressor extends ByteToMessageCodec<ByteBuf> { public class PacketCompressor extends ByteToMessageCodec<ByteBuf> {
private final byte[] buffer = new byte[8192];
private final int threshold; private final int threshold;
private final Inflater inflater; private byte[] buffer = new byte[8192];
private final Deflater deflater;
private Deflater deflater = new Deflater();
private Inflater inflater = new Inflater();
public PacketCompressor(int threshold) { public PacketCompressor(int threshold) {
this.inflater = new Inflater();
this.deflater = new Deflater();
this.threshold = threshold; this.threshold = threshold;
} }
@Override @Override
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) { protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
int i = from.readableBytes(); final int packetLength = from.readableBytes();
if (i < this.threshold) { if (packetLength < this.threshold) {
Utils.writeVarIntBuf(to, 0); Utils.writeVarIntBuf(to, 0);
to.writeBytes(from); to.writeBytes(from);
} else { } else {
byte[] abyte = new byte[i]; Utils.writeVarIntBuf(to, packetLength);
from.readBytes(abyte);
Utils.writeVarIntBuf(to, abyte.length); deflater.setInput(from.nioBuffer());
this.deflater.setInput(abyte, 0, i); deflater.finish();
this.deflater.finish();
while (!this.deflater.finished()) { while (!deflater.finished()) {
int j = this.deflater.deflate(this.buffer); final int length = deflater.deflate(buffer);
to.writeBytes(buffer, 0, length);
to.writeBytes(this.buffer, 0, j);
} }
this.deflater.reset(); deflater.reset();
} }
} }
@ -85,16 +78,18 @@ public class PacketCompressor extends ByteToMessageCodec<ByteBuf> {
throw new DecoderException("Badly compressed packet - size of " + i + " is larger than protocol maximum of 2097152"); throw new DecoderException("Badly compressed packet - size of " + i + " is larger than protocol maximum of 2097152");
} }
// TODO optimize to do not initialize arrays each time
byte[] abyte = new byte[buf.readableBytes()]; byte[] abyte = new byte[buf.readableBytes()];
buf.readBytes(abyte); buf.readBytes(abyte);
this.inflater.setInput(abyte); inflater.setInput(abyte);
byte[] abyte1 = new byte[i]; byte[] abyte1 = new byte[i];
this.inflater.inflate(abyte1); inflater.inflate(abyte1);
out.add(Unpooled.wrappedBuffer(abyte1)); out.add(Unpooled.wrappedBuffer(abyte1));
this.inflater.reset(); inflater.reset();
} }
} }
} }

View File

@ -4,12 +4,25 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec; import io.netty.handler.codec.ByteToMessageCodec;
import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.CorruptedFrameException;
import net.minestom.server.MinecraftServer;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.Utils; import net.minestom.server.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
public class PacketFramer extends ByteToMessageCodec<ByteBuf> { public class PacketFramer extends ByteToMessageCodec<ByteBuf> {
public final static Logger LOGGER = LoggerFactory.getLogger(PacketFramer.class);
private final PacketProcessor packetProcessor;
public PacketFramer(PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
@Override @Override
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) { protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
final int packetSize = from.readableBytes(); final int packetSize = from.readableBytes();
@ -40,14 +53,26 @@ public class PacketFramer extends ByteToMessageCodec<ByteBuf> {
if (b >= 0) { if (b >= 0) {
buf.resetReaderIndex(); buf.resetReaderIndex();
final int j = Utils.readVarInt(buf); final int packetSize = Utils.readVarInt(buf);
if (buf.readableBytes() < j) { // Max packet size check
if (packetSize >= MinecraftServer.getMaxPacketSize()) {
final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
if (playerConnection != null) {
final String identifier = playerConnection.getIdentifier();
LOGGER.warn("An user (" + identifier + ") sent a packet over the maximum size (" + packetSize + ")");
} else {
LOGGER.warn("An unregistered user sent a packet over the maximum size (" + packetSize + ")");
}
ctx.close();
}
if (buf.readableBytes() < packetSize) {
buf.resetReaderIndex(); buf.resetReaderIndex();
return; return;
} }
out.add(buf.readRetainedSlice(j)); out.add(buf.readRetainedSlice(packetSize));
return; return;
} }
} }

View File

@ -1,13 +1,14 @@
package net.minestom.server.network.netty.packet; package net.minestom.server.network.netty.packet;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class InboundPacket { public class InboundPacket {
public final int packetId; public final int packetId;
public final ByteBuf body; public final ByteBuf body;
public InboundPacket(int id, ByteBuf body) { public InboundPacket(int id, @NotNull ByteBuf body) {
this.packetId = id; this.packetId = id;
this.body = body; this.body = body;
} }

View File

@ -17,7 +17,7 @@ public abstract class ClientPlayPacket implements ClientPacket {
* @param player the player who sent the packet * @param player the player who sent the packet
*/ */
public void process(@NotNull Player player) { public void process(@NotNull Player player) {
PACKET_LISTENER_MANAGER.process(this, player); PACKET_LISTENER_MANAGER.processClientPacket(this, player);
} }
} }

View File

@ -1,10 +1,19 @@
package net.minestom.server.network.packet.client.handler; package net.minestom.server.network.packet.client.handler;
import net.minestom.server.network.packet.client.ClientPacket; import net.minestom.server.network.packet.client.ClientPacket;
import net.minestom.server.utils.binary.BinaryReader;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* Contains registered packets and a way to instantiate them.
* <p>
* Packets are register using {@link #register(int, ClientPacketSupplier)}
* (you can override a packet id even if not recommended and not officially supported) and retrieved with {@link #getPacketInstance(int)}.
* <p>
* If you want to fill the packet from a buffer, consider using {@link ClientPacket#read(BinaryReader)} after getting the packet instance.
*/
public class ClientPacketsHandler { public class ClientPacketsHandler {
// Max packet id // Max packet id

View File

@ -3,6 +3,7 @@ package net.minestom.server.network.packet.client.handshake;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor; import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.extras.bungee.BungeeCordProxy; import net.minestom.server.extras.bungee.BungeeCordProxy;
import net.minestom.server.network.ConnectionState; import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.packet.client.ClientPreplayPacket; import net.minestom.server.network.packet.client.ClientPreplayPacket;
@ -13,6 +14,7 @@ import net.minestom.server.utils.binary.BinaryReader;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.UUID;
public class HandshakePacket implements ClientPreplayPacket { public class HandshakePacket implements ClientPreplayPacket {
@ -31,7 +33,7 @@ public class HandshakePacket implements ClientPreplayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.protocolVersion = reader.readVarInt(); this.protocolVersion = reader.readVarInt();
this.serverAddress = reader.readSizedString(); this.serverAddress = reader.readSizedString(BungeeCordProxy.isEnabled() ? Short.MAX_VALUE : 255);
this.serverPort = reader.readUnsignedShort(); this.serverPort = reader.readUnsignedShort();
this.nextState = reader.readVarInt(); this.nextState = reader.readVarInt();
} }
@ -39,6 +41,7 @@ public class HandshakePacket implements ClientPreplayPacket {
@Override @Override
public void process(@NotNull PlayerConnection connection) { public void process(@NotNull PlayerConnection connection) {
// Bungee support (IP forwarding)
if (BungeeCordProxy.isEnabled() && connection instanceof NettyPlayerConnection) { if (BungeeCordProxy.isEnabled() && connection instanceof NettyPlayerConnection) {
NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection; NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
@ -48,8 +51,25 @@ public class HandshakePacket implements ClientPreplayPacket {
if (split.length == 3 || split.length == 4) { if (split.length == 3 || split.length == 4) {
this.serverAddress = split[0]; this.serverAddress = split[0];
final SocketAddress socketAddress = new java.net.InetSocketAddress(split[1], ((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort()); final SocketAddress socketAddress = new java.net.InetSocketAddress(split[1],
((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort());
nettyPlayerConnection.setRemoteAddress(socketAddress); nettyPlayerConnection.setRemoteAddress(socketAddress);
UUID playerUuid = UUID.fromString(
split[2]
.replaceFirst(
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5"
)
);
PlayerSkin playerSkin = null;
if (split.length == 4) {
playerSkin = BungeeCordProxy.readSkin(split[3]);
}
nettyPlayerConnection.UNSAFE_setBungeeUuid(playerUuid);
nettyPlayerConnection.UNSAFE_setBungeeSkin(playerSkin);
} else { } else {
nettyPlayerConnection.sendPacket(new LoginDisconnectPacket(INVALID_BUNGEE_FORWARDING)); nettyPlayerConnection.sendPacket(new LoginDisconnectPacket(INVALID_BUNGEE_FORWARDING));
nettyPlayerConnection.disconnect(); nettyPlayerConnection.disconnect();

View File

@ -4,6 +4,7 @@ import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationUnavailableException; import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.data.type.array.ByteArrayData; import net.minestom.server.data.type.array.ByteArrayData;
import net.minestom.server.extras.MojangAuth;
import net.minestom.server.extras.mojangAuth.MojangCrypt; import net.minestom.server.extras.mojangAuth.MojangCrypt;
import net.minestom.server.network.packet.client.ClientPreplayPacket; import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.player.NettyPlayerConnection; import net.minestom.server.network.player.NettyPlayerConnection;
@ -38,25 +39,25 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
try { try {
final String loginUsername = nettyConnection.getLoginUsername(); final String loginUsername = nettyConnection.getLoginUsername();
if (!Arrays.equals(nettyConnection.getNonce(), getNonce())) { if (!Arrays.equals(nettyConnection.getNonce(), getNonce())) {
MinecraftServer.getLOGGER().error(loginUsername + " tried to login with an invalid nonce!"); MinecraftServer.LOGGER.error(loginUsername + " tried to login with an invalid nonce!");
return; return;
} }
if (!loginUsername.isEmpty()) { if (!loginUsername.isEmpty()) {
final byte[] digestedData = MojangCrypt.digestData("", MinecraftServer.getKeyPair().getPublic(), getSecretKey()); final byte[] digestedData = MojangCrypt.digestData("", MojangAuth.getKeyPair().getPublic(), getSecretKey());
if (digestedData == null) { if (digestedData == null) {
// Incorrect key, probably because of the client // Incorrect key, probably because of the client
MinecraftServer.getLOGGER().error("Connection " + nettyConnection.getRemoteAddress() + " failed initializing encryption."); MinecraftServer.LOGGER.error("Connection " + nettyConnection.getRemoteAddress() + " failed initializing encryption.");
connection.disconnect(); connection.disconnect();
return; return;
} }
final String string3 = new BigInteger(digestedData).toString(16); final String string3 = new BigInteger(digestedData).toString(16);
final GameProfile gameProfile = MinecraftServer.getSessionService().hasJoinedServer(new GameProfile(null, loginUsername), string3); final GameProfile gameProfile = MojangAuth.getSessionService().hasJoinedServer(new GameProfile(null, loginUsername), string3);
nettyConnection.setEncryptionKey(getSecretKey()); nettyConnection.setEncryptionKey(getSecretKey());
MinecraftServer.getLOGGER().info("UUID of player {} is {}", loginUsername, gameProfile.getId()); MinecraftServer.LOGGER.info("UUID of player {} is {}", loginUsername, gameProfile.getId());
CONNECTION_MANAGER.startPlayState(connection, gameProfile.getId(), gameProfile.getName()); CONNECTION_MANAGER.startPlayState(connection, gameProfile.getId(), gameProfile.getName());
} }
} catch (AuthenticationUnavailableException e) { } catch (AuthenticationUnavailableException e) {
@ -73,10 +74,11 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
} }
public SecretKey getSecretKey() { public SecretKey getSecretKey() {
return MojangCrypt.decryptByteToSecretKey(MinecraftServer.getKeyPair().getPrivate(), sharedSecret); return MojangCrypt.decryptByteToSecretKey(MojangAuth.getKeyPair().getPrivate(), sharedSecret);
} }
public byte[] getNonce() { public byte[] getNonce() {
return MinecraftServer.getKeyPair().getPrivate() == null ? this.verifyToken : MojangCrypt.decryptUsingKey(MinecraftServer.getKeyPair().getPrivate(), this.verifyToken); return MojangAuth.getKeyPair().getPrivate() == null ?
this.verifyToken : MojangCrypt.decryptUsingKey(MojangAuth.getKeyPair().getPrivate(), this.verifyToken);
} }
} }

View File

@ -3,6 +3,8 @@ package net.minestom.server.network.packet.client.login;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor; import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.extras.velocity.VelocityProxy; import net.minestom.server.extras.velocity.VelocityProxy;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.packet.client.ClientPreplayPacket; import net.minestom.server.network.packet.client.ClientPreplayPacket;
@ -37,7 +39,11 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
if (channel != null) { if (channel != null) {
boolean success = false; boolean success = false;
SocketAddress socketAddress = null; SocketAddress socketAddress = null;
UUID playerUuid = null;
String playerUsername = null;
PlayerSkin playerSkin = null;
// Velocity // Velocity
if (VelocityProxy.isEnabled() && channel.equals(VelocityProxy.PLAYER_INFO_CHANNEL)) { if (VelocityProxy.isEnabled() && channel.equals(VelocityProxy.PLAYER_INFO_CHANNEL)) {
@ -49,6 +55,12 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
final InetAddress address = VelocityProxy.readAddress(reader); final InetAddress address = VelocityProxy.readAddress(reader);
final int port = ((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort(); final int port = ((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort();
socketAddress = new InetSocketAddress(address, port); socketAddress = new InetSocketAddress(address, port);
playerUuid = reader.readUuid();
playerUsername = reader.readSizedString(16);
playerSkin = VelocityProxy.readSkin(reader);
} }
} }
} }
@ -57,12 +69,16 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
if (socketAddress != null) { if (socketAddress != null) {
nettyPlayerConnection.setRemoteAddress(socketAddress); nettyPlayerConnection.setRemoteAddress(socketAddress);
} }
if (playerUsername != null) {
nettyPlayerConnection.UNSAFE_setLoginUsername(playerUsername);
}
// Proxy usage always mean that the server is in offline mode
final String username = nettyPlayerConnection.getLoginUsername(); final String username = nettyPlayerConnection.getLoginUsername();
final UUID playerUuid = CONNECTION_MANAGER.getPlayerConnectionUuid(connection, username); final UUID uuid = playerUuid != null ?
playerUuid : CONNECTION_MANAGER.getPlayerConnectionUuid(connection, username);
CONNECTION_MANAGER.startPlayState(connection, playerUuid, username); Player player = CONNECTION_MANAGER.startPlayState(connection, uuid, username);
player.setSkin(playerSkin);
} else { } else {
LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE); LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE);
nettyPlayerConnection.sendPacket(disconnectPacket); nettyPlayerConnection.sendPacket(disconnectPacket);

View File

@ -3,7 +3,9 @@ package net.minestom.server.network.packet.client.login;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor; import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.Player;
import net.minestom.server.extras.MojangAuth; import net.minestom.server.extras.MojangAuth;
import net.minestom.server.extras.bungee.BungeeCordProxy;
import net.minestom.server.extras.velocity.VelocityProxy; import net.minestom.server.extras.velocity.VelocityProxy;
import net.minestom.server.network.ConnectionState; import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.packet.client.ClientPreplayPacket; import net.minestom.server.network.packet.client.ClientPreplayPacket;
@ -27,8 +29,10 @@ public class LoginStartPacket implements ClientPreplayPacket {
@Override @Override
public void process(@NotNull PlayerConnection connection) { public void process(@NotNull PlayerConnection connection) {
final boolean isNettyClient = connection instanceof NettyPlayerConnection;
// Cache the login username and start compression if enabled // Cache the login username and start compression if enabled
if (connection instanceof NettyPlayerConnection) { if (isNettyClient) {
NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection; NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
nettyPlayerConnection.UNSAFE_setLoginUsername(username); nettyPlayerConnection.UNSAFE_setLoginUsername(username);
@ -40,7 +44,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
} }
// Proxy support (only for netty clients) // Proxy support (only for netty clients)
if (connection instanceof NettyPlayerConnection) { if (isNettyClient) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection; final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
{ {
@ -65,7 +69,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
} }
if (MojangAuth.isUsingMojangAuth() && connection instanceof NettyPlayerConnection) { if (MojangAuth.isEnabled() && isNettyClient) {
// Mojang auth // Mojang auth
if (CONNECTION_MANAGER.getPlayer(username) != null) { if (CONNECTION_MANAGER.getPlayer(username) != null) {
connection.sendPacket(new LoginDisconnectPacket(ALREADY_CONNECTED_JSON)); connection.sendPacket(new LoginDisconnectPacket(ALREADY_CONNECTED_JSON));
@ -79,16 +83,22 @@ public class LoginStartPacket implements ClientPreplayPacket {
EncryptionRequestPacket encryptionRequestPacket = new EncryptionRequestPacket(nettyPlayerConnection); EncryptionRequestPacket encryptionRequestPacket = new EncryptionRequestPacket(nettyPlayerConnection);
nettyPlayerConnection.sendPacket(encryptionRequestPacket); nettyPlayerConnection.sendPacket(encryptionRequestPacket);
} else { } else {
final boolean bungee = BungeeCordProxy.isEnabled();
// Offline // Offline
final UUID playerUuid = CONNECTION_MANAGER.getPlayerConnectionUuid(connection, username); final UUID playerUuid = bungee && isNettyClient ?
((NettyPlayerConnection) connection).getBungeeUuid() :
CONNECTION_MANAGER.getPlayerConnectionUuid(connection, username);
CONNECTION_MANAGER.startPlayState(connection, playerUuid, username); Player player = CONNECTION_MANAGER.startPlayState(connection, playerUuid, username);
if (bungee && isNettyClient) {
player.setSkin(((NettyPlayerConnection) connection).getBungeeSkin());
}
} }
} }
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.username = reader.readSizedString(); this.username = reader.readSizedString(16);
} }
} }

View File

@ -15,7 +15,7 @@ public class ClientAdvancementTabPacket extends ClientPlayPacket {
this.action = AdvancementAction.values()[reader.readVarInt()]; this.action = AdvancementAction.values()[reader.readVarInt()];
if (action == AdvancementAction.OPENED_TAB) { if (action == AdvancementAction.OPENED_TAB) {
this.tabIdentifier = reader.readSizedString(); this.tabIdentifier = reader.readSizedString(256);
} }
} }

View File

@ -10,6 +10,6 @@ public class ClientChatMessagePacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.message = reader.readSizedString(); this.message = reader.readSizedString(256);
} }
} }

View File

@ -13,7 +13,7 @@ public class ClientCraftRecipeRequest extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.windowId = reader.readByte(); this.windowId = reader.readByte();
this.recipe = reader.readSizedString(); this.recipe = reader.readSizedString(256);
this.makeAll = reader.readBoolean(); this.makeAll = reader.readBoolean();
} }
} }

View File

@ -10,6 +10,6 @@ public class ClientNameItemPacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.itemName = reader.readSizedString(); this.itemName = reader.readSizedString(Short.MAX_VALUE);
} }
} }

View File

@ -11,7 +11,7 @@ public class ClientPluginMessagePacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.channel = reader.readSizedString(); this.channel = reader.readSizedString(256);
this.data = reader.getRemainingBytes(); this.data = reader.getRemainingBytes();
} }
} }

View File

@ -25,7 +25,7 @@ public class ClientRecipeBookData extends ClientPlayPacket {
switch (type) { switch (type) {
case 0: case 0:
this.recipeId = reader.readSizedString(); this.recipeId = reader.readSizedString(256);
break; break;
case 1: case 1:
this.craftingRecipeBookOpen = reader.readBoolean(); this.craftingRecipeBookOpen = reader.readBoolean();

View File

@ -16,7 +16,7 @@ public class ClientSettingsPacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.locale = reader.readSizedString(); this.locale = reader.readSizedString(128);
this.viewDistance = reader.readByte(); this.viewDistance = reader.readByte();
this.chatMode = Player.ChatMode.values()[reader.readVarInt()]; this.chatMode = Player.ChatMode.values()[reader.readVarInt()];
this.chatColors = reader.readBoolean(); this.chatColors = reader.readBoolean();

View File

@ -12,6 +12,6 @@ public class ClientTabCompletePacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.transactionId = reader.readVarInt(); this.transactionId = reader.readVarInt();
this.text = reader.readSizedString(); this.text = reader.readSizedString(Short.MAX_VALUE);
} }
} }

View File

@ -13,7 +13,7 @@ public class ClientUpdateCommandBlockMinecartPacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.entityId = reader.readVarInt(); this.entityId = reader.readVarInt();
this.command = reader.readSizedString(); this.command = reader.readSizedString(Short.MAX_VALUE);
this.trackOutput = reader.readBoolean(); this.trackOutput = reader.readBoolean();
} }
} }

View File

@ -15,7 +15,7 @@ public class ClientUpdateCommandBlockPacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.blockPosition = reader.readBlockPosition(); this.blockPosition = reader.readBlockPosition();
this.command = reader.readSizedString(); this.command = reader.readSizedString(Short.MAX_VALUE);
this.mode = Mode.values()[reader.readVarInt()]; this.mode = Mode.values()[reader.readVarInt()];
this.flags = reader.readByte(); this.flags = reader.readByte();
} }

View File

@ -16,10 +16,10 @@ public class ClientUpdateSignPacket extends ClientPlayPacket {
@Override @Override
public void read(@NotNull BinaryReader reader) { public void read(@NotNull BinaryReader reader) {
this.blockPosition = reader.readBlockPosition(); this.blockPosition = reader.readBlockPosition();
this.line1 = reader.readSizedString(); this.line1 = reader.readSizedString(384);
this.line2 = reader.readSizedString(); this.line2 = reader.readSizedString(384);
this.line3 = reader.readSizedString(); this.line3 = reader.readSizedString(384);
this.line4 = reader.readSizedString(); this.line4 = reader.readSizedString(384);
} }
} }

View File

@ -1,7 +1,7 @@
package net.minestom.server.network.packet.server.login; package net.minestom.server.network.packet.server.login;
import net.minestom.server.MinecraftServer;
import net.minestom.server.data.type.array.ByteArrayData; import net.minestom.server.data.type.array.ByteArrayData;
import net.minestom.server.extras.MojangAuth;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.network.player.NettyPlayerConnection; import net.minestom.server.network.player.NettyPlayerConnection;
@ -23,7 +23,7 @@ public class EncryptionRequestPacket implements ServerPacket {
@Override @Override
public void write(@NotNull BinaryWriter writer) { public void write(@NotNull BinaryWriter writer) {
writer.writeSizedString(""); writer.writeSizedString("");
final byte[] publicKey = MinecraftServer.getKeyPair().getPublic().getEncoded(); final byte[] publicKey = MojangAuth.getKeyPair().getPublic().getEncoded();
ByteArrayData.encodeByteArray(writer, publicKey); ByteArrayData.encodeByteArray(writer, publicKey);
ByteArrayData.encodeByteArray(writer, nonce); ByteArrayData.encodeByteArray(writer, nonce);
} }

View File

@ -5,9 +5,9 @@ import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.data.Data; import net.minestom.server.data.Data;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.block.BlockManager; import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.instance.palette.PaletteStorage;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BlockPosition;
@ -28,18 +28,17 @@ public class ChunkDataPacket implements ServerPacket {
public Biome[] biomes; public Biome[] biomes;
public int chunkX, chunkZ; public int chunkX, chunkZ;
public short[] blocksStateId; public PaletteStorage paletteStorage;
public short[] customBlocksId; public PaletteStorage customBlockPaletteStorage;
public Set<Integer> blockEntities; public Set<Integer> blockEntities;
public Int2ObjectMap<Data> blocksData; public Int2ObjectMap<Data> blocksData;
//public Chunk chunk;
public int[] sections; public int[] sections;
private static final byte CHUNK_SECTION_COUNT = 16; private static final byte CHUNK_SECTION_COUNT = 16;
private static final int BITS_PER_ENTRY = 15; private static final int MAX_BITS_PER_ENTRY = 16;
private static final int MAX_BUFFER_SIZE = (Short.BYTES + Byte.BYTES + 5 * Byte.BYTES + (4096 * BITS_PER_ENTRY / Long.SIZE * Long.BYTES)) * CHUNK_SECTION_COUNT + 256 * Integer.BYTES; private static final int MAX_BUFFER_SIZE = (Short.BYTES + Byte.BYTES + 5 * Byte.BYTES + (4096 * MAX_BITS_PER_ENTRY / Long.SIZE * Long.BYTES)) * CHUNK_SECTION_COUNT + 256 * Integer.BYTES;
@Override @Override
public void write(@NotNull BinaryWriter writer) { public void write(@NotNull BinaryWriter writer) {
@ -51,10 +50,10 @@ public class ChunkDataPacket implements ServerPacket {
ByteBuf blocks = Unpooled.buffer(MAX_BUFFER_SIZE); ByteBuf blocks = Unpooled.buffer(MAX_BUFFER_SIZE);
for (byte i = 0; i < CHUNK_SECTION_COUNT; i++) { for (byte i = 0; i < CHUNK_SECTION_COUNT; i++) {
if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) { if (fullChunk || (sections.length == CHUNK_SECTION_COUNT && sections[i] != 0)) {
short[] section = getSection(i); final long[] section = paletteStorage.getSectionBlocks()[i];
if (section != null) { // section contains at least one block if (section.length > 0) { // section contains at least one block
mask |= 1 << i; mask |= 1 << i;
Utils.writeBlocks(blocks, section, BITS_PER_ENTRY); Utils.writeBlocks(blocks, paletteStorage.getPalette(i), section, paletteStorage.getBitsPerEntry());
} else { } else {
mask |= 0; mask |= 0;
} }
@ -106,36 +105,18 @@ public class ChunkDataPacket implements ServerPacket {
.setInt("y", blockPosition.getY()) .setInt("y", blockPosition.getY())
.setInt("z", blockPosition.getZ()); .setInt("z", blockPosition.getZ());
final short customBlockId = customBlocksId[index]; if (customBlockPaletteStorage != null) {
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId); final short customBlockId = customBlockPaletteStorage.getBlockAt(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ());
if (customBlock != null) { final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
final Data data = blocksData.get(index); if (customBlock != null) {
customBlock.writeBlockEntity(blockPosition, data, nbt); final Data data = blocksData.get(index);
customBlock.writeBlockEntity(blockPosition, data, nbt);
}
} }
writer.writeNBT("", nbt); writer.writeNBT("", nbt);
} }
} }
private short[] getSection(byte section) {
short[] blocks = new short[Chunk.CHUNK_SIZE_X * Chunk.CHUNK_SECTION_SIZE * Chunk.CHUNK_SIZE_Z];
boolean empty = true;
for (byte y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
for (byte x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (byte z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
final int yPos = (y + Chunk.CHUNK_SECTION_SIZE * section);
final int index = ChunkUtils.getBlockIndex(x, yPos, z);
final short blockStateId = blocksStateId[index];
if (blockStateId != 0)
empty = false;
final int packetIndex = (((y * 16) + x) * 16) + z;
blocks[packetIndex] = blockStateId;
}
}
}
return empty ? null : blocks;
}
@Override @Override
public int getId() { public int getId() {
return ServerPacketIdentifier.CHUNK_DATA; return ServerPacketIdentifier.CHUNK_DATA;

View File

@ -25,7 +25,7 @@ public class JoinGamePacket implements ServerPacket {
@Override @Override
public void write(@NotNull BinaryWriter writer) { public void write(@NotNull BinaryWriter writer) {
writer.writeInt(entityId); writer.writeInt(entityId);
writer.writeBoolean(MinecraftServer.isHardcoreLook()); writer.writeBoolean(gameMode.isHardcore());
writer.writeByte(gameMode.getId()); writer.writeByte(gameMode.getId());
//Previous Gamemode //Previous Gamemode
writer.writeByte(gameMode.getId()); writer.writeByte(gameMode.getId());

View File

@ -34,8 +34,9 @@ public class PluginMessagePacket implements ServerPacket {
PluginMessagePacket brandMessage = new PluginMessagePacket(); PluginMessagePacket brandMessage = new PluginMessagePacket();
brandMessage.channel = "minecraft:brand"; brandMessage.channel = "minecraft:brand";
BinaryWriter writer = new BinaryWriter(); final String brandName = MinecraftServer.getBrandName();
writer.writeSizedString(MinecraftServer.getBrandName()); BinaryWriter writer = new BinaryWriter(4 + brandName.length());
writer.writeSizedString(brandName);
brandMessage.data = writer.toByteArray(); brandMessage.data = writer.toByteArray();

View File

@ -1,5 +1,6 @@
package net.minestom.server.network.packet.server.play; package net.minestom.server.network.packet.server.play;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.BinaryWriter;
@ -22,7 +23,7 @@ public class TeamsPacket implements ServerPacket {
/** /**
* The display name for the team * The display name for the team
*/ */
public String teamDisplayName; public JsonMessage teamDisplayName;
/** /**
* The friendly flags to * The friendly flags to
*/ */
@ -42,11 +43,11 @@ public class TeamsPacket implements ServerPacket {
/** /**
* The prefix of the team * The prefix of the team
*/ */
public String teamPrefix; public JsonMessage teamPrefix;
/** /**
* The suffix of the team * The suffix of the team
*/ */
public String teamSuffix; public JsonMessage teamSuffix;
/** /**
* An array with all entities in the team * An array with all entities in the team
*/ */
@ -65,13 +66,13 @@ public class TeamsPacket implements ServerPacket {
switch (action) { switch (action) {
case CREATE_TEAM: case CREATE_TEAM:
case UPDATE_TEAM_INFO: case UPDATE_TEAM_INFO:
writer.writeSizedString(this.teamDisplayName); writer.writeSizedString(this.teamDisplayName.toString());
writer.writeByte(this.friendlyFlags); writer.writeByte(this.friendlyFlags);
writer.writeSizedString(this.nameTagVisibility.getIdentifier()); writer.writeSizedString(this.nameTagVisibility.getIdentifier());
writer.writeSizedString(this.collisionRule.getIdentifier()); writer.writeSizedString(this.collisionRule.getIdentifier());
writer.writeVarInt(this.teamColor); writer.writeVarInt(this.teamColor);
writer.writeSizedString(this.teamPrefix); writer.writeSizedString(this.teamPrefix.toString());
writer.writeSizedString(this.teamSuffix); writer.writeSizedString(this.teamSuffix.toString());
break; break;
case REMOVE_TEAM: case REMOVE_TEAM:

View File

@ -1,6 +1,5 @@
package net.minestom.server.network.player; package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.entity.fakeplayer.FakePlayer; import net.minestom.server.entity.fakeplayer.FakePlayer;
@ -13,24 +12,11 @@ import java.net.SocketAddress;
public class FakePlayerConnection extends PlayerConnection { public class FakePlayerConnection extends PlayerConnection {
@Override
public void sendPacket(@NotNull ByteBuf buffer, boolean copy) {
throw new UnsupportedOperationException("FakePlayer cannot read Bytebuf");
}
@Override
public void writePacket(@NotNull ByteBuf buffer, boolean copy) {
throw new UnsupportedOperationException("FakePlayer cannot write to Bytebuf");
}
@Override @Override
public void sendPacket(@NotNull ServerPacket serverPacket) { public void sendPacket(@NotNull ServerPacket serverPacket) {
getFakePlayer().getController().consumePacket(serverPacket); if (shouldSendPacket(serverPacket)) {
} getFakePlayer().getController().consumePacket(serverPacket);
}
@Override
public void flush() {
// Does nothing
} }
@NotNull @NotNull

View File

@ -1,10 +1,8 @@
package net.minestom.server.network.player; package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import lombok.Getter; import net.minestom.server.entity.PlayerSkin;
import lombok.Setter;
import net.minestom.server.extras.mojangAuth.Decrypter; import net.minestom.server.extras.mojangAuth.Decrypter;
import net.minestom.server.extras.mojangAuth.Encrypter; import net.minestom.server.extras.mojangAuth.Encrypter;
import net.minestom.server.extras.mojangAuth.MojangCrypt; import net.minestom.server.extras.mojangAuth.MojangCrypt;
@ -19,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
@ -31,16 +30,14 @@ public class NettyPlayerConnection extends PlayerConnection {
private final SocketChannel channel; private final SocketChannel channel;
private SocketAddress remoteAddress; private SocketAddress remoteAddress;
@Getter
private boolean encrypted = false; private boolean encrypted = false;
@Getter
private boolean compressed = false; private boolean compressed = false;
//Could be null. Only used for Mojang Auth //Could be null. Only used for Mojang Auth
@Getter
@Setter
private byte[] nonce = new byte[4]; private byte[] nonce = new byte[4];
// Data from client packets
private String loginUsername; private String loginUsername;
private String serverAddress; private String serverAddress;
private int serverPort; private int serverPort;
@ -49,6 +46,10 @@ public class NettyPlayerConnection extends PlayerConnection {
// cleared once the player enters the play state // cleared once the player enters the play state
private final Map<Integer, String> pluginRequestMap = new ConcurrentHashMap<>(); private final Map<Integer, String> pluginRequestMap = new ConcurrentHashMap<>();
// Bungee
private UUID bungeeUuid;
private PlayerSkin bungeeSkin;
public NettyPlayerConnection(@NotNull SocketChannel channel) { public NettyPlayerConnection(@NotNull SocketChannel channel) {
super(); super();
this.channel = channel; this.channel = channel;
@ -81,38 +82,22 @@ public class NettyPlayerConnection extends PlayerConnection {
channel.pipeline().addAfter("framer", "compressor", new PacketCompressor(threshold)); channel.pipeline().addAfter("framer", "compressor", new PacketCompressor(threshold));
} }
@Override /**
public void sendPacket(@NotNull ByteBuf buffer, boolean copy) { * Writes a packet to the connection channel.
if (copy) { * <p>
buffer = buffer.copy(); * All packets are flushed during {@link net.minestom.server.entity.Player#update(long)}.
buffer.retain(); *
channel.writeAndFlush(buffer); * @param serverPacket the packet to write
buffer.release(); */
} else {
channel.writeAndFlush(buffer);
}
}
@Override
public void writePacket(@NotNull ByteBuf buffer, boolean copy) {
if (copy) {
buffer = buffer.copy();
buffer.retain();
channel.write(buffer);
buffer.release();
} else {
channel.write(buffer);
}
}
@Override @Override
public void sendPacket(@NotNull ServerPacket serverPacket) { public void sendPacket(@NotNull ServerPacket serverPacket) {
channel.writeAndFlush(serverPacket); if (shouldSendPacket(serverPacket)) {
} if (getPlayer() != null) {
channel.write(serverPacket); // Flush on player update
@Override } else {
public void flush() { channel.writeAndFlush(serverPacket);
getChannel().flush(); }
}
} }
@NotNull @NotNull
@ -186,6 +171,24 @@ public class NettyPlayerConnection extends PlayerConnection {
return serverPort; return serverPort;
} }
@Nullable
public UUID getBungeeUuid() {
return bungeeUuid;
}
public void UNSAFE_setBungeeUuid(UUID bungeeUuid) {
this.bungeeUuid = bungeeUuid;
}
@Nullable
public PlayerSkin getBungeeSkin() {
return bungeeSkin;
}
public void UNSAFE_setBungeeSkin(PlayerSkin bungeeSkin) {
this.bungeeSkin = bungeeSkin;
}
/** /**
* Adds an entry to the plugin request map. * Adds an entry to the plugin request map.
* <p> * <p>
@ -235,4 +238,12 @@ public class NettyPlayerConnection extends PlayerConnection {
this.serverAddress = serverAddress; this.serverAddress = serverAddress;
this.serverPort = serverPort; this.serverPort = serverPort;
} }
public byte[] getNonce() {
return nonce;
}
public void setNonce(byte[] nonce) {
this.nonce = nonce;
}
} }

View File

@ -1,16 +1,18 @@
package net.minestom.server.network.player; package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor; import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText; import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.listener.manager.PacketConsumer;
import net.minestom.server.listener.manager.PacketListenerManager;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState; import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.packet.server.play.DisconnectPacket; import net.minestom.server.network.packet.server.play.DisconnectPacket;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -21,6 +23,8 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
public abstract class PlayerConnection { public abstract class PlayerConnection {
protected static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
private Player player; private Player player;
private ConnectionState connectionState; private ConnectionState connectionState;
private boolean online; private boolean online;
@ -29,7 +33,6 @@ public abstract class PlayerConnection {
private static final ColoredText rateLimitKickMessage = ColoredText.of(ChatColor.RED + "Too Many Packets"); private static final ColoredText rateLimitKickMessage = ColoredText.of(ChatColor.RED + "Too Many Packets");
//Connection Stats //Connection Stats
@Getter
private final AtomicInteger packetCounter = new AtomicInteger(0); private final AtomicInteger packetCounter = new AtomicInteger(0);
private final AtomicInteger lastPacketCounter = new AtomicInteger(0); private final AtomicInteger lastPacketCounter = new AtomicInteger(0);
private short tickCounter = 0; private short tickCounter = 0;
@ -68,33 +71,37 @@ public abstract class PlayerConnection {
} }
} }
/** public AtomicInteger getPacketCounter() {
* Sends a raw {@link ByteBuf} to the client. return packetCounter;
* }
* @param buffer The buffer to send.
* @param copy Should be true unless your only using the ByteBuf once.
*/
public abstract void sendPacket(@NotNull ByteBuf buffer, boolean copy);
/** /**
* Writes a raw {@link ByteBuf} to the client. * Returns a printable identifier for this connection, will be the player username
* or the connection remote address.
* *
* @param buffer The buffer to send. * @return this connection identifier
* @param copy Should be true unless your only using the ByteBuf once.
*/ */
public abstract void writePacket(@NotNull ByteBuf buffer, boolean copy); @NotNull
public String getIdentifier() {
final Player player = getPlayer();
return player != null ?
player.getUsername() :
getRemoteAddress().toString();
}
/** /**
* Serializes the packet and send it to the client. * Serializes the packet and send it to the client.
* <p>
* Also responsible for executing {@link ConnectionManager#onPacketSend(PacketConsumer)} consumers.
* *
* @param serverPacket the packet to send * @param serverPacket the packet to send
* @see #shouldSendPacket(ServerPacket)
*/ */
public abstract void sendPacket(@NotNull ServerPacket serverPacket); public abstract void sendPacket(@NotNull ServerPacket serverPacket);
/** protected boolean shouldSendPacket(@NotNull ServerPacket serverPacket) {
* Flush all waiting packets. return player == null || PACKET_LISTENER_MANAGER.processServerPacket(serverPacket, player);
*/ }
public abstract void flush();
/** /**
* Gets the remote address of the client. * Gets the remote address of the client.
@ -112,8 +119,9 @@ public abstract class PlayerConnection {
/** /**
* Gets the player linked to this connection. * Gets the player linked to this connection.
* *
* @return the player * @return the player, can be null if not initialized yet
*/ */
@Nullable
public Player getPlayer() { public Player getPlayer() {
return player; return player;
} }
@ -164,4 +172,12 @@ public abstract class PlayerConnection {
public int getLastPacketCounter() { public int getLastPacketCounter() {
return lastPacketCounter.get(); return lastPacketCounter.get();
} }
@Override
public String toString() {
return "PlayerConnection{" +
"connectionState=" + connectionState +
", identifier=" + getIdentifier() +
'}';
}
} }

Some files were not shown because too many files have changed in this diff Show More