mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-27 19:47:44 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
974372d2bd
2
.github/README.md
vendored
2
.github/README.md
vendored
@ -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
27
.github/workflows/javadoc.yml
vendored
Normal 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
25
.github/workflows/tests.yml
vendored
Normal 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
|
30
build.gradle
30
build.gradle
@ -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
|
||||
|
@ -1,3 +1,3 @@
|
||||
asmVersion=8.0.1
|
||||
mixinVersion=0.8
|
||||
asmVersion=9.0
|
||||
mixinVersion=0.8.1
|
||||
hephaistos_version=v1.1.5
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)}.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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}.
|
||||
*
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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) && 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() {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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 = "~";
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
package net.minestom.server.instance.block;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BlockProvider {
|
||||
short getBlockStateId(int x, int y, int z);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user