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
* 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
* Longer to develop something playable
* 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'
}
group 'net.minestom.server'
version '1.0'
sourceCompatibility = 1.11
project.ext.lwjglVersion = "3.2.3"
switch (OperatingSystem.current()) {
@ -37,17 +41,12 @@ allprojects {
}
javadoc {
options {
destinationDir(file("docs"))
addBooleanOption('html5', true)
addBooleanOption('-no-module-directories', true)
}
}
}
group 'net.minestom.server'
version '1.0'
sourceCompatibility = 1.11
sourceSets {
main {
java {
@ -101,12 +100,13 @@ dependencies {
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.6.2')
// Netty
api 'io.netty:netty-handler:4.1.52.Final'
api 'io.netty:netty-codec:4.1.52.Final'
implementation 'io.netty:netty-transport-native-epoll:4.1.52.Final:linux-x86_64'
api 'io.netty:netty-handler:4.1.54.Final'
api 'io.netty:netty-codec:4.1.54.Final'
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
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
api 'com.google.code.gson:gson:2.8.6'
@ -116,25 +116,21 @@ dependencies {
api 'com.github.Articdive:Jnoise:1.0.0'
// https://mvnrepository.com/artifact/org.rocksdb/rocksdbjni
api 'org.rocksdb:rocksdbjni:6.11.4'
api 'org.rocksdb:rocksdbjni:6.13.3'
// 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.
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 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'
// Code modification
api "org.ow2.asm:asm:${asmVersion}"
api "org.ow2.asm:asm-tree:${asmVersion}"
api "org.ow2.asm:asm-analysis:${asmVersion}"
api "org.ow2.asm:asm-util:${asmVersion}"
api "org.ow2.asm:asm-commons:${asmVersion}"
implementation 'com.google.guava:guava:21.0'
api "org.spongepowered:mixin:${mixinVersion}"
// Path finding

View File

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

View File

@ -1,23 +1,21 @@
package net.minestom.codegen;
import lombok.Getter;
import java.io.File;
public class PrismarinePaths {
@Getter private String blocks;
@Getter private String biomes;
@Getter private String effects;
@Getter private String items;
@Getter private String recipes;
@Getter private String instruments;
@Getter private String materials;
@Getter private String entities;
@Getter private String protocol;
@Getter private String windows;
@Getter private String version;
@Getter private String language;
private String blocks;
private String biomes;
private String effects;
private String items;
private String recipes;
private String instruments;
private String materials;
private String entities;
private String protocol;
private String windows;
private String version;
private String language;
public File getBlockFile() {
return getFile(blocks, "blocks");
@ -32,6 +30,6 @@ public class PrismarinePaths {
}
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;
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.benchmark.BenchmarkManager;
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.extensions.Extension;
import net.minestom.server.extensions.ExtensionManager;
import net.minestom.server.extras.mojangAuth.MojangCrypt;
import net.minestom.server.fluids.Fluid;
import net.minestom.server.gamedata.loottables.LootTableManager;
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.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.netty.NettyServer;
import net.minestom.server.network.packet.server.play.PluginMessagePacket;
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.timer.SchedulerManager;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.Difficulty;
@ -60,8 +54,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Proxy;
import java.security.KeyPair;
import java.util.Collection;
/**
@ -72,8 +64,7 @@ import java.util.Collection;
*/
public final class MinecraftServer {
@Getter
private final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
public static final String VERSION_NAME = "1.16.4";
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_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 int THREAD_COUNT_BLOCK_BATCH = 2;
@ -103,21 +91,12 @@ public final class MinecraftServer {
private static final int MS_TO_SEC = 1000;
public static final int TICK_MS = MS_TO_SEC / TICK_PER_SECOND;
@Getter
@Setter
private static boolean hardcoreLook = false;
//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;
// Network monitoring
private static int rateLimit = 300;
private static int maxPacketSize = 30_000;
// Network
private static PacketListenerManager packetListenerManager;
private static PacketProcessor packetProcessor;
private static NettyServer nettyServer;
// In-Game Manager
@ -154,14 +133,6 @@ public final class MinecraftServer {
private static LootTableManager lootTableManager;
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() {
if (minecraftServer != null) // don't init twice
return minecraftServer;
@ -185,7 +156,7 @@ public final class MinecraftServer {
connectionManager = new ConnectionManager();
// Networking
final PacketProcessor packetProcessor = new PacketProcessor();
packetProcessor = new PacketProcessor();
packetListenerManager = new PacketListenerManager();
instanceManager = new InstanceManager();
@ -239,19 +210,17 @@ public final class MinecraftServer {
* @param brandName the server brand name
* @throws NullPointerException if {@code brandName} is null
*/
@NotNull
public static void setBrandName(String brandName) {
public static void setBrandName(@NotNull String brandName) {
Check.notNull(brandName, "The brand name cannot be null");
MinecraftServer.brandName = brandName;
PluginMessagePacket brandMessage = PluginMessagePacket.getBrandPacket();
PacketWriterUtils.writeAndSend(connectionManager.getOnlinePlayers(), brandMessage);
PacketUtils.sendGroupedPacket(connectionManager.getOnlinePlayers(), PluginMessagePacket.getBrandPacket());
}
/**
* 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() {
return rateLimit;
@ -266,6 +235,24 @@ public final class MinecraftServer {
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.
*
@ -281,17 +268,15 @@ public final class MinecraftServer {
*
* @param difficulty the new server difficulty
*/
@NotNull
public static void setDifficulty(@NotNull Difficulty difficulty) {
Check.notNull(difficulty, "The server difficulty cannot be null.");
MinecraftServer.difficulty = difficulty;
// The difficulty packet
// Send the packet to all online players
ServerDifficultyPacket serverDifficultyPacket = new ServerDifficultyPacket();
serverDifficultyPacket.difficulty = difficulty;
serverDifficultyPacket.locked = true; // Can only be modified on single-player
// Send the packet to all online players
PacketWriterUtils.writeAndSend(connectionManager.getOnlinePlayers(), serverDifficultyPacket);
PacketUtils.sendGroupedPacket(connectionManager.getOnlinePlayers(), serverDifficultyPacket);
}
/**
@ -424,6 +409,18 @@ public final class MinecraftServer {
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.
*
@ -453,14 +450,16 @@ public final class MinecraftServer {
"The chunk view distance must be between 2 and 32");
MinecraftServer.chunkViewDistance = chunkViewDistance;
if (started) {
UpdateViewDistancePacket updateViewDistancePacket = new UpdateViewDistancePacket();
updateViewDistancePacket.viewDistance = chunkViewDistance;
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();
if (playerChunk != null) {
player.refreshVisibleChunks(playerChunk);
@ -625,11 +624,11 @@ public final class MinecraftServer {
extensionManager.getExtensions().forEach(Extension::initialize);
extensionManager.getExtensions().forEach(Extension::postInitialize);
MinecraftServer.started = true;
final double loadTime = MathUtils.round((t1 + System.nanoTime()) / 1_000_000D, 2);
LOGGER.info("Extensions loaded in " + loadTime + "ms");
LOGGER.info("Minestom server started successfully.");
MinecraftServer.started = true;
}
/**

View File

@ -1,23 +1,19 @@
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.Player;
import net.minestom.server.instance.Instance;
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.ThreadProvider;
import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.DoubleConsumer;
import java.util.function.LongConsumer;
/**
* Manager responsible for the server ticks.
@ -27,37 +23,30 @@ import java.util.function.DoubleConsumer;
*/
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 boolean stopRequested;
private ThreadProvider threadProvider;
private final ConcurrentLinkedQueue<Runnable> tickStartCallbacks = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<DoubleConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
{
// DEFAULT THREAD PROVIDER
//threadProvider = new PerInstanceThreadProvider();
threadProvider = new PerGroupChunkProvider();
}
/**
* Should only be created in MinecraftServer
* Should only be created in MinecraftServer.
*/
protected UpdateManager() {
}
/**
* Starts the server loop in the update thread
* Starts the server loop in the update thread.
*/
protected void start() {
mainUpdate.execute(() -> {
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
final EntityManager entityManager = MinecraftServer.getEntityManager();
final long tickDistance = MinecraftServer.TICK_MS * 1000000;
@ -67,56 +56,25 @@ public final class UpdateManager {
final long tickStart = System.currentTimeMillis();
// Tick start callbacks
if (!tickStartCallbacks.isEmpty()) {
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);
}
doTickCallback(tickStartCallbacks, tickStart);
// Waiting players update (newly connected clients waiting to get into the server)
entityManager.updateWaitingPlayers();
// Keep Alive Handling
final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(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);
}
}
entityManager.handleKeepAlive(tickStart);
for (final Future<?> future : futures) {
try {
future.get();
} catch (Throwable e) {
e.printStackTrace();
}
}
// Server tick (chunks/entities)
serverTick(tickStart);
// the time that the tick took in nanoseconds
final long tickTime = System.nanoTime() - currentTime;
// Tick end callbacks
if (!tickEndCallbacks.isEmpty()) {
final double tickEnd = (System.nanoTime() - currentTime) / 1000000D;
DoubleConsumer callback;
while ((callback = tickEndCallbacks.poll()) != null) {
callback.accept(tickEnd);
}
}
doTickCallback(tickEndCallbacks, tickTime / 1000000L);
// 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 {
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}.
*
@ -206,10 +202,12 @@ public final class UpdateManager {
/**
* 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
*/
public void addTickStartCallback(Runnable callback) {
public void addTickStartCallback(@NotNull LongConsumer callback) {
this.tickStartCallbacks.add(callback);
}
@ -218,18 +216,18 @@ public final class UpdateManager {
*
* @param callback the callback to remove
*/
public void removeTickStartCallback(Runnable callback) {
public void removeTickStartCallback(@NotNull LongConsumer callback) {
this.tickStartCallbacks.remove(callback);
}
/**
* Adds a callback executed at the end of the next server tick.
* <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
*/
public void addTickEndCallback(DoubleConsumer callback) {
public void addTickEndCallback(@NotNull LongConsumer callback) {
this.tickEndCallbacks.add(callback);
}
@ -238,12 +236,12 @@ public final class UpdateManager {
*
* @param callback the callback to remove
*/
public void removeTickEndCallback(DoubleConsumer callback) {
public void removeTickEndCallback(@NotNull LongConsumer callback) {
this.tickEndCallbacks.remove(callback);
}
/**
* Stops the server loop
* Stops the server loop.
*/
public void stop() {
stopRequested = true;

View File

@ -1,11 +1,10 @@
package net.minestom.server;
import net.minestom.server.entity.Player;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set;
/**
@ -56,7 +55,7 @@ public interface Viewable {
* @param packet the packet to send to all viewers
*/
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) {
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.
* <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
*/
default void sendPacketToViewersAndSelf(@NotNull ServerPacket packet) {
if (this instanceof Player) {
if (getViewers().isEmpty()) {
PacketWriterUtils.writeAndSend((Player) this, packet);
} else {
UNSAFE_sendPacketToViewersAndSelf(packet);
}
} else {
sendPacketToViewers(packet);
((Player) this).getPlayerConnection().sendPacket(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);
sendPacketToViewers(packet);
}
}

View File

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

View File

@ -1,11 +1,9 @@
package net.minestom.server.advancements;
import io.netty.buffer.ByteBuf;
import net.minestom.server.Viewable;
import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.play.AdvancementsPacket;
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.validate.Check;
import org.jetbrains.annotations.NotNull;
@ -14,7 +12,7 @@ import org.jetbrains.annotations.Nullable;
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>
* 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)}.
@ -33,19 +31,16 @@ public class AdvancementTab implements Viewable {
// Advancement -> its parent
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)
// 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) {
this.root = root;
cacheAdvancement(rootIdentifier, root, null);
final AdvancementsPacket removePacket = AdvancementUtils.getRemovePacket(new String[]{rootIdentifier});
this.removeBuffer = PacketUtils.writePacket(removePacket);
this.removePacket = AdvancementUtils.getRemovePacket(new String[]{rootIdentifier});
}
/**
@ -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.
*
@ -135,8 +123,6 @@ public class AdvancementTab implements Viewable {
advancement.setParent(parent);
advancement.updateCriteria();
this.advancementMap.put(advancement, parent);
updatePacket();
}
@Override
@ -149,7 +135,7 @@ public class AdvancementTab implements Viewable {
final PlayerConnection playerConnection = player.getPlayerConnection();
// Send the tab to the player
playerConnection.sendPacket(createBuffer, true);
playerConnection.sendPacket(createPacket());
addPlayer(player);
@ -166,7 +152,7 @@ public class AdvancementTab implements Viewable {
// Remove the tab
if (!player.isRemoved()) {
playerConnection.sendPacket(removeBuffer, true);
playerConnection.sendPacket(removePacket);
}
removePlayer(player);

View File

@ -38,7 +38,6 @@ public final class BenchmarkManager {
THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
THREADS.add(THREAD_NAME_MAIN_UPDATE);
THREADS.add(THREAD_NAME_PACKET_WRITER);
THREADS.add(THREAD_NAME_BLOCK_BATCH);
THREADS.add(THREAD_NAME_SCHEDULER);
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
*/

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

View File

@ -83,8 +83,15 @@ public final class CommandManager {
* Registers a {@link Command}.
*
* @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);
}
@ -103,13 +110,20 @@ public final class CommandManager {
* Registers a {@link CommandProcessor}.
*
* @param commandProcessor the command to register
* @throws IllegalStateException if a command with the same name already exists
*/
public void register(@NotNull CommandProcessor commandProcessor) {
this.commandProcessorMap.put(commandProcessor.getCommandName().toLowerCase(), commandProcessor);
public synchronized void register(@NotNull CommandProcessor 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
final String[] aliases = commandProcessor.getAliases();
if (aliases != null && aliases.length > 0) {
for (String alias : aliases) {
Check.stateCondition(commandExists(alias),
"A command with the name " + alias + " is already registered!");
this.commandProcessorMap.put(alias.toLowerCase(), commandProcessor);
}
}
@ -126,6 +140,18 @@ public final class CommandManager {
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}.
*

View File

@ -12,6 +12,8 @@ import org.jetbrains.annotations.Nullable;
* <p>
* 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.
* <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 {

View File

@ -1,18 +1,15 @@
package net.minestom.server.command;
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.Nullable;
import java.util.Collection;
/**
* Represents something which can send commands to the server.
* <p>
* Main implementations are {@link Player} and {@link ConsoleSender}.
*/
public interface CommandSender {
public interface CommandSender extends PermissionHandler {
/**
* 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}.
*
@ -133,6 +51,7 @@ public interface CommandSender {
* Casts this object to a {@link Player}.
* No checks are performed, {@link ClassCastException} can very much happen.
*
* @throws ClassCastException if 'this' is not a player
* @see #isPlayer()
*/
default Player asPlayer() {
@ -143,6 +62,7 @@ public interface CommandSender {
* Casts this object to a {@link ConsoleSender}.
* No checks are performed, {@link ClassCastException} can very much happen.
*
* @throws ClassCastException if 'this' is not a console sender
* @see #isConsole()
*/
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
* @see #Command(String, String...)
@ -70,8 +70,10 @@ public class Command {
/**
* Gets the {@link CommandCondition}.
* <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.
* <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
*/
@ -84,6 +86,7 @@ public class Command {
* Sets the {@link CommandCondition}.
*
* @param commandCondition the new command condition, null to do not call anything
* @see #getCondition()
*/
public void setCondition(@Nullable CommandCondition commandCondition) {
this.condition = commandCondition;
@ -104,7 +107,7 @@ public class Command {
/**
* Adds a new syntax in the command.
* <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 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 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) {
this.commandMap.put(command.getName().toLowerCase(), command);

View File

@ -12,11 +12,11 @@ import org.jetbrains.annotations.NotNull;
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,
* 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 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 static final String RELATIVE_CHAR = "~";

View File

@ -26,13 +26,23 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
// Check if each element is correct
for (String element : split) {
if (!element.equals(RELATIVE_CHAR)) {
if (!element.startsWith(RELATIVE_CHAR)) {
try {
// Will throw the exception if not an integer
Integer.parseInt(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 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++) {
final String element = split[i];
if (element.equals(RELATIVE_CHAR)) {
if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) {
relativeX = true;
} else if (i == 1) {
@ -59,6 +70,19 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
} else if (i == 2) {
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 {
final int number = Integer.parseInt(element);
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>
* Example: -1.2 ~
*/
public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> {
public class ArgumentRelativeVec2 extends ArgumentRelativeVec {
public ArgumentRelativeVec2(@NotNull String id) {
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
@Override
public RelativeVec parse(@NotNull String value) {
@ -50,12 +26,23 @@ public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> {
for (int i = 0; i < split.length; i++) {
final String element = split[i];
if (element.equals(RELATIVE_CHAR)) {
if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) {
relativeX = true;
} else if (i == 1) {
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 {
final float number = Float.parseFloat(element);
if (i == 0) {
@ -69,8 +56,4 @@ public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> {
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>
* Example: -1.2 ~ 5
*/
public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
public class ArgumentRelativeVec3 extends ArgumentRelativeVec {
public ArgumentRelativeVec3(@NotNull String id) {
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
@Override
public RelativeVec parse(@NotNull String value) {
@ -51,7 +27,8 @@ public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
for (int i = 0; i < split.length; i++) {
final String element = split[i];
if (element.equals(RELATIVE_CHAR)) {
if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) {
relativeX = true;
} else if (i == 1) {
@ -59,6 +36,19 @@ public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
} else if (i == 2) {
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 {
final float number = Float.parseFloat(element);
if (i == 0) {
@ -73,9 +63,4 @@ public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
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 java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -66,4 +67,17 @@ public class DataImpl implements 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 {
// 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_";
@NotNull
@ -45,7 +45,8 @@ public class NbtDataImpl extends DataImpl {
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");
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();
@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.
@ -109,7 +109,7 @@ public interface SerializableData extends Data {
{
final int dataIndexSize = binaryReader.readVarInt();
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();
typeToIndexMap.put(className, classIndex);
}

View File

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

View File

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

View File

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

View File

@ -20,6 +20,8 @@ import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.WorldBorder;
import net.minestom.server.instance.block.CustomBlock;
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.utils.BlockPosition;
import net.minestom.server.utils.Position;
@ -41,7 +43,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
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 AtomicInteger lastEntityId = new AtomicInteger();
@ -82,8 +89,9 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
private boolean autoViewable;
private final int id;
private Data data;
protected final Set<Player> viewers = new CopyOnWriteArraySet<>();
private Data data;
private final List<Permission> permissions = new LinkedList<>();
protected UUID uuid;
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);
}
public Entity(@NotNull EntityType entityType) {
this(entityType, new Position());
}
/**
* 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}).
@ -149,12 +161,12 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
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
*/
@Nullable
@ -169,7 +181,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
/**
* 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);
@ -341,6 +353,12 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
this.data = data;
}
@NotNull
@Override
public Collection<Permission> getAllPermissions() {
return permissions;
}
/**
* Updates the entity, called every tick.
* <p>
@ -365,8 +383,9 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
return;
}
BlockPosition blockPosition = position.toBlockPosition();
if (!ChunkUtils.isLoaded(instance, position.getX(), position.getZ()) || !ChunkUtils.isLoaded(instance, blockPosition.getX(), blockPosition.getZ())) {
final Chunk currentChunk = getChunk(); // current entity chunk
if (!ChunkUtils.isLoaded(currentChunk)) {
// No update for entities in unloaded chunk
return;
}
@ -454,8 +473,8 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
float drag;
if (onGround) {
final CustomBlock customBlock =
instance.getCustomBlock(blockPosition);
final BlockPosition blockPosition = position.toBlockPosition();
final CustomBlock customBlock = instance.getCustomBlock(blockPosition);
if (customBlock != null) {
// Custom drag
drag = customBlock.getDrag(instance, blockPosition);
@ -484,6 +503,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer {
}
// handle block contacts
// TODO do not call every tick (it is pretty expensive)
final int minX = (int) Math.floor(boundingBox.getMinX());
final int maxX = (int) Math.ceil(boundingBox.getMaxX());
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.
* <p>
* All entities can be retrieved by calling {@link Entity#getEntity(int)}.
* Each entity has an unique id (server-wide) which will change after a restart.
*
* @return the unique entity id
* @see Entity#getEntity(int) to retrive an entity based on its id
*/
public int getEntityId() {
return id;

View File

@ -1,10 +1,15 @@
package net.minestom.server.entity;
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.PlayerPreLoginEvent;
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 org.jetbrains.annotations.NotNull;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -12,10 +17,16 @@ import java.util.function.Consumer;
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<>();
/**
* Connect waiting players
* Connects waiting players.
*/
public void updateWaitingPlayers() {
// 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() {
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.
* <p>
* Can be considered as a pre-init thing.
*
* @param player the {@link Player} to add
*/
public void addWaitingPlayer(Player player) {
public void addWaitingPlayer(@NotNull Player player) {
// Init player (register events)
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.utils.Position;
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.UpdateOption;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -29,7 +31,11 @@ import java.util.function.Consumer;
public abstract class LivingEntity extends Entity implements EquipmentHandler {
// Item pickup
protected boolean canPickupItem;
protected UpdateOption itemPickupCooldown = new UpdateOption(5, TimeUnit.TICK);
private long lastItemPickupCheckTime;
protected boolean isDead;
private float health;
@ -90,8 +96,10 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
}
// Items picking
if (canPickupItem()) {
final Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks
if (canPickupItem() && !CooldownUtils.hasCooldown(time, lastItemPickupCheckTime, itemPickupCooldown)) {
this.lastItemPickupCheckTime = time;
final Chunk chunk = getChunk(); // TODO check surrounding chunks
final Set<Entity> entities = instance.getChunkEntities(chunk);
for (Entity entity : entities) {
if (entity instanceof ItemEntity) {
@ -305,7 +313,10 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
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);
}
});

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.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.permission.Permission;
import net.minestom.server.recipe.Recipe;
import net.minestom.server.recipe.RecipeManager;
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.ChunkUtils;
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.world.DimensionType;
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 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 int permissionLevel;
@ -148,8 +155,6 @@ public class Player extends LivingEntity implements CommandSender {
// Tick related
private final PlayerTickEvent playerTickEvent = new PlayerTickEvent(this);
private final List<Permission> permissions = new LinkedList<>();
public Player(UUID uuid, String username, PlayerConnection playerConnection) {
super(EntityType.PLAYER);
this.uuid = uuid; // Override Entity#uuid defined in the constructor
@ -210,13 +215,13 @@ public class Player extends LivingEntity implements CommandSender {
playerConnection.sendPacket(serverDifficultyPacket);
SpawnPositionPacket spawnPositionPacket = new SpawnPositionPacket();
spawnPositionPacket.x = 0;
spawnPositionPacket.y = 0;
spawnPositionPacket.z = 0;
spawnPositionPacket.x = (int) respawnPoint.getX();
spawnPositionPacket.y = (int) respawnPoint.getY();
spawnPositionPacket.z = (int) respawnPoint.getZ();
playerConnection.sendPacket(spawnPositionPacket);
// Add player to list with spawning skin
PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this);
PlayerSkinInitEvent skinInitEvent = new PlayerSkinInitEvent(this, skin);
callEvent(PlayerSkinInitEvent.class, skinInitEvent);
this.skin = skinInitEvent.getSkin();
playerConnection.sendPacket(getAddPlayerToList());
@ -291,9 +296,13 @@ public class Player extends LivingEntity implements CommandSender {
@Override
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();
// Process received packets
@ -338,7 +347,8 @@ public class Player extends LivingEntity implements CommandSender {
final Chunk chunk = instance.getChunkAt(targetBlockPosition);
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.");
chunk.sendPacketToViewers(blockBreakAnimationPacket);
@ -350,20 +360,24 @@ public class Player extends LivingEntity implements CommandSender {
}
// Experience orb pickup
final Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks
final Set<Entity> entities = instance.getChunkEntities(chunk);
for (Entity entity : entities) {
if (entity instanceof ExperienceOrb) {
final ExperienceOrb experienceOrb = (ExperienceOrb) entity;
final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox();
if (expandedBoundingBox.intersect(itemBoundingBox)) {
if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled())
continue;
PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(experienceOrb);
callCancellableEvent(PickupExperienceEvent.class, pickupExperienceEvent, () -> {
short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player
entity.remove();
});
if (!CooldownUtils.hasCooldown(time, lastExperiencePickupCheckTime, experiencePickupCooldown)) {
this.lastExperiencePickupCheckTime = time;
final Chunk chunk = getChunk(); // TODO check surrounding chunks
final Set<Entity> entities = instance.getChunkEntities(chunk);
for (Entity entity : entities) {
if (entity instanceof ExperienceOrb) {
final ExperienceOrb experienceOrb = (ExperienceOrb) entity;
final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox();
if (expandedBoundingBox.intersect(itemBoundingBox)) {
if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled())
continue;
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.onGround = onGround;
lastX = position.getX();
lastY = position.getY();
lastZ = position.getZ();
lastYaw = position.getYaw();
lastPitch = position.getPitch();
updatePacket = entityPositionAndRotationPacket;
} else if (positionChanged) {
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.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128);
entityPositionPacket.onGround = onGround;
lastX = position.getX();
lastY = position.getY();
lastZ = position.getZ();
updatePacket = entityPositionPacket;
} else {
// View changed
@ -439,12 +446,11 @@ public class Player extends LivingEntity implements CommandSender {
entityRotationPacket.pitch = position.getPitch();
entityRotationPacket.onGround = onGround;
lastYaw = position.getYaw();
lastPitch = position.getPitch();
updatePacket = entityRotationPacket;
}
if (viewChanged) {
// Yaw from the rotation packet seems to be ignored, which is why this is required
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
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
public void kill() {
if (!isDead()) {
// send death screen text to the killed player
{
ColoredText deathText;
@ -608,6 +625,16 @@ public class Player extends LivingEntity implements CommandSender {
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
public void setInstance(@NotNull Instance instance) {
Check.notNull(instance, "instance cannot be null!");
@ -641,7 +668,8 @@ public class Player extends LivingEntity implements CommandSender {
final ChunkCallback callback = (chunk) -> {
if (chunk != null) {
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);
}
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) {
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
super.setInstance(instance);
if (firstSpawn) {
teleport(getRespawnPoint());
}
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
callEvent(PlayerSpawnEvent.class, spawnEvent);
}
@ -736,12 +769,6 @@ public class Player extends LivingEntity implements CommandSender {
sendMessage(ColoredText.of(message));
}
@NotNull
@Override
public Collection<Permission> getAllPermissions() {
return permissions;
}
/**
* Sends a message to the player.
*
@ -1136,6 +1163,9 @@ public class Player extends LivingEntity implements CommandSender {
public synchronized void setSkin(@Nullable PlayerSkin skin) {
this.skin = skin;
if (instance == null)
return;
DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket();
destroyEntitiesPacket.entityIds = new int[]{getEntityId()};
@ -1260,7 +1290,8 @@ public class Player extends LivingEntity implements CommandSender {
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.entityFacePosition = facePoint == FacePoint.EYE ?
FacePlayerPacket.FacePosition.EYES : FacePlayerPacket.FacePosition.FEET;
@ -1687,7 +1718,7 @@ public class Player extends LivingEntity implements CommandSender {
public void setTeam(Team team) {
super.setTeam(team);
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 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.targetBlockPosition = targetBlockPosition;
@ -2338,7 +2370,7 @@ public class Player extends LivingEntity implements CommandSender {
// Team
if (this.getTeam() != null)
connection.sendPacket(this.getTeam().getTeamsCreationPacket());
connection.sendPacket(this.getTeam().createTeamsCreationPacket());
EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket();
entityHeadLookPacket.entityId = getEntityId();
@ -2513,7 +2545,8 @@ public class Player extends LivingEntity implements CommandSender {
* @param displayedSkinParts the player displayed skin parts
* @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;

View File

@ -60,6 +60,9 @@ public class FakePlayer extends Player {
/**
* 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 username the FakePlayer username

View File

@ -35,7 +35,7 @@ public class PlayerLoginEvent extends Event {
* <p>
* 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
public Instance getSpawningInstance() {

View File

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

View File

@ -1,11 +1,11 @@
package net.minestom.server.extensions;
import com.google.gson.*;
import lombok.extern.slf4j.Slf4j;
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixins;
@ -19,9 +19,10 @@ import java.net.URLClassLoader;
import java.util.*;
import java.util.zip.ZipFile;
@Slf4j
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_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
private final static Gson GSON = new Gson();
@ -40,7 +41,7 @@ public final class ExtensionManager {
if (!extensionFolder.exists()) {
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;
}
}
@ -57,7 +58,7 @@ public final class ExtensionManager {
}
loader = newClassLoader(urls);
} catch (MalformedURLException e) {
log.error("Failed to get URL.", e);
LOGGER.error("Failed to get URL.", e);
continue;
}
// 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("'");
}
log.error("Failed to find extension.json in the urls '{}'.", urlsString);
LOGGER.error("Failed to find extension.json in the urls '{}'.", urlsString);
continue;
}
JsonObject extensionDescriptionJson = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject();
@ -80,8 +81,8 @@ public final class ExtensionManager {
final String extensionName = extensionDescriptionJson.get("name").getAsString();
// Check the validity of the extension's name.
if (!extensionName.matches("[A-Za-z]+")) {
log.error("Extension '{}' specified an invalid name.", extensionName);
log.error("Extension '{}' will not be loaded.", extensionName);
LOGGER.error("Extension '{}' specified an invalid name.", extensionName);
LOGGER.error("Extension '{}' will not be loaded.", extensionName);
continue;
}
@ -90,8 +91,8 @@ public final class ExtensionManager {
{
String version;
if (!extensionDescriptionJson.has("version")) {
log.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 '{}' did not specify a version.", extensionName);
LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName);
version = "Not Specified";
} else {
version = extensionDescriptionJson.get("version").getAsString();
@ -109,7 +110,7 @@ public final class ExtensionManager {
extensionLoaders.put(extensionName.toLowerCase(), loader);
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;
}
@ -117,7 +118,7 @@ public final class ExtensionManager {
try {
jarClass = Class.forName(mainClass, true, loader);
} 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;
}
@ -125,7 +126,7 @@ public final class ExtensionManager {
try {
extensionClass = jarClass.asSubclass(Extension.class);
} 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;
}
@ -135,19 +136,19 @@ public final class ExtensionManager {
// Let's just make it accessible, plugin creators don't have to make this public.
constructor.setAccessible(true);
} 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;
}
Extension extension = null;
try {
extension = constructor.newInstance();
} 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;
} catch (IllegalAccessException ignored) {
// We made it accessible, should not occur
} catch (InvocationTargetException e) {
log.error(
LOGGER.error(
"While instantiating the main class '{}' in '{}' an exception was thrown.",
mainClass,
extensionName,
@ -164,7 +165,7 @@ public final class ExtensionManager {
} catch (IllegalAccessException e) {
// We made it accessible, should not occur
} 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;
}
@ -178,7 +179,7 @@ public final class ExtensionManager {
e.printStackTrace();
} catch (NoSuchFieldException e) {
// 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);
@ -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
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 extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER);
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) {
final ClassLoader cl = getClass().getClassLoader();
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;
}
MinestomOverwriteClassLoader modifiableClassLoader = (MinestomOverwriteClassLoader) cl;
log.info("Start loading code modifiers...");
LOGGER.info("Start loading code modifiers...");
for (DiscoveredExtension extension : extensions) {
try {
if (extension.description.has("codeModifiers")) {
@ -276,14 +277,14 @@ public final class ExtensionManager {
if (extension.description.has("mixinConfig")) {
final String mixinConfigFile = extension.description.get("mixinConfig").getAsString();
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) {
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 {

View File

@ -1,12 +1,21 @@
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.extras.mojangAuth.MojangCrypt;
import java.net.Proxy;
import java.security.KeyPair;
public final class MojangAuth {
@Getter
private static boolean usingMojangAuth = false;
private static boolean enabled = 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.
@ -15,9 +24,25 @@ public final class MojangAuth {
*/
public static void init() {
if (MinecraftServer.getNettyServer().getAddress() == null) {
usingMojangAuth = true;
enabled = true;
} else {
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;
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.
* <p>
@ -7,7 +14,7 @@ package net.minestom.server.extras.bungee;
*/
public final class BungeeCordProxy {
private static boolean enabled;
private static volatile boolean enabled;
/**
* Enables bungee IP forwarding.
@ -24,4 +31,29 @@ public final class BungeeCordProxy {
public static boolean isEnabled() {
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;
import lombok.extern.slf4j.Slf4j;
import net.minestom.server.MinecraftServer;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
@ -20,9 +22,10 @@ import java.util.Set;
/**
* Class Loader that can modify class bytecode when they are loaded
*/
@Slf4j
public class MinestomOverwriteClassLoader extends URLClassLoader {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
private static MinestomOverwriteClassLoader INSTANCE;
/**
@ -105,18 +108,18 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
try {
// we do not load system classes by ourselves
Class<?> systemClass = ClassLoader.getPlatformClassLoader().loadClass(name);
log.trace("System class: " + systemClass);
LOGGER.trace("System class: " + systemClass);
return systemClass;
} catch (ClassNotFoundException e) {
try {
if (isProtected(name)) {
log.trace("Protected: " + name);
LOGGER.trace("Protected: " + name);
return super.loadClass(name, resolve);
}
return define(name, loadBytes(name, true), resolve);
} 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
// this forbids code modification, but at least it will load
return super.loadClass(name, resolve);
@ -137,7 +140,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
private Class<?> define(String name, byte[] bytes, boolean resolve) {
Class<?> defined = defineClass(name, bytes, 0, bytes.length);
log.trace("Loaded with code modifiers: " + name);
LOGGER.trace("Loaded with code modifiers: " + name);
if (resolve) {
resolveClass(defined);
}
@ -180,7 +183,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
};
node.accept(writer);
bytes = writer.toByteArray();
log.trace("Modified " + name);
LOGGER.trace("Modified " + name);
}
}
return bytes;
@ -208,7 +211,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
if (CodeModifier.class.isAssignableFrom(modifierClass)) {
CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance();
synchronized (modifiers) {
log.warn("Added Code modifier: " + modifier);
LOGGER.warn("Added Code modifier: " + modifier);
addCodeModifier(modifier);
}
}

View File

@ -1,25 +1,29 @@
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;
/**
* Takes care of logging mixin operations
*/
@Slf4j
public class MixinAuditTrailMinestom implements IMixinAuditTrail {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
@Override
public void onApply(String className, String mixinName) {
log.trace("Applied mixin "+mixinName+" to class "+className);
LOGGER.trace("Applied mixin " + mixinName + " to class " + className);
}
@Override
public void onPostProcess(String className) {
log.trace("Post processing "+className);
LOGGER.trace("Post processing " + className);
}
@Override
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 io.netty.buffer.ByteBuf;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.utils.binary.BinaryReader;
import org.jetbrains.annotations.NotNull;
@ -22,7 +23,7 @@ public final class VelocityProxy {
public static final String PLAYER_INFO_CHANNEL = "velocity:player_info";
private static final int SUPPORTED_FORWARDING_VERSION = 1;
private static boolean enabled;
private static volatile boolean enabled;
private static byte[] secret;
/**
@ -72,7 +73,30 @@ public final class VelocityProxy {
}
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;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer;
import net.minestom.server.Viewable;
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.BlockManager;
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.UpdateLightPacket;
import net.minestom.server.network.player.PlayerConnection;
@ -33,7 +31,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
// 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)},
* allowing you to implement your own storage solution if needed.
* <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 {
@ -68,11 +69,6 @@ public abstract class Chunk implements Viewable, DataContainer {
private final boolean shouldGenerate;
private boolean readOnly;
// Packet cache
private volatile boolean enableCachePacket;
protected volatile boolean packetUpdated;
private ByteBuf fullDataPacket;
protected volatile boolean loaded = true;
protected final Set<Player> viewers = new CopyOnWriteArraySet<>();
@ -87,9 +83,6 @@ public abstract class Chunk implements Viewable, DataContainer {
this.chunkZ = chunkZ;
this.shouldGenerate = shouldGenerate;
// true by default
this.enableCachePacket = true;
if (biomes != null && biomes.length == BIOME_COUNT) {
this.biomes = biomes;
} else {
@ -332,51 +325,6 @@ public abstract class Chunk implements Viewable, DataContainer {
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.
*
@ -386,27 +334,6 @@ public abstract class Chunk implements Viewable, DataContainer {
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.
*
@ -517,10 +444,10 @@ public abstract class Chunk implements Viewable, DataContainer {
final PlayerConnection playerConnection = player.getPlayerConnection();
// Retrieve & send the buffer to the connection
retrieveDataBuffer(buf -> playerConnection.sendPacket(buf, true));
playerConnection.sendPacket(getFreshFullDataPacket());
// TODO do not hardcode
if (MinecraftServer.isFixLighting()) {
// TODO do not hardcode light
{
UpdateLightPacket updateLightPacket = new UpdateLightPacket();
updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ();
@ -540,7 +467,8 @@ public abstract class Chunk implements Viewable, DataContainer {
}
updateLightPacket.skyLight = temp;
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
*/
public void sendChunkUpdate(@NotNull Player player) {
retrieveDataBuffer(buf -> {
final PlayerConnection playerConnection = player.getPlayerConnection();
playerConnection.sendPacket(buf, true);
});
final PlayerConnection playerConnection = player.getPlayerConnection();
playerConnection.sendPacket(getFreshFullDataPacket());
}
/**
@ -562,14 +488,10 @@ public abstract class Chunk implements Viewable, DataContainer {
public void sendChunkUpdate() {
final Set<Player> chunkViewers = getViewers();
if (!chunkViewers.isEmpty()) {
retrieveDataBuffer(buf -> chunkViewers.forEach(player -> {
chunkViewers.forEach(player -> {
final PlayerConnection playerConnection = player.getPlayerConnection();
if (!PlayerUtils.isNettyClient(playerConnection))
return;
playerConnection.sendPacket(buf, true);
}));
playerConnection.sendPacket(getFreshFullDataPacket());
});
}
}
@ -578,6 +500,7 @@ public abstract class Chunk implements Viewable, DataContainer {
*
* @param section the section to update
* @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) {
if (!PlayerUtils.isNettyClient(player))
@ -585,7 +508,7 @@ public abstract class Chunk implements Viewable, DataContainer {
Check.argCondition(!MathUtils.isBetween(section, 0, CHUNK_SECTION_COUNT),
"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
*/
@NotNull
protected ChunkDataPacket getChunkSectionUpdatePacket(int section) {
protected ChunkDataPacket createChunkSectionUpdatePacket(int section) {
ChunkDataPacket chunkDataPacket = getFreshPartialDataPacket();
chunkDataPacket.fullChunk = false;
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.entity.pathfinding.PFBlockDescription;
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.utils.ArrayUtils;
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.BinaryWriter;
import net.minestom.server.utils.block.CustomBlockUtils;
@ -42,11 +41,9 @@ public class DynamicChunk extends Chunk {
*/
private static final int DATA_FORMAT_VERSION = 1;
// blocks id based on coordinate, see Chunk#getBlockIndex
// WARNING: those arrays are NOT thread-safe
// and modifying them can cause issue with block data, update, block entity and the cached chunk packet
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];
// WARNING: not thread-safe and should not be changed
protected PaletteStorage blockPalette;
protected PaletteStorage customBlockPalette;
// Used to get all blocks with data (no null)
// Key is still chunk coordinates (see #getBlockIndex)
@ -60,8 +57,17 @@ public class DynamicChunk extends Chunk {
// Block entities
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);
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
@ -79,14 +85,12 @@ public class DynamicChunk extends Chunk {
final int index = getBlockIndex(x, y, z);
// True if the block is not complete air without any custom block capabilities
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.customBlocksId[index] = 0; // Remove custom block
this.blockPalette.setBlockAt(x, y, z, blockStateId);
this.customBlockPalette.setBlockAt(x, y, z, customBlockId);
if (!hasBlock) {
// Block has been deleted, clear cache and return
this.blocksData.remove(index);
@ -94,8 +98,6 @@ public class DynamicChunk extends Chunk {
this.updatableBlocksLastUpdate.remove(index);
this.blockEntities.remove(index);
this.packetUpdated = false;
return;
}
@ -121,8 +123,6 @@ public class DynamicChunk extends Chunk {
} else {
this.blockEntities.remove(index);
}
this.packetUpdated = false;
}
@Override
@ -155,41 +155,23 @@ public class DynamicChunk extends Chunk {
@Override
public short getBlockStateId(int x, int y, int z) {
final int index = getBlockIndex(x, y, z);
if (!MathUtils.isBetween(index, 0, blocksStateId.length)) {
return 0; // TODO: custom invalid block
}
return blocksStateId[index];
return this.blockPalette.getBlockAt(x, y, z);
}
@Override
public short getCustomBlockId(int x, int y, int z) {
final int index = getBlockIndex(x, y, z);
if (!MathUtils.isBetween(index, 0, blocksStateId.length)) {
return 0; // TODO: custom invalid block
}
return customBlocksId[index];
return customBlockPalette.getBlockAt(x, y, z);
}
@Override
protected void refreshBlockValue(int x, int y, int z, short blockStateId, short customBlockId) {
final int blockIndex = getBlockIndex(x, y, z);
if (!MathUtils.isBetween(blockIndex, 0, blocksStateId.length)) {
return;
}
this.blocksStateId[blockIndex] = blockStateId;
this.customBlocksId[blockIndex] = customBlockId;
this.blockPalette.setBlockAt(x, y, z, blockStateId);
this.customBlockPalette.setBlockAt(x, y, z, customBlockId);
}
@Override
protected void refreshBlockStateId(int x, int y, int z, short blockStateId) {
final int blockIndex = getBlockIndex(x, y, z);
if (!MathUtils.isBetween(blockIndex, 0, blocksStateId.length)) {
return;
}
this.blocksStateId[blockIndex] = blockStateId;
this.blockPalette.setBlockAt(x, y, z, blockStateId);
}
@Override
@ -260,8 +242,8 @@ public class DynamicChunk extends Chunk {
for (byte z = 0; z < CHUNK_SIZE_Z; z++) {
final int index = getBlockIndex(x, y, z);
final short blockStateId = blocksStateId[index];
final short customBlockId = customBlocksId[index];
final short blockStateId = blockPalette.getBlockAt(x, y, z);
final short customBlockId = customBlockPalette.getBlockAt(x, y, z);
// No block at the position
if (blockStateId == 0 && customBlockId == 0)
@ -398,8 +380,8 @@ public class DynamicChunk extends Chunk {
fullDataPacket.biomes = biomes.clone();
fullDataPacket.chunkX = chunkX;
fullDataPacket.chunkZ = chunkZ;
fullDataPacket.blocksStateId = blocksStateId.clone();
fullDataPacket.customBlocksId = customBlocksId.clone();
fullDataPacket.paletteStorage = blockPalette.copy();
fullDataPacket.customBlockPaletteStorage = customBlockPalette.copy();
fullDataPacket.blockEntities = new HashSet<>(blockEntities);
fullDataPacket.blocksData = new Int2ObjectOpenHashMap<>(blocksData);
return fullDataPacket;
@ -409,8 +391,8 @@ public class DynamicChunk extends Chunk {
@Override
public Chunk copy(int chunkX, int chunkZ) {
DynamicChunk dynamicChunk = new DynamicChunk(biomes.clone(), chunkX, chunkZ);
ArrayUtils.copyToDestination(blocksStateId, dynamicChunk.blocksStateId);
ArrayUtils.copyToDestination(customBlocksId, dynamicChunk.customBlocksId);
dynamicChunk.blockPalette = blockPalette.copy();
dynamicChunk.customBlockPalette = customBlockPalette.copy();
dynamicChunk.blocksData.putAll(blocksData);
dynamicChunk.updatableBlocks.addAll(updatableBlocks);
dynamicChunk.updatableBlocksLastUpdate.putAll(updatableBlocksLastUpdate);

View File

@ -1,9 +1,10 @@
package net.minestom.server.instance;
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.utils.BlockPosition;
import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -54,7 +55,7 @@ public abstract class Explosion {
*
* @param instance instance to perform this explosion in
*/
public void apply(Instance instance) {
public void apply(@NotNull Instance instance) {
List<BlockPosition> blocks = prepare(instance);
ExplosionPacket packet = new ExplosionPacket();
packet.x = getCenterX();
@ -80,7 +81,7 @@ public abstract class Explosion {
postExplosion(instance, blocks, packet);
// TODO send only to close players
PacketWriterUtils.writeAndSend(instance.getPlayers(), packet);
PacketUtils.sendGroupedPacket(instance.getPlayers(), packet);
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.BlockManager;
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.TimeUpdatePacket;
import net.minestom.server.storage.StorageLocation;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils;
@ -67,7 +67,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
// The time of the instance
private long time;
private int timeRate = 1;
private UpdateOption timeUpdate = new UpdateOption(1, TimeUnit.TICK);
private UpdateOption timeUpdate = new UpdateOption(1, TimeUnit.SECOND);
private long lastTimeUpdate;
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.
* 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
*/
@ -369,7 +370,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
*/
public void setTime(long 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
*/
@NotNull
private TimeUpdatePacket getTimePacket() {
private TimeUpdatePacket createTimePacket() {
TimeUpdatePacket timeUpdatePacket = new TimeUpdatePacket();
timeUpdatePacket.worldAge = worldAge;
timeUpdatePacket.timeOfDay = time;
@ -996,7 +997,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
// time needs to be send to players
if (timeUpdate != null && !CooldownUtils.hasCooldown(time, lastTimeUpdate, timeUpdate)) {
PacketWriterUtils.writeAndSend(getPlayers(), getTimePacket());
PacketUtils.sendGroupedPacket(getPlayers(), createTimePacket());
this.lastTimeUpdate = time;
}

View File

@ -581,7 +581,7 @@ public class InstanceContainer extends Instance {
* @throws NullPointerException if {@code chunkSupplier} is null
*/
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;
}
@ -605,6 +605,15 @@ public class InstanceContainer extends Instance {
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.
* <p>
@ -744,7 +753,7 @@ public class InstanceContainer extends Instance {
* @param blockPosition the block position
* @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.blockPosition = blockPosition;
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.Player;
import net.minestom.server.network.PacketWriterUtils;
import net.minestom.server.network.packet.server.play.WorldBorderPacket;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.Position;
import org.jetbrains.annotations.NotNull;
@ -100,6 +100,7 @@ public class WorldBorder {
*/
public void setWarningTime(int warningTime) {
this.warningTime = warningTime;
WorldBorderPacket worldBorderPacket = new WorldBorderPacket();
worldBorderPacket.action = WorldBorderPacket.Action.SET_WARNING_TIME;
worldBorderPacket.wbAction = new WorldBorderPacket.WBSetWarningTime(warningTime);
@ -115,6 +116,7 @@ public class WorldBorder {
*/
public void setWarningBlocks(int warningBlocks) {
this.warningBlocks = warningBlocks;
WorldBorderPacket worldBorderPacket = new WorldBorderPacket();
worldBorderPacket.action = WorldBorderPacket.Action.SET_WARNING_BLOCKS;
worldBorderPacket.wbAction = new WorldBorderPacket.WBSetWarningBlocks(warningBlocks);
@ -137,6 +139,7 @@ public class WorldBorder {
this.speed = speed;
this.lerpStartTime = System.currentTimeMillis();
WorldBorderPacket worldBorderPacket = new WorldBorderPacket();
worldBorderPacket.action = WorldBorderPacket.Action.LERP_SIZE;
worldBorderPacket.wbAction = new WorldBorderPacket.WBLerpSize(oldDiameter, newDiameter, speed);
@ -270,10 +273,10 @@ public class WorldBorder {
/**
* 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) {
PacketWriterUtils.writeAndSend(instance.getPlayers(), worldBorderPacket);
private void sendPacket(@NotNull WorldBorderPacket packet) {
PacketUtils.sendGroupedPacket(instance.getPlayers(), packet);
}
public enum CollisionAxis {

View File

@ -1,11 +1,18 @@
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.instance.*;
import net.minestom.server.instance.block.CustomBlock;
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.ChunkUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@ -21,49 +28,76 @@ import java.util.List;
*/
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 Chunk chunk;
// 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.chunk = chunk;
}
@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);
}
@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);
Check.notNull(customBlock, "The custom block with the id " + customBlockId + " does not exist!");
addBlockData((byte) x, y, (byte) z, customBlock.getDefaultBlockStateId(), customBlockId, data);
}
@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);
}
private void addBlockData(byte x, int y, byte z, short blockStateId, short customBlockId, Data data) {
// TODO store a single long with bitwise operators (xyz;boolean,short,short,boolean) with the data in a map
final BlockData blockData = new BlockData(x, y, z, blockStateId, customBlockId, data);
synchronized (dataList) {
this.dataList.add(blockData);
private void addBlockData(byte x, int y, byte z, short blockStateId, short customBlockId, @Nullable Data data) {
final int index = ChunkUtils.getBlockIndex(x, y, z);
if (data != null) {
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(() -> {
final List<ChunkPopulator> populators = chunkGenerator.getPopulators();
final boolean hasPopulator = populators != null && !populators.isEmpty();
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);
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.
*/
public void clearData() {
synchronized (dataList) {
this.dataList.clear();
synchronized (blocks) {
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
*/
private void singleThreadFlush(@Nullable ChunkCallback callback, boolean safeCallback) {
if (blocks.isEmpty()) {
OptionalCallback.execute(callback, chunk);
return;
}
synchronized (chunk) {
if (!chunk.isLoaded())
return;
synchronized (dataList) {
for (BlockData data : dataList) {
data.apply(chunk);
synchronized (blocks) {
for (long block : blocks) {
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;
private final short blockStateId;
private final short customBlockId;
private final Data data;
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;
Data data = null;
if (!blockDataMap.isEmpty()) {
synchronized (blockDataMap) {
data = blockDataMap.get(index);
}
}
public void apply(Chunk chunk) {
chunk.UNSAFE_setBlock(x, y, z, blockStateId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId));
}
chunk.UNSAFE_setBlock(ChunkUtils.blockIndexToChunkPositionX(index),
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 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();
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");
@ -41,7 +41,7 @@ public class BlockManager {
* @param blockPlacementRule the block placement rule to register
* @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();
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());
}
// Send the cleared inventory to viewers
// TODO cached packet with empty content
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.item.ItemStack;
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.SetSlotPacket;
import net.minestom.server.network.packet.server.play.WindowItemsPacket;
@ -214,7 +213,7 @@ public class PlayerInventory implements InventoryModifier, InventoryClickHandler
* the inventory items
*/
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.
*
* @param slot the packet slot
* @param slot the packet slot,
* see {@link net.minestom.server.utils.inventory.PlayerInventoryUtils#convertToPacketSlot(int)}
* @param itemStack the item stack in the slot
*/

View File

@ -37,7 +37,7 @@ import java.util.*;
*/
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;
@ -76,7 +76,7 @@ public class ItemStack implements DataContainer {
{
if (defaultStackingRule == null)
defaultStackingRule = DEFAULT_STACKING_RULE;
defaultStackingRule = VANILLA_STACKING_RULE;
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) {
}
}
}

View File

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

View File

@ -9,9 +9,9 @@ import net.minestom.server.command.CommandManager;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerChatEvent;
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.server.play.ChatMessagePacket;
import net.minestom.server.utils.PacketUtils;
import java.util.Collection;
import java.util.function.Function;
@ -62,7 +62,7 @@ public class ChatMessageListener {
ChatMessagePacket chatMessagePacket =
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)
return;
final ItemStack item = packet.item;
short slot = packet.slot;
final ItemStack item = packet.item;
if (slot != -1) {
// Set item
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.utils.Position;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
public class PlayerPositionListener {
@ -47,7 +48,7 @@ public class PlayerPositionListener {
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) {
// Try to move in an unloaded chunk, prevent it

View File

@ -1,21 +1,24 @@
package net.minestom.server.listener.manager;
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;
/**
* 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
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 packetController the packet controller, used to cancel or control which listener will be called
* @param packet the received packet
* @param player the player concerned by the packet
* @param packetController the packet controller, can be used to cancel the 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;
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 {
private boolean cancel;
private PacketListenerConsumer packetListenerConsumer;
protected PacketController(@Nullable PacketListenerConsumer packetListenerConsumer) {
this.packetListenerConsumer = packetListenerConsumer;
protected PacketController() {
}
/**
@ -33,25 +29,4 @@ public class PacketController {
public void setCancel(boolean 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.packet.client.ClientPlayPacket;
import net.minestom.server.network.packet.client.play.*;
import net.minestom.server.network.packet.server.ServerPacket;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class PacketListenerManager {
public final static Logger LOGGER = LoggerFactory.getLogger(PacketListenerManager.class);
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
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 <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();
@ -64,26 +68,37 @@ public final class PacketListenerManager {
// Listener can be null if none has been set before, call PacketConsumer anyway
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(packetListenerConsumer);
for (PacketConsumer packetConsumer : CONNECTION_MANAGER.getReceivePacketConsumers()) {
final PacketController packetController = new PacketController();
for (PacketConsumer<ClientPlayPacket> packetConsumer : CONNECTION_MANAGER.getReceivePacketConsumers()) {
packetConsumer.accept(player, packetController, packet);
}
if (packetController.isCancel())
return;
// Get the new listener (or the same) from the packet controller
packetListenerConsumer = packetController.getPacketListenerConsumer();
// Finally execute the listener
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)
if (packetListenerConsumer != null) {
packetListenerConsumer.accept(packet, player);
/**
* Executes the consumers from {@link ConnectionManager#onPacketSend(PacketConsumer)}.
*
* @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.fakeplayer.FakePlayer;
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.server.ServerPacket;
import net.minestom.server.network.packet.server.login.LoginSuccessPacket;
import net.minestom.server.network.packet.server.play.ChatMessagePacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.callback.validator.PlayerValidator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -26,7 +29,9 @@ public final class ConnectionManager {
private final Map<PlayerConnection, Player> connectionPlayerMap = Collections.synchronizedMap(new HashMap<>());
// 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
private UuidProvider uuidProvider;
// 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) {
ChatMessagePacket chatMessagePacket =
new ChatMessagePacket(json, ChatMessagePacket.Position.SYSTEM_MESSAGE);
PacketWriterUtils.writeAndSend(recipients, chatMessagePacket);
PacketUtils.sendGroupedPacket(recipients, chatMessagePacket);
}
private Collection<Player> getRecipients(@Nullable PlayerValidator condition) {
@ -142,7 +148,7 @@ public final class ConnectionManager {
* @return an unmodifiable list of packet's consumers
*/
@NotNull
public List<PacketConsumer> getReceivePacketConsumers() {
public List<PacketConsumer<ClientPlayPacket>> getReceivePacketConsumers() {
return Collections.unmodifiableList(receivePacketConsumers);
}
@ -151,14 +157,39 @@ public final class ConnectionManager {
*
* @param packetConsumer the packet consumer
*/
public void onPacketReceive(@NotNull PacketConsumer packetConsumer) {
public void onPacketReceive(@NotNull PacketConsumer<ClientPlayPacket> 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.
* <p>
* 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,
* 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 username the new player username
* @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);
createPlayer(player);
return player;
}
/**
@ -277,12 +311,14 @@ public final class ConnectionManager {
* @param connection the player connection
* @param uuid the uuid 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);
connection.sendPacket(loginSuccessPacket);
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.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 {
private final static Logger LOGGER = LoggerFactory.getLogger(PacketProcessor.class);
@ -94,10 +104,43 @@ public final class PacketProcessor {
return connectionPlayerConnectionMap.get(channel);
}
public void removePlayerConnection(ChannelHandlerContext channel) {
public void removePlayerConnection(@NotNull ChannelHandlerContext 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.
*
@ -106,12 +149,20 @@ public final class PacketProcessor {
* @param reader the buffer containing the packet
*/
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 {
readable.read(reader);
} catch (Exception e) {
final Player player = connection.getPlayer();
final String username = player != null ? player.getUsername() : "null";
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.EpollEventLoopGroup;
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.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
@ -29,24 +32,29 @@ public class NettyServer {
private String address;
private int port;
public NettyServer(PacketProcessor packetProcessor) {
public NettyServer(@NotNull PacketProcessor packetProcessor) {
Class<? extends ServerChannel> channel;
if (Epoll.isAvailable()) {
boss = new EpollEventLoopGroup(2);
worker = new EpollEventLoopGroup();
worker = new EpollEventLoopGroup(); // thread count = core * 2
channel = EpollServerSocketChannel.class;
} else if (KQueue.isAvailable()) {
boss = new KQueueEventLoopGroup(2);
worker = new KQueueEventLoopGroup(); // thread count = core * 2
channel = KQueueServerSocketChannel.class;
} else {
boss = new NioEventLoopGroup(2);
worker = new NioEventLoopGroup();
worker = new NioEventLoopGroup(); // thread count = core * 2
channel = NioServerSocketChannel.class;
}
bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker);
bootstrap.channel(channel);
bootstrap = new ServerBootstrap()
.group(boss, worker)
.channel(channel);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(@NotNull SocketChannel ch) {
@ -55,10 +63,11 @@ public class NettyServer {
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());
// Adds packetLength at start | Reads framed bytebuf
pipeline.addLast("framer", new PacketFramer());
pipeline.addLast("framer", new PacketFramer(packetProcessor));
// Reads bytebuf and creating inbound packet
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.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
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.player.PlayerConnection;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
private final static Logger LOGGER = LoggerFactory.getLogger(ClientChannel.class);
private final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
private final PacketProcessor packetProcessor;
public ClientChannel(PacketProcessor packetProcessor) {
public ClientChannel(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
@ -34,9 +36,10 @@ public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
final int availableBytes = packet.body.readableBytes();
if (availableBytes > 0) {
// TODO log4j2
System.err.println("WARNING: Packet 0x" + Integer.toHexString(packet.packetId)
+ " not fully read (" + availableBytes + " bytes left)");
final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
LOGGER.warn("WARNING: Packet 0x" + Integer.toHexString(packet.packetId)
+ " not fully read (" + availableBytes + " bytes left), " + playerConnection);
packet.body.skipBytes(availableBytes);
}
@ -60,7 +63,7 @@ public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.info(cause.getMessage());
LOGGER.info(cause.getMessage());
cause.printStackTrace();
ctx.close();
}

View File

@ -27,45 +27,38 @@ import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
// TODO Optimize
public class PacketCompressor extends ByteToMessageCodec<ByteBuf> {
private final byte[] buffer = new byte[8192];
private final int threshold;
private final Inflater inflater;
private final Deflater deflater;
private byte[] buffer = new byte[8192];
private Deflater deflater = new Deflater();
private Inflater inflater = new Inflater();
public PacketCompressor(int threshold) {
this.inflater = new Inflater();
this.deflater = new Deflater();
this.threshold = threshold;
}
@Override
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);
to.writeBytes(from);
} else {
byte[] abyte = new byte[i];
from.readBytes(abyte);
Utils.writeVarIntBuf(to, packetLength);
Utils.writeVarIntBuf(to, abyte.length);
this.deflater.setInput(abyte, 0, i);
this.deflater.finish();
deflater.setInput(from.nioBuffer());
deflater.finish();
while (!this.deflater.finished()) {
int j = this.deflater.deflate(this.buffer);
to.writeBytes(this.buffer, 0, j);
while (!deflater.finished()) {
final int length = deflater.deflate(buffer);
to.writeBytes(buffer, 0, length);
}
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");
}
// TODO optimize to do not initialize arrays each time
byte[] abyte = new byte[buf.readableBytes()];
buf.readBytes(abyte);
this.inflater.setInput(abyte);
inflater.setInput(abyte);
byte[] abyte1 = new byte[i];
this.inflater.inflate(abyte1);
inflater.inflate(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.handler.codec.ByteToMessageCodec;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
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
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
final int packetSize = from.readableBytes();
@ -40,14 +53,26 @@ public class PacketFramer extends ByteToMessageCodec<ByteBuf> {
if (b >= 0) {
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();
return;
}
out.add(buf.readRetainedSlice(j));
out.add(buf.readRetainedSlice(packetSize));
return;
}
}

View File

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

View File

@ -17,7 +17,7 @@ public abstract class ClientPlayPacket implements ClientPacket {
* @param player the player who sent the packet
*/
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;
import net.minestom.server.network.packet.client.ClientPacket;
import net.minestom.server.utils.binary.BinaryReader;
import org.jetbrains.annotations.NotNull;
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 {
// 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.chat.ChatColor;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.extras.bungee.BungeeCordProxy;
import net.minestom.server.network.ConnectionState;
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 java.net.SocketAddress;
import java.util.UUID;
public class HandshakePacket implements ClientPreplayPacket {
@ -31,7 +33,7 @@ public class HandshakePacket implements ClientPreplayPacket {
@Override
public void read(@NotNull BinaryReader reader) {
this.protocolVersion = reader.readVarInt();
this.serverAddress = reader.readSizedString();
this.serverAddress = reader.readSizedString(BungeeCordProxy.isEnabled() ? Short.MAX_VALUE : 255);
this.serverPort = reader.readUnsignedShort();
this.nextState = reader.readVarInt();
}
@ -39,6 +41,7 @@ public class HandshakePacket implements ClientPreplayPacket {
@Override
public void process(@NotNull PlayerConnection connection) {
// Bungee support (IP forwarding)
if (BungeeCordProxy.isEnabled() && connection instanceof NettyPlayerConnection) {
NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
@ -48,8 +51,25 @@ public class HandshakePacket implements ClientPreplayPacket {
if (split.length == 3 || split.length == 4) {
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);
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 {
nettyPlayerConnection.sendPacket(new LoginDisconnectPacket(INVALID_BUNGEE_FORWARDING));
nettyPlayerConnection.disconnect();

View File

@ -4,6 +4,7 @@ import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
import net.minestom.server.MinecraftServer;
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.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
@ -38,25 +39,25 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
try {
final String loginUsername = nettyConnection.getLoginUsername();
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;
}
if (!loginUsername.isEmpty()) {
final byte[] digestedData = MojangCrypt.digestData("", MinecraftServer.getKeyPair().getPublic(), getSecretKey());
final byte[] digestedData = MojangCrypt.digestData("", MojangAuth.getKeyPair().getPublic(), getSecretKey());
if (digestedData == null) {
// 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();
return;
}
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());
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());
}
} catch (AuthenticationUnavailableException e) {
@ -73,10 +74,11 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
}
public SecretKey getSecretKey() {
return MojangCrypt.decryptByteToSecretKey(MinecraftServer.getKeyPair().getPrivate(), sharedSecret);
return MojangCrypt.decryptByteToSecretKey(MojangAuth.getKeyPair().getPrivate(), sharedSecret);
}
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.chat.ChatColor;
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.network.ConnectionManager;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
@ -37,7 +39,11 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
if (channel != null) {
boolean success = false;
SocketAddress socketAddress = null;
UUID playerUuid = null;
String playerUsername = null;
PlayerSkin playerSkin = null;
// Velocity
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 int port = ((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort();
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) {
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 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 {
LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE);
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.chat.ChatColor;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.Player;
import net.minestom.server.extras.MojangAuth;
import net.minestom.server.extras.bungee.BungeeCordProxy;
import net.minestom.server.extras.velocity.VelocityProxy;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
@ -27,8 +29,10 @@ public class LoginStartPacket implements ClientPreplayPacket {
@Override
public void process(@NotNull PlayerConnection connection) {
final boolean isNettyClient = connection instanceof NettyPlayerConnection;
// Cache the login username and start compression if enabled
if (connection instanceof NettyPlayerConnection) {
if (isNettyClient) {
NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
nettyPlayerConnection.UNSAFE_setLoginUsername(username);
@ -40,7 +44,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
}
// Proxy support (only for netty clients)
if (connection instanceof NettyPlayerConnection) {
if (isNettyClient) {
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
if (CONNECTION_MANAGER.getPlayer(username) != null) {
connection.sendPacket(new LoginDisconnectPacket(ALREADY_CONNECTED_JSON));
@ -79,16 +83,22 @@ public class LoginStartPacket implements ClientPreplayPacket {
EncryptionRequestPacket encryptionRequestPacket = new EncryptionRequestPacket(nettyPlayerConnection);
nettyPlayerConnection.sendPacket(encryptionRequestPacket);
} else {
final boolean bungee = BungeeCordProxy.isEnabled();
// 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
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()];
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
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
public void read(@NotNull BinaryReader reader) {
this.windowId = reader.readByte();
this.recipe = reader.readSizedString();
this.recipe = reader.readSizedString(256);
this.makeAll = reader.readBoolean();
}
}

View File

@ -10,6 +10,6 @@ public class ClientNameItemPacket extends ClientPlayPacket {
@Override
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
public void read(@NotNull BinaryReader reader) {
this.channel = reader.readSizedString();
this.channel = reader.readSizedString(256);
this.data = reader.getRemainingBytes();
}
}

View File

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

View File

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

View File

@ -12,6 +12,6 @@ public class ClientTabCompletePacket extends ClientPlayPacket {
@Override
public void read(@NotNull BinaryReader reader) {
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
public void read(@NotNull BinaryReader reader) {
this.entityId = reader.readVarInt();
this.command = reader.readSizedString();
this.command = reader.readSizedString(Short.MAX_VALUE);
this.trackOutput = reader.readBoolean();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.fakeplayer.FakePlayer;
@ -13,24 +12,11 @@ import java.net.SocketAddress;
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
public void sendPacket(@NotNull ServerPacket serverPacket) {
getFakePlayer().getController().consumePacket(serverPacket);
}
@Override
public void flush() {
// Does nothing
if (shouldSendPacket(serverPacket)) {
getFakePlayer().getController().consumePacket(serverPacket);
}
}
@NotNull

View File

@ -1,10 +1,8 @@
package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.socket.SocketChannel;
import lombok.Getter;
import lombok.Setter;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.extras.mojangAuth.Decrypter;
import net.minestom.server.extras.mojangAuth.Encrypter;
import net.minestom.server.extras.mojangAuth.MojangCrypt;
@ -19,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
import javax.crypto.SecretKey;
import java.net.SocketAddress;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
@ -31,16 +30,14 @@ public class NettyPlayerConnection extends PlayerConnection {
private final SocketChannel channel;
private SocketAddress remoteAddress;
@Getter
private boolean encrypted = false;
@Getter
private boolean compressed = false;
//Could be null. Only used for Mojang Auth
@Getter
@Setter
private byte[] nonce = new byte[4];
// Data from client packets
private String loginUsername;
private String serverAddress;
private int serverPort;
@ -49,6 +46,10 @@ public class NettyPlayerConnection extends PlayerConnection {
// cleared once the player enters the play state
private final Map<Integer, String> pluginRequestMap = new ConcurrentHashMap<>();
// Bungee
private UUID bungeeUuid;
private PlayerSkin bungeeSkin;
public NettyPlayerConnection(@NotNull SocketChannel channel) {
super();
this.channel = channel;
@ -81,38 +82,22 @@ public class NettyPlayerConnection extends PlayerConnection {
channel.pipeline().addAfter("framer", "compressor", new PacketCompressor(threshold));
}
@Override
public void sendPacket(@NotNull ByteBuf buffer, boolean copy) {
if (copy) {
buffer = buffer.copy();
buffer.retain();
channel.writeAndFlush(buffer);
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);
}
}
/**
* Writes a packet to the connection channel.
* <p>
* All packets are flushed during {@link net.minestom.server.entity.Player#update(long)}.
*
* @param serverPacket the packet to write
*/
@Override
public void sendPacket(@NotNull ServerPacket serverPacket) {
channel.writeAndFlush(serverPacket);
}
@Override
public void flush() {
getChannel().flush();
if (shouldSendPacket(serverPacket)) {
if (getPlayer() != null) {
channel.write(serverPacket); // Flush on player update
} else {
channel.writeAndFlush(serverPacket);
}
}
}
@NotNull
@ -186,6 +171,24 @@ public class NettyPlayerConnection extends PlayerConnection {
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.
* <p>
@ -235,4 +238,12 @@ public class NettyPlayerConnection extends PlayerConnection {
this.serverAddress = serverAddress;
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;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText;
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.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.packet.server.play.DisconnectPacket;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.SocketAddress;
import java.util.concurrent.atomic.AtomicInteger;
@ -21,6 +23,8 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public abstract class PlayerConnection {
protected static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
private Player player;
private ConnectionState connectionState;
private boolean online;
@ -29,7 +33,6 @@ public abstract class PlayerConnection {
private static final ColoredText rateLimitKickMessage = ColoredText.of(ChatColor.RED + "Too Many Packets");
//Connection Stats
@Getter
private final AtomicInteger packetCounter = new AtomicInteger(0);
private final AtomicInteger lastPacketCounter = new AtomicInteger(0);
private short tickCounter = 0;
@ -68,33 +71,37 @@ public abstract class PlayerConnection {
}
}
/**
* Sends a raw {@link ByteBuf} to the client.
*
* @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);
public AtomicInteger getPacketCounter() {
return packetCounter;
}
/**
* 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.
* @param copy Should be true unless your only using the ByteBuf once.
* @return this connection identifier
*/
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.
* <p>
* Also responsible for executing {@link ConnectionManager#onPacketSend(PacketConsumer)} consumers.
*
* @param serverPacket the packet to send
* @see #shouldSendPacket(ServerPacket)
*/
public abstract void sendPacket(@NotNull ServerPacket serverPacket);
/**
* Flush all waiting packets.
*/
public abstract void flush();
protected boolean shouldSendPacket(@NotNull ServerPacket serverPacket) {
return player == null || PACKET_LISTENER_MANAGER.processServerPacket(serverPacket, player);
}
/**
* Gets the remote address of the client.
@ -112,8 +119,9 @@ public abstract class PlayerConnection {
/**
* Gets the player linked to this connection.
*
* @return the player
* @return the player, can be null if not initialized yet
*/
@Nullable
public Player getPlayer() {
return player;
}
@ -164,4 +172,12 @@ public abstract class PlayerConnection {
public int getLastPacketCounter() {
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