Merge remote-tracking branch 'upstream/master'

This commit is contained in:
iamceph 2021-01-25 00:18:27 +01:00
commit 399c857dc4
237 changed files with 5617 additions and 2574 deletions

3
.gitignore vendored
View File

@ -53,3 +53,6 @@ gradle-app.setting
# When running the demo we generate the extensions folder
/extensions/
# When compiling we get a docs folder
/docs

View File

@ -5,7 +5,7 @@ plugins {
id 'java'
id 'maven-publish'
id 'net.ltgt.apt' version '0.10'
id 'org.jetbrains.kotlin.jvm' version '1.4.10'
id 'org.jetbrains.kotlin.jvm' version '1.4.21'
id 'checkstyle'
}
@ -106,11 +106,14 @@ dependencies {
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.6.2')
// Netty
api 'io.netty:netty-handler:4.1.55.Final'
api 'io.netty:netty-codec:4.1.55.Final'
api 'io.netty:netty-transport-native-epoll:4.1.55.Final:linux-x86_64'
api 'io.netty:netty-transport-native-kqueue:4.1.55.Final:osx-x86_64'
api 'io.netty.incubator:netty-incubator-transport-native-io_uring:0.0.2.Final:linux-x86_64'
api 'io.netty:netty-handler:4.1.58.Final'
api 'io.netty:netty-codec:4.1.58.Final'
api 'io.netty:netty-transport-native-epoll:4.1.58.Final:linux-x86_64'
api 'io.netty:netty-transport-native-kqueue:4.1.58.Final:osx-x86_64'
api 'io.netty.incubator:netty-incubator-transport-native-io_uring:0.0.3.Final:linux-x86_64'
// https://mvnrepository.com/artifact/org.apache.commons/commons-text
compile group: 'org.apache.commons', name: 'commons-text', version: '1.9'
// https://mvnrepository.com/artifact/it.unimi.dsi/fastutil
api 'it.unimi.dsi:fastutil:8.4.4'
@ -123,7 +126,7 @@ dependencies {
api 'com.github.Articdive:Jnoise:1.0.0'
// https://mvnrepository.com/artifact/org.rocksdb/rocksdbjni
api 'org.rocksdb:rocksdbjni:6.13.3'
api 'org.rocksdb:rocksdbjni:6.15.2'
// Logging
api 'org.apache.logging.log4j:log4j-core:2.14.0'
@ -143,14 +146,15 @@ dependencies {
api "org.spongepowered:mixin:${mixinVersion}"
// Path finding
api 'com.github.MadMartian:hydrazine-path-finding:1.5.1'
api 'com.github.MadMartian:hydrazine-path-finding:1.5.2'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${project.kotlinVersion}"
api "org.jetbrains.kotlin:kotlin-reflect:${project.kotlinVersion}"
// NBT parsing/manipulation/saving
api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}")
api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}:gson")
api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}") {
api("com.github.jglrxavpok:Hephaistos:${project.hephaistosVersion}")
api("com.github.jglrxavpok:Hephaistos:${project.hephaistosVersion}:gson")
api("com.github.jglrxavpok:Hephaistos:${project.hephaistosVersion}") {
capabilities {
requireCapability("org.jglrxavpok.nbt:Hephaistos-gson")
}

View File

@ -1,3 +1,4 @@
asmVersion=9.0
mixinVersion=0.8.1
hephaistos_version=v1.1.5
hephaistosVersion=v1.1.7
kotlinVersion=1.4.21

Binary file not shown.

View File

@ -1,7 +1,6 @@
#Thu Aug 01 13:49:10 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=fd591a34af7385730970399f473afabdb8b28d57fd97d6625c388d090039d6fd
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
distributionSha256Sum=0f316a67b971b7b571dac7215dcf2591a30994b3450e0629925ffcfe2c68cc5c

53
gradlew vendored
View File

@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

43
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -45,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@ -1,7 +1,6 @@
// AUTOGENERATED by net.minestom.codegen.RegistriesGenerator
package net.minestom.server.registry;
import java.util.HashMap;
import net.minestom.server.entity.EntityType;
import net.minestom.server.fluids.Fluid;
import net.minestom.server.instance.block.Block;
@ -16,6 +15,8 @@ import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
/**
* AUTOGENERATED
*/
@ -129,19 +130,19 @@ public final class Registries {
}
/**
* Returns the corresponding EntityType matching the given id. Returns 'PIG' if none match.
* Returns the corresponding EntityType matching the given id. Returns null if none match.
*/
@NotNull
@Nullable
public static EntityType getEntityType(String id) {
return getEntityType(NamespaceID.from(id));
}
/**
* Returns the corresponding EntityType matching the given id. Returns 'PIG' if none match.
* Returns the corresponding EntityType matching the given id. Returns null if none match.
*/
@NotNull
@Nullable
public static EntityType getEntityType(NamespaceID id) {
return entityTypes.getOrDefault(id, EntityType.PIG);
return entityTypes.get(id);
}
/**

View File

@ -35,7 +35,7 @@ public class BlockEnumGenerator extends MinestomEnumGenerator<BlockContainer> {
private final String targetVersion;
private final File targetFolder;
private CodeBlock.Builder staticBlock = CodeBlock.builder();
private final CodeBlock.Builder staticBlock = CodeBlock.builder();
public static void main(String[] args) throws IOException {
@ -161,8 +161,6 @@ public class BlockEnumGenerator extends MinestomEnumGenerator<BlockContainer> {
}
return blocks;
} catch (IOException e) {
throw e;
}
}
@ -178,8 +176,6 @@ public class BlockEnumGenerator extends MinestomEnumGenerator<BlockContainer> {
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(blockFile))) {
PrismarineJSBlock[] blocks = gson.fromJson(bufferedReader, PrismarineJSBlock[].class);
return Arrays.asList(blocks);
} catch (IOException e) {
throw e;
}
}

View File

@ -85,8 +85,6 @@ public class ItemEnumGenerator extends MinestomEnumGenerator<ItemContainer> {
items.add(item);
}
return items;
} catch (IOException e) {
throw e;
}
}

View File

@ -1,5 +1,8 @@
package net.minestom.server.map;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
@ -10,7 +13,7 @@ import java.util.Map;
public class PaletteGenerator {
public static void main(String[] args) {
Map<Integer, Integer> colors = new HashMap<>();
Int2IntMap colors = new Int2IntOpenHashMap();
int highestIndex = 0;
for(MapColors c : MapColors.values()) {
if (c == MapColors.NONE)

View File

@ -6,10 +6,10 @@ import net.minestom.server.command.CommandManager;
import net.minestom.server.data.DataManager;
import net.minestom.server.data.DataType;
import net.minestom.server.data.SerializableData;
import net.minestom.server.entity.EntityManager;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.exception.ExceptionManager;
import net.minestom.server.extensions.Extension;
import net.minestom.server.extensions.ExtensionManager;
import net.minestom.server.fluids.Fluid;
@ -56,7 +56,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
/**
* The main server class used to start the server and retrieve all the managers.
@ -68,7 +67,7 @@ public final class MinecraftServer {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
public static final String VERSION_NAME = "1.16.4";
public static final String VERSION_NAME = "1.16.5";
public static final int PROTOCOL_VERSION = 754;
// Threads
@ -101,11 +100,12 @@ public final class MinecraftServer {
private static int nettyThreadCount = Runtime.getRuntime().availableProcessors();
private static boolean processNettyErrors = false;
private static ExceptionManager exceptionManager;
// In-Game Manager
private static ConnectionManager connectionManager;
private static InstanceManager instanceManager;
private static BlockManager blockManager;
private static EntityManager entityManager;
private static CommandManager commandManager;
private static RecipeManager recipeManager;
private static StorageManager storageManager;
@ -133,6 +133,7 @@ public final class MinecraftServer {
private static int entityViewDistance = 5;
private static int compressionThreshold = 256;
private static boolean packetCaching = true;
private static boolean groupedPacket = true;
private static ResponseDataConsumer responseDataConsumer;
private static String brandName = "Minestom";
private static Difficulty difficulty = Difficulty.NORMAL;
@ -142,8 +143,11 @@ public final class MinecraftServer {
public static MinecraftServer init() {
if (minecraftServer != null) // don't init twice
return minecraftServer;
// Initialize the ExceptionManager at first
exceptionManager = new ExceptionManager();
extensionManager = new ExtensionManager();
extensionManager.loadExtensions();
// warmup/force-init registries
// without this line, registry types that are not loaded explicitly will have an internal empty registry in Registries
@ -167,7 +171,6 @@ public final class MinecraftServer {
instanceManager = new InstanceManager();
blockManager = new BlockManager();
entityManager = new EntityManager();
commandManager = new CommandManager();
recipeManager = new RecipeManager();
storageManager = new StorageManager();
@ -217,7 +220,6 @@ public final class MinecraftServer {
* @throws NullPointerException if {@code brandName} is null
*/
public static void setBrandName(@NotNull String brandName) {
Check.notNull(brandName, "The brand name cannot be null");
MinecraftServer.brandName = brandName;
PacketUtils.sendGroupedPacket(connectionManager.getOnlinePlayers(), PluginMessagePacket.getBrandPacket());
@ -275,7 +277,6 @@ public final class MinecraftServer {
* @param difficulty the new server difficulty
*/
public static void setDifficulty(@NotNull Difficulty difficulty) {
Check.notNull(difficulty, "The server difficulty cannot be null.");
MinecraftServer.difficulty = difficulty;
// Send the packet to all online players
@ -337,16 +338,6 @@ public final class MinecraftServer {
return blockManager;
}
/**
* Gets the manager handling waiting players.
*
* @return the entity manager
*/
public static EntityManager getEntityManager() {
checkInitStatus(entityManager);
return entityManager;
}
/**
* Gets the manager handling commands.
*
@ -417,6 +408,16 @@ public final class MinecraftServer {
return benchmarkManager;
}
/**
* Gets the exception manager for exception handling.
*
* @return the exception manager
*/
public static ExceptionManager getExceptionManager() {
checkInitStatus(exceptionManager);
return exceptionManager;
}
/**
* Gets the manager handling server connections.
*
@ -478,9 +479,7 @@ public final class MinecraftServer {
MinecraftServer.chunkViewDistance = chunkViewDistance;
if (started) {
final Collection<Player> players = connectionManager.getOnlinePlayers();
players.forEach(player -> {
for (final Player player : connectionManager.getOnlinePlayers()) {
final Chunk playerChunk = player.getChunk();
if (playerChunk != null) {
@ -490,7 +489,7 @@ public final class MinecraftServer {
player.refreshVisibleChunks(playerChunk);
}
});
}
}
}
@ -514,12 +513,12 @@ public final class MinecraftServer {
"The entity view distance must be between 0 and 32");
MinecraftServer.entityViewDistance = entityViewDistance;
if (started) {
connectionManager.getOnlinePlayers().forEach(player -> {
for (final Player player : connectionManager.getOnlinePlayers()) {
final Chunk playerChunk = player.getChunk();
if (playerChunk != null) {
player.refreshVisibleEntities(playerChunk);
}
});
}
}
}
@ -551,7 +550,8 @@ public final class MinecraftServer {
* This feature allows some packets (implementing the {@link net.minestom.server.utils.cache.CacheablePacket} to be cached
* in order to do not have to be written and compressed over and over again), this is especially useful for chunk and light packets.
* <p>
* It is enabled by default and it is our recommendation, you should only disable it if you want to focus on low memory usage
* It is enabled by default and it is our recommendation,
* you should only disable it if you want to focus on low memory usage
* at the cost of many packet writing and compression.
*
* @return true if the packet caching feature is enabled, false otherwise
@ -572,6 +572,34 @@ public final class MinecraftServer {
MinecraftServer.packetCaching = packetCaching;
}
/**
* Gets if the packet caching feature is enabled.
* <p>
* This features allow sending the exact same packet/buffer to multiple connections.
* It does provide a great performance benefit by allocating and writing/compressing only once.
* <p>
* It is enabled by default and it is our recommendation,
* you should only disable it if you want to modify packet per-players instead of sharing it.
* Disabling the feature would result in performance decrease.
*
* @return true if the grouped packet feature is enabled, false otherwise
*/
public static boolean hasGroupedPacket() {
return groupedPacket;
}
/**
* Enables or disable grouped packet.
*
* @param groupedPacket true to enable grouped packet
* @throws IllegalStateException if this is called after the server started
* @see #hasGroupedPacket()
*/
public static void setGroupedPacket(boolean groupedPacket) {
Check.stateCondition(started, "You cannot change the grouped packet value after the server has been started.");
MinecraftServer.groupedPacket = groupedPacket;
}
/**
* Gets the consumer executed to show server-list data.
*
@ -675,7 +703,7 @@ public final class MinecraftServer {
}
/**
* Gets if the server should process netty errors and other unnecessary netty events
* Gets if the server should process netty errors and other unnecessary netty events.
*
* @return should process netty errors
*/
@ -684,7 +712,7 @@ public final class MinecraftServer {
}
/**
* Sets if the server should process netty errors and other unnecessary netty events
* Sets if the server should process netty errors and other unnecessary netty events.
* false is faster
*
* @param processNettyErrors should process netty errors
@ -718,17 +746,25 @@ public final class MinecraftServer {
nettyServer.init();
nettyServer.start(address, port);
final long t1 = -System.nanoTime();
// Init extensions
// TODO: Extensions should handle depending on each other and have a load-order.
extensionManager.getExtensions().forEach(Extension::preInitialize);
extensionManager.getExtensions().forEach(Extension::initialize);
extensionManager.getExtensions().forEach(Extension::postInitialize);
if (extensionManager.shouldLoadOnStartup()) {
final long loadStartTime = System.nanoTime();
// Load extensions
extensionManager.loadExtensions();
// Init extensions
// TODO: Extensions should handle depending on each other and have a load-order.
extensionManager.getExtensions().forEach(Extension::preInitialize);
extensionManager.getExtensions().forEach(Extension::initialize);
extensionManager.getExtensions().forEach(Extension::postInitialize);
final double loadTime = MathUtils.round((t1 + System.nanoTime()) / 1_000_000D, 2);
LOGGER.info("Extensions loaded in {}ms", loadTime);
final double loadTime = MathUtils.round((System.nanoTime() - loadStartTime) / 1_000_000D, 2);
LOGGER.info("Extensions loaded in {}ms", loadTime);
} else {
LOGGER.warn("Extension loadOnStartup option is set to false, extensions are therefore neither loaded or initialized.");
}
LOGGER.info("Minestom server started successfully.");
commandManager.startConsoleThread();
}
/**

View File

@ -1,12 +1,11 @@
package net.minestom.server;
import com.google.common.collect.Queues;
import net.minestom.server.entity.EntityManager;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.thread.PerInstanceThreadProvider;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -50,35 +49,39 @@ public final class UpdateManager {
* Starts the server loop in the update thread.
*/
protected void start() {
final EntityManager entityManager = MinecraftServer.getEntityManager();
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
updateExecutionService.scheduleAtFixedRate(() -> {
if (stopRequested) {
updateExecutionService.shutdown();
return;
try {
if (stopRequested) {
updateExecutionService.shutdown();
return;
}
long currentTime = System.nanoTime();
final long tickStart = System.currentTimeMillis();
// Tick start callbacks
doTickCallback(tickStartCallbacks, tickStart);
// Waiting players update (newly connected clients waiting to get into the server)
connectionManager.updateWaitingPlayers();
// Keep Alive Handling
connectionManager.handleKeepAlive(tickStart);
// Server tick (chunks/entities)
serverTick(tickStart);
// the time that the tick took in nanoseconds
final long tickTime = System.nanoTime() - currentTime;
// Tick end callbacks
doTickCallback(tickEndCallbacks, tickTime / 1000000L);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
long currentTime = System.nanoTime();
final long tickStart = System.currentTimeMillis();
// Tick start callbacks
doTickCallback(tickStartCallbacks, tickStart);
// Waiting players update (newly connected clients waiting to get into the server)
entityManager.updateWaitingPlayers();
// Keep Alive Handling
entityManager.handleKeepAlive(tickStart);
// Server tick (chunks/entities)
serverTick(tickStart);
// the time that the tick took in nanoseconds
final long tickTime = System.nanoTime() - currentTime;
// Tick end callbacks
doTickCallback(tickEndCallbacks, tickTime / 1000000L);
}, 0, MinecraftServer.TICK_MS, TimeUnit.MILLISECONDS);
}
@ -100,7 +103,7 @@ public final class UpdateManager {
try {
future.get();
} catch (Throwable e) {
e.printStackTrace();
MinecraftServer.getExceptionManager().handleException(e);
}
}
}
@ -136,7 +139,6 @@ public final class UpdateManager {
* @throws NullPointerException if <code>threadProvider</code> is null
*/
public synchronized void setThreadProvider(ThreadProvider threadProvider) {
Check.notNull(threadProvider, "The thread provider cannot be null");
this.threadProvider = threadProvider;
}

View File

@ -75,7 +75,7 @@ public interface Viewable {
/**
* Sends a packet to all viewers and the viewable element if it is a player.
* <p>
* If 'this' isn't a player, then {only @link #sendPacketToViewers(ServerPacket)} is called.
* If 'this' isn't a player, then only {@link #sendPacketToViewers(ServerPacket)} is called.
*
* @param packet the packet to send
*/

View File

@ -1,6 +1,6 @@
package net.minestom.server.advancements;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.entity.Player;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
@ -23,8 +23,8 @@ public class Advancement {
private boolean achieved;
private ColoredText title;
private ColoredText description;
private JsonMessage title;
private JsonMessage description;
private ItemStack icon;
@ -42,7 +42,7 @@ public class Advancement {
// Packet
private AdvancementsPacket.Criteria criteria;
public Advancement(@NotNull ColoredText title, ColoredText description,
public Advancement(@NotNull JsonMessage title, JsonMessage description,
@NotNull ItemStack icon, @NotNull FrameType frameType,
float x, float y) {
this.title = title;
@ -53,7 +53,7 @@ public class Advancement {
this.y = y;
}
public Advancement(@NotNull ColoredText title, @NotNull ColoredText description,
public Advancement(@NotNull JsonMessage title, @NotNull JsonMessage description,
@NotNull Material icon, @NotNull FrameType frameType,
float x, float y) {
this(title, description, new ItemStack(icon, (byte) 1), frameType, x, y);
@ -100,7 +100,7 @@ public class Advancement {
* @return the advancement title
*/
@NotNull
public ColoredText getTitle() {
public JsonMessage getTitle() {
return title;
}
@ -109,7 +109,7 @@ public class Advancement {
*
* @param title the new title
*/
public void setTitle(@NotNull ColoredText title) {
public void setTitle(@NotNull JsonMessage title) {
this.title = title;
update();
}
@ -120,7 +120,7 @@ public class Advancement {
* @return the description title
*/
@NotNull
public ColoredText getDescription() {
public JsonMessage getDescription() {
return description;
}
@ -129,7 +129,7 @@ public class Advancement {
*
* @param description the new description
*/
public void setDescription(@NotNull ColoredText description) {
public void setDescription(@NotNull JsonMessage description) {
this.description = description;
update();
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.advancements;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
@ -14,7 +15,7 @@ import org.jetbrains.annotations.Nullable;
*/
public class AdvancementRoot extends Advancement {
public AdvancementRoot(@NotNull ColoredText title, @NotNull ColoredText description,
public AdvancementRoot(@NotNull JsonMessage title, @NotNull JsonMessage description,
@NotNull ItemStack icon, @NotNull FrameType frameType,
float x, float y,
@Nullable String background) {
@ -22,7 +23,7 @@ public class AdvancementRoot extends Advancement {
setBackground(background);
}
public AdvancementRoot(@NotNull ColoredText title, @NotNull ColoredText description,
public AdvancementRoot(@NotNull JsonMessage title, @NotNull JsonMessage description,
@NotNull Material icon, FrameType frameType,
float x, float y,
@Nullable String background) {

View File

@ -73,7 +73,6 @@ public class AdvancementTab implements Viewable {
* @param parent the parent of this advancement, it cannot be null
*/
public void createAdvancement(@NotNull String identifier, @NotNull Advancement advancement, @NotNull Advancement parent) {
Check.argCondition(identifier == null, "the advancement identifier cannot be null");
Check.stateCondition(!advancementMap.containsKey(parent),
"You tried to set a parent which doesn't exist or isn't registered");
cacheAdvancement(identifier, advancement, parent);

View File

@ -1,7 +1,7 @@
package net.minestom.server.advancements.notifications;
import net.minestom.server.advancements.FrameType;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
@ -11,17 +11,17 @@ import org.jetbrains.annotations.NotNull;
*/
public class Notification {
private final ColoredText title;
private final JsonMessage title;
private final FrameType frameType;
private final ItemStack icon;
public Notification(@NotNull ColoredText title, @NotNull FrameType frameType, @NotNull ItemStack icon) {
public Notification(@NotNull JsonMessage title, @NotNull FrameType frameType, @NotNull ItemStack icon) {
this.title = title;
this.frameType = frameType;
this.icon = icon;
}
public Notification(@NotNull ColoredText title, @NotNull FrameType frameType, @NotNull Material icon) {
public Notification(@NotNull JsonMessage title, @NotNull FrameType frameType, @NotNull Material icon) {
this.title = title;
this.frameType = frameType;
this.icon = new ItemStack(icon, (byte) 1);
@ -33,7 +33,7 @@ public class Notification {
* @return the notification title
*/
@NotNull
public ColoredText getTitle() {
public JsonMessage getTitle() {
return title;
}

View File

@ -13,6 +13,10 @@ public class Attribute {
private static final Map<String, Attribute> ATTRIBUTES = new HashMap<>();
static {
Attributes.init();
}
private final String key;
private final float defaultValue;
private final float maxValue;

View File

@ -18,13 +18,13 @@ public class AttributeInstance {
private final Map<UUID, AttributeModifier> modifiers = new HashMap<>();
private final Consumer<AttributeInstance> propertyChangeListener;
private float baseValue;
private boolean dirty = true;
private float cachedValue = 0.0f;
public AttributeInstance(@NotNull Attribute attribute, @Nullable Consumer<AttributeInstance> listener) {
this.attribute = attribute;
this.propertyChangeListener = listener;
this.baseValue = attribute.getDefaultValue();
refreshCachedValue();
}
/**
@ -47,19 +47,6 @@ public class AttributeInstance {
return baseValue;
}
/**
* Sets this instance dirty to trigger calculation of the new value.
* Triggers the {@link #propertyChangeListener}.
*/
private void setDirty() {
if (!dirty) {
dirty = true;
if (propertyChangeListener != null) {
propertyChangeListener.accept(this);
}
}
}
/**
* Sets the base value of this instance.
*
@ -69,7 +56,11 @@ public class AttributeInstance {
public void setBaseValue(float baseValue) {
if (this.baseValue != baseValue) {
this.baseValue = baseValue;
setDirty();
refreshCachedValue();
if (propertyChangeListener != null) {
propertyChangeListener.accept(this);
}
}
}
@ -80,7 +71,7 @@ public class AttributeInstance {
*/
public void addModifier(@NotNull AttributeModifier modifier) {
if (modifiers.putIfAbsent(modifier.getId(), modifier) == null) {
setDirty();
refreshCachedValue();
}
}
@ -91,7 +82,7 @@ public class AttributeInstance {
*/
public void removeModifier(@NotNull AttributeModifier modifier) {
if (modifiers.remove(modifier.getId()) != null) {
setDirty();
refreshCachedValue();
}
}
@ -100,6 +91,7 @@ public class AttributeInstance {
*
* @return the modifiers.
*/
@NotNull
public Collection<AttributeModifier> getModifiers() {
return modifiers.values();
}
@ -110,34 +102,29 @@ public class AttributeInstance {
* @return the attribute value
*/
public float getValue() {
if (dirty) {
cachedValue = processModifiers();
dirty = false;
}
return cachedValue;
}
/**
* Recalculate the value of this attribute instance using the modifiers.
*
* @return the attribute value
*/
protected float processModifiers() {
protected void refreshCachedValue() {
final Collection<AttributeModifier> modifiers = getModifiers();
float base = getBaseValue();
for (var modifier : modifiers.values().stream().filter(mod -> mod.getOperation() == AttributeOperation.ADDITION).toArray(AttributeModifier[]::new)) {
for (var modifier : modifiers.stream().filter(mod -> mod.getOperation() == AttributeOperation.ADDITION).toArray(AttributeModifier[]::new)) {
base += modifier.getAmount();
}
float result = base;
for (var modifier : modifiers.values().stream().filter(mod -> mod.getOperation() == AttributeOperation.MULTIPLY_BASE).toArray(AttributeModifier[]::new)) {
for (var modifier : modifiers.stream().filter(mod -> mod.getOperation() == AttributeOperation.MULTIPLY_BASE).toArray(AttributeModifier[]::new)) {
result += (base * modifier.getAmount());
}
for (var modifier : modifiers.values().stream().filter(mod -> mod.getOperation() == AttributeOperation.MULTIPLY_TOTAL).toArray(AttributeModifier[]::new)) {
for (var modifier : modifiers.stream().filter(mod -> mod.getOperation() == AttributeOperation.MULTIPLY_TOTAL).toArray(AttributeModifier[]::new)) {
result *= (1.0f + modifier.getAmount());
}
return Math.min(result, getAttribute().getMaxValue());
this.cachedValue = Math.min(result, getAttribute().getMaxValue());
}
}

View File

@ -4,6 +4,7 @@ package net.minestom.server.attribute;
* The Minecraft, vanilla, standards attributes.
*/
public final class Attributes {
public static final Attribute MAX_HEALTH = (new Attribute("generic.max_health", true, 20, 1024)).register();
public static final Attribute FOLLOW_RANGE = (new Attribute("generic.follow_range", true, 32, 2048)).register();
public static final Attribute KNOCKBACK_RESISTANCE = (new Attribute("generic.knockback_resistance", true, 0, 1)).register();
@ -21,4 +22,8 @@ public final class Attributes {
private Attributes() throws IllegalAccessException {
throw new IllegalAccessException("Cannot instantiate a static class");
}
protected static void init() {
// Empty, here to register all the vanilla attributes
}
}

View File

@ -67,7 +67,7 @@ public final class BenchmarkManager {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
MinecraftServer.getExceptionManager().handleException(e);
}
}

View File

@ -1,7 +1,7 @@
package net.minestom.server.bossbar;
import net.minestom.server.Viewable;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.play.BossBarPacket;
import net.minestom.server.utils.MathUtils;
@ -29,7 +29,7 @@ public class BossBar implements Viewable {
private final Set<Player> viewers = new CopyOnWriteArraySet<>();
private final Set<Player> unmodifiableViewers = Collections.unmodifiableSet(viewers);
private ColoredText title;
private JsonMessage title;
private float progress;
private BarColor color;
private BarDivision division;
@ -42,7 +42,7 @@ public class BossBar implements Viewable {
* @param color the boss bar color
* @param division the boss bar division
*/
public BossBar(@NotNull ColoredText title, @NotNull BarColor color, @NotNull BarDivision division) {
public BossBar(@NotNull JsonMessage title, @NotNull BarColor color, @NotNull BarDivision division) {
this.title = title;
this.color = color;
this.division = division;
@ -99,7 +99,7 @@ public class BossBar implements Viewable {
* @return the current title of the bossbar
*/
@NotNull
public ColoredText getTitle() {
public JsonMessage getTitle() {
return title;
}
@ -108,7 +108,7 @@ public class BossBar implements Viewable {
*
* @param title the new title of the bossbar
*/
public void setTitle(@NotNull ColoredText title) {
public void setTitle(@NotNull JsonMessage title) {
this.title = title;
updateTitle();
}

View File

@ -8,6 +8,7 @@ import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@ -199,6 +200,15 @@ public final class ChatColor {
return legacyColorCodesMap.getOrDefault(colorCode, NO_COLOR);
}
/**
* Gets a collection of all chat colors
* @return a collection of all chat colors
*/
@NotNull
public static Collection<ChatColor> values() {
return colorCode.values();
}
public boolean isEmpty() {
return empty;
}

View File

@ -51,13 +51,13 @@ public class ChatHoverEvent {
}
/**
* Shows a {@link ColoredText} when hovered.
* Shows a {@link JsonMessage} when hovered.
*
* @param text the text to show
* @return the chat hover event
*/
@NotNull
public static ChatHoverEvent showText(@NotNull ColoredText text) {
public static ChatHoverEvent showText(@NotNull JsonMessage text) {
return new ChatHoverEvent("show_text", text.getJsonObject());
}

View File

@ -19,8 +19,11 @@ import java.util.regex.Pattern;
*/
public class ColoredText extends JsonMessage {
// the raw text
private String message;
/**
* The raw text StringBuilder
* Its a single StringBuilder instance for easier and faster concenation
*/
private final StringBuilder message = new StringBuilder();
/**
* Creates a colored text.
@ -31,7 +34,7 @@ public class ColoredText extends JsonMessage {
* @see #of(String) to create a colored text
*/
private ColoredText(@NotNull String message) {
this.message = message;
this.message.append(message);
refreshUpdate();
}
@ -79,7 +82,7 @@ public class ColoredText extends JsonMessage {
*/
@NotNull
public ColoredText append(@NotNull ChatColor color, @NotNull String message) {
this.message += color + message;
this.message.append(color).append(message);
refreshUpdate();
return this;
}
@ -116,7 +119,7 @@ public class ColoredText extends JsonMessage {
*/
@NotNull
public String getMessage() {
return message;
return message.toString();
}
/**
@ -160,7 +163,7 @@ public class ColoredText extends JsonMessage {
protected List<JsonObject> getComponents() {
final List<JsonObject> objects = new ArrayList<>();
// No message, return empty list
if (message.isEmpty())
if (getMessage().isEmpty())
return objects;
boolean inFormat = false;

View File

@ -1,8 +1,11 @@
package net.minestom.server.chat;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* Represents a json message which can be send to a player.
* <p>
@ -35,6 +38,16 @@ public abstract class JsonMessage {
this.updated = false;
}
/**
* Gets the content of the message without any formatting or effects.
*
* @return The message without formatting or effects
*/
@NotNull
public String getRawMessage() {
return getTextMessage(getJsonObject()).toString();
}
/**
* Gets the Json representation.
* <p>
@ -56,6 +69,38 @@ public abstract class JsonMessage {
return compiledJson;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JsonMessage message = (JsonMessage) o;
return Objects.equals(toString(), message.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
/**
* Recursively collects the 'text' field from the provided object and it's 'extra's.
*
* @param obj The object to parse
* @return The text content of the object and its 'extra's
*/
@NotNull
private static StringBuilder getTextMessage(@NotNull JsonObject obj) {
StringBuilder message = new StringBuilder(obj.get("text").getAsString());
JsonElement extra = obj.get("extra");
if (extra != null && extra.isJsonArray()) {
for (JsonElement child : extra.getAsJsonArray()) {
if (!child.isJsonObject()) continue;
message.append(getTextMessage(child.getAsJsonObject()));
}
}
return message;
}
public static class RawJsonMessage extends JsonMessage {
private final JsonObject jsonObject;

View File

@ -37,7 +37,6 @@ public class RichMessage extends JsonMessage {
* @return the created rich message object
*/
public static RichMessage of(@NotNull ColoredText coloredText) {
Check.notNull(coloredText, "ColoredText cannot be null");
RichMessage richMessage = new RichMessage();
appendText(richMessage, coloredText);
@ -58,7 +57,6 @@ public class RichMessage extends JsonMessage {
* @return the rich message
*/
public RichMessage append(@NotNull ColoredText coloredText) {
Check.notNull(coloredText, "ColoredText cannot be null");
appendText(this, coloredText);
return this;

View File

@ -26,7 +26,10 @@ public class CollisionUtils {
* @param velocityOut the Vector object in which the new velocity will be saved
* @return whether this entity is on the ground
*/
public static boolean handlePhysics(Entity entity, Vector deltaPosition, Position positionOut, Vector velocityOut) {
public static boolean handlePhysics(@NotNull Entity entity,
@NotNull Vector deltaPosition,
@NotNull Position positionOut,
@NotNull Vector velocityOut) {
// TODO handle collisions with nearby entities (should it be done here?)
final Instance instance = entity.getInstance();
final Position currentPosition = entity.getPosition();
@ -75,7 +78,7 @@ public class CollisionUtils {
* @param stepAmount how much to step in the direction (in blocks)
* @param positionOut the vector in which to store the new position
* @param corners the corners to check against
* @return true iif a collision has been found
* @return true if a collision has been found
*/
private static boolean stepAxis(Instance instance, Vector startPosition, Vector axis, float stepAmount, Vector positionOut, Vector... corners) {
positionOut.copy(startPosition);
@ -182,6 +185,7 @@ public class CollisionUtils {
* @param newPosition the future target position
* @return the position with the world border collision applied (can be {@code newPosition} if not changed)
*/
@NotNull
public static Position applyWorldBorder(@NotNull Instance instance,
@NotNull Position currentPosition, @NotNull Position newPosition) {
final WorldBorder worldBorder = instance.getWorldBorder();

View File

@ -2,6 +2,7 @@ package net.minestom.server.command;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandDispatcher;
import net.minestom.server.command.builder.CommandSyntax;
@ -22,6 +23,7 @@ import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.callback.CommandCallback;
import net.minestom.server.utils.validate.Check;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -41,7 +43,7 @@ public final class CommandManager {
public static final String COMMAND_PREFIX = "/";
private volatile boolean running;
private volatile boolean running = true;
private final ConsoleSender consoleSender = new ConsoleSender();
@ -51,38 +53,6 @@ public final class CommandManager {
private CommandCallback unknownCommandCallback;
public CommandManager() {
running = true;
// Setup console thread
Thread consoleThread = new Thread(() -> {
BufferedReader bi = new BufferedReader(new InputStreamReader(System.in));
while (running) {
try {
if (bi.ready()) {
final String command = bi.readLine();
execute(consoleSender, command);
}
} catch (IOException e) {
e.printStackTrace();
continue;
}
// Prevent permanent looping
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
bi.close();
} catch (IOException e) {
e.printStackTrace();
}
}, "ConsoleCommand-Thread");
consoleThread.setDaemon(true);
consoleThread.start();
}
/**
@ -185,8 +155,6 @@ public final class CommandManager {
* @return true if the command hadn't been cancelled and has been successful
*/
public boolean execute(@NotNull CommandSender sender, @NotNull String command) {
Check.notNull(sender, "Source cannot be null");
Check.notNull(command, "Command string cannot be null");
// Command event
if (sender instanceof Player) {
@ -210,7 +178,7 @@ public final class CommandManager {
return true;
} else {
// Check for legacy-command
final String[] splitCommand = command.split(" ");
final String[] splitCommand = command.split(StringUtils.SPACE);
final String commandName = splitCommand[0];
final CommandProcessor commandProcessor = commandProcessorMap.get(commandName.toLowerCase());
if (commandProcessor == null) {
@ -221,7 +189,7 @@ public final class CommandManager {
}
// Execute the legacy-command
final String[] args = command.substring(command.indexOf(" ") + 1).split(" ");
final String[] args = command.substring(command.indexOf(StringUtils.SPACE) + 1).split(StringUtils.SPACE);
return commandProcessor.process(sender, commandName, args);
}
@ -258,6 +226,43 @@ public final class CommandManager {
return consoleSender;
}
/**
* Starts the thread responsible for executing commands from the console.
*/
public void startConsoleThread() {
Thread consoleThread = new Thread(() -> {
BufferedReader bi = new BufferedReader(new InputStreamReader(System.in));
while (running) {
try {
if (bi.ready()) {
final String command = bi.readLine();
execute(consoleSender, command);
}
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
continue;
}
// Prevent permanent looping
try {
Thread.sleep(200);
} catch (InterruptedException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
try {
bi.close();
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}, "ConsoleCommand-Thread");
consoleThread.setDaemon(true);
consoleThread.start();
}
/**
* Gets the {@link DeclareCommandsPacket} for a specific player.
* <p>
@ -388,9 +393,9 @@ public final class CommandManager {
nodes.add(literalNode);
// Contains the arguments of the already-parsed syntaxes
List<Argument[]> syntaxesArguments = new ArrayList<>();
List<Argument<?>[]> syntaxesArguments = new ArrayList<>();
// Contains the nodes of an argument
Map<Argument, List<DeclareCommandsPacket.Node>> storedArgumentsNodes = new HashMap<>();
Map<Argument<?>, List<DeclareCommandsPacket.Node>> storedArgumentsNodes = new HashMap<>();
for (CommandSyntax syntax : syntaxes) {
final CommandCondition commandCondition = syntax.getCommandCondition();
@ -406,16 +411,17 @@ public final class CommandManager {
// Represent the children of the last node
IntList argChildren = null;
final Argument[] arguments = syntax.getArguments();
final Argument<?>[] arguments = syntax.getArguments();
for (int i = 0; i < arguments.length; i++) {
final Argument argument = arguments[i];
final Argument<?> argument = arguments[i];
final boolean isFirst = i == 0;
final boolean isLast = i == arguments.length - 1;
// Find shared part
boolean foundSharedPart = false;
for (Argument[] parsedArguments : syntaxesArguments) {
for (Argument<?>[] parsedArguments : syntaxesArguments) {
if (ArrayUtils.sameStart(arguments, parsedArguments, i + 1)) {
final Argument sharedArgument = parsedArguments[i];
final Argument<?> sharedArgument = parsedArguments[i];
argChildren = new IntArrayList();
lastNodes = storedArgumentsNodes.get(sharedArgument);
@ -508,7 +514,6 @@ public final class CommandManager {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(nodes, argument, executable, false);
argumentNode.parser = "brigadier:bool";
argumentNode.properties = packetWriter -> packetWriter.writeByte((byte) 0);
} else if (argument instanceof ArgumentDouble) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(nodes, argument, executable, false);
@ -633,16 +638,16 @@ public final class CommandManager {
} else if (argument instanceof ArgumentFloatRange) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(nodes, argument, executable, false);
argumentNode.parser = "minecraft:float_range";
} else if (argument instanceof ArgumentEntities) {
ArgumentEntities argumentEntities = (ArgumentEntities) argument;
} else if (argument instanceof ArgumentEntity) {
ArgumentEntity argumentEntity = (ArgumentEntity) argument;
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(nodes, argument, executable, false);
argumentNode.parser = "minecraft:entity";
argumentNode.properties = packetWriter -> {
byte mask = 0;
if (argumentEntities.isOnlySingleEntity()) {
if (argumentEntity.isOnlySingleEntity()) {
mask += 1;
}
if (argumentEntities.isOnlyPlayers()) {
if (argumentEntity.isOnlyPlayers()) {
mask += 2;
}
packetWriter.writeByte(mask);

View File

@ -11,7 +11,7 @@ import org.jetbrains.annotations.Nullable;
* start by {@link #getCommandName()} or any of the aliases in {@link #getAliases()}.
* <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.
* {@link #onWrite(CommandSender, 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.
*/
@ -36,7 +36,7 @@ public interface CommandProcessor {
String[] getAliases();
/**
* Called when the command is executed by a {@link CommandSender}
* Called when the command is executed by a {@link CommandSender}.
*
* @param sender the sender which executed the command
* @param command the command name used
@ -48,7 +48,7 @@ public interface CommandProcessor {
/**
* Called to know if a player has access to the command.
* <p>
* Right now it is only used to know if the player should see the command in auto-completion
* Right now it is only used to know if the player should see the command in auto-completion.
* Conditions still need to be checked in {@link #process(CommandSender, String, String[])}.
*
* @param player the player to check the access
@ -57,12 +57,12 @@ public interface CommandProcessor {
boolean hasAccess(@NotNull Player player);
/**
* Needed to enable {@link #onWrite(String)} callback.
* Needed to enable {@link #onWrite(CommandSender, String)} callback.
* <p>
* Be aware that enabling it can cost some performance because of how often it will be called.
*
* @return true to enable writing tracking (and server auto completion)
* @see #onWrite(String)
* @see #onWrite(CommandSender, String)
*/
default boolean enableWritingTracking() {
return false;
@ -73,12 +73,13 @@ public interface CommandProcessor {
* <p>
* WARNING: {@link #enableWritingTracking()} needs to return true, you need to override it by default.
*
* @param text the whole player text
* @return the array containing all the suggestion for the current arg (split " "), can be null
* @param sender the command sender
* @param text the whole player text
* @return the array containing all the suggestions for the current arg (split SPACE), can be null
* @see #enableWritingTracking()
*/
@Nullable
default String[] onWrite(@NotNull String text) {
default String[] onWrite(@NotNull CommandSender sender, String text) {
return null;
}
}

View File

@ -32,16 +32,16 @@ public interface CommandSender extends PermissionHandler {
}
/**
* Sends a {@link ColoredText} message.
* Sends a {@link JsonMessage} message.
* If this is not a {@link Player}, only the content of the message will be sent as a string.
*
* @param text The {@link ColoredText} to send.
* @param text The {@link JsonMessage} to send.
* */
default void sendMessage(@NotNull ColoredText text) {
default void sendMessage(@NotNull JsonMessage text) {
if (this instanceof Player) {
((Player) this).sendMessage((JsonMessage) text);
this.sendMessage(text);
} else {
sendMessage(text.getMessage());
sendMessage(text.getRawMessage());
}
}

View File

@ -2,6 +2,7 @@ package net.minestom.server.command.builder;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
/**
@ -13,9 +14,8 @@ public interface ArgumentCallback {
/**
* Executed when an error is found.
*
* @param source the sender which executed the command
* @param value the raw string argument which is responsible for the error
* @param error the error id (you can check its meaning in the specific argument class or ask the developer about it)
* @param sender the sender which executed the command
* @param exception the exception containing the message, input and error code related to the issue
*/
void apply(@NotNull CommandSender source, @NotNull String value, int error);
void apply(@NotNull CommandSender sender, @NotNull ArgumentSyntaxException exception);
}

View File

@ -1,24 +1,25 @@
package net.minestom.server.command.builder;
import net.minestom.server.chat.ChatColor;
import net.minestom.server.entity.Entity;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.entity.EntityType;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Enchantment;
import net.minestom.server.item.ItemStack;
import net.minestom.server.particle.Particle;
import net.minestom.server.potion.PotionEffect;
import net.minestom.server.utils.entity.EntityFinder;
import net.minestom.server.utils.location.RelativeBlockPosition;
import net.minestom.server.utils.location.RelativeVec;
import net.minestom.server.utils.math.FloatRange;
import net.minestom.server.utils.math.IntRange;
import net.minestom.server.utils.time.UpdateOption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -34,6 +35,11 @@ public final class Arguments {
private Map<String, Object> args = new HashMap<>();
@NotNull
public <T> T get(@NotNull Argument<T> argument) {
return (T) getObject(argument.getId());
}
public boolean getBoolean(@NotNull String id) {
return (boolean) getObject(id);
}
@ -115,8 +121,8 @@ public final class Arguments {
}
@NotNull
public List<Entity> getEntities(@NotNull String id) {
return (List<Entity>) getObject(id);
public EntityFinder getEntities(@NotNull String id) {
return (EntityFinder) getObject(id);
}
@NotNull
@ -164,4 +170,15 @@ public final class Arguments {
this.args.clear();
}
protected void retrieveDefaultValues(@Nullable Map<String, Object> defaultValuesMap) {
if (defaultValuesMap == null)
return;
for (Map.Entry<String, Object> entry : defaultValuesMap.entrySet()) {
final String key = entry.getKey();
if (!args.containsKey(key))
this.args.put(key, entry.getValue());
}
}
}

View File

@ -10,10 +10,10 @@ import net.minestom.server.command.builder.condition.CommandCondition;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.*;
/**
* Represents a command which has suggestion/auto-completion.
@ -37,6 +37,8 @@ import java.util.List;
*/
public class Command {
public final static Logger LOGGER = LoggerFactory.getLogger(Command.class);
private final String name;
private final String[] aliases;
@ -114,27 +116,83 @@ public class Command {
* @param commandCondition the condition to use the syntax
* @param executor the executor to call when the syntax is successfully received
* @param args all the arguments of the syntax, the length needs to be higher than 0
* @return the created {@link CommandSyntax}
* @return the created {@link CommandSyntax syntaxes},
* there can be multiple of them when optional arguments are used
*/
public CommandSyntax addSyntax(@Nullable CommandCondition commandCondition,
@NotNull CommandExecutor executor,
@NotNull Argument<?>... args) {
@NotNull
public Collection<CommandSyntax> addSyntax(@Nullable CommandCondition commandCondition,
@NotNull CommandExecutor executor,
@NotNull Argument<?>... args) {
Check.argCondition(args.length == 0,
"The syntax argument cannot be empty, consider using Command#setDefaultExecutor");
final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, args);
this.syntaxes.add(syntax);
return syntax;
// Check optional argument(s)
boolean hasOptional = false;
{
for (Argument<?> argument : args) {
if (argument.isOptional()) {
hasOptional = true;
}
if (hasOptional && !argument.isOptional()) {
LOGGER.warn("Optional arguments are followed by a non-optional one, the default values will be ignored.");
hasOptional = false;
break;
}
}
}
if (!hasOptional) {
final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, args);
this.syntaxes.add(syntax);
return Collections.singleton(syntax);
} else {
List<CommandSyntax> optionalSyntaxes = new ArrayList<>();
// the 'args' array starts by all the required arguments, followed by the optional ones
List<Argument<?>> requiredArguments = new ArrayList<>();
Map<String, Object> defaultValuesMap = new HashMap<>();
boolean optionalBranch = false;
int i = 0;
for (Argument<?> argument : args) {
final boolean isLast = ++i == args.length;
if (argument.isOptional()) {
// Set default value
defaultValuesMap.put(argument.getId(), argument.getDefaultValue());
if (!optionalBranch && !requiredArguments.isEmpty()) {
// First optional argument, create a syntax with current cached arguments
final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, defaultValuesMap,
requiredArguments.toArray(new Argument[0]));
optionalSyntaxes.add(syntax);
optionalBranch = true;
} else {
// New optional argument, save syntax with current cached arguments and save default value
final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, defaultValuesMap,
requiredArguments.toArray(new Argument[0]));
optionalSyntaxes.add(syntax);
}
}
requiredArguments.add(argument);
if (isLast) {
// Create the last syntax
final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, defaultValuesMap,
requiredArguments.toArray(new Argument[0]));
optionalSyntaxes.add(syntax);
}
}
this.syntaxes.addAll(optionalSyntaxes);
return optionalSyntaxes;
}
}
/**
* Adds a new syntax in the command without any condition.
* Adds a new syntax without condition.
*
* @param executor the executor to call when the syntax is successfully received
* @param args all the arguments of the syntax, the length needs to be higher than 0
* @return the created {@link CommandSyntax}
* @see #addSyntax(CommandCondition, CommandExecutor, Argument[])
*/
public CommandSyntax addSyntax(@NotNull CommandExecutor executor, @NotNull Argument<?>... args) {
@NotNull
public Collection<CommandSyntax> addSyntax(@NotNull CommandExecutor executor, @NotNull Argument<?>... args) {
return addSyntax(null, executor, args);
}
@ -196,11 +254,12 @@ public class Command {
* when in a dynamic argument ({@link ArgumentDynamicWord} (when {@link SuggestionType#ASK_SERVER} is used)
* and {@link ArgumentDynamicStringArray}).
*
* @param text the whole player's text
* @return the array containing all the suggestion for the current arg (split " "), can be null
* @param sender the command sender
* @param text the whole player's text
* @return the array containing all the suggestion for the current arg (split SPACE), can be null
*/
@Nullable
public String[] onDynamicWrite(@NotNull String text) {
public String[] onDynamicWrite(@NotNull CommandSender sender, @NotNull String text) {
return null;
}

View File

@ -1,8 +1,11 @@
package net.minestom.server.command.builder;
import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.condition.CommandCondition;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -56,15 +59,15 @@ public class CommandDispatcher {
* @param commandString the command (containing the command name and the args if any)
* @return the result of the parsing, null if the command doesn't exist
*/
@Nullable
public CommandResult parse(@NotNull String commandString) {
commandString = commandString.trim();
// Split space
final String spaceRegex = " ";
final String[] parts = commandString.split(spaceRegex);
final String[] parts = commandString.split(StringUtils.SPACE);
final String commandName = parts[0];
final String[] args = commandString.replaceFirst(Pattern.quote(commandName), "").trim().split(spaceRegex);
final String[] args = commandString.replaceFirst(Pattern.quote(commandName), "").trim().split(StringUtils.SPACE);
final Command command = findCommand(commandName);
// Check if the command exists
@ -107,6 +110,7 @@ public class CommandDispatcher {
return commandMap.getOrDefault(commandName, null);
}
@NotNull
private CommandResult findCommandResult(@NotNull Command command, @NotNull String[] args) {
CommandResult result = new CommandResult();
result.command = command;
@ -125,75 +129,89 @@ public class CommandDispatcher {
// All the registered syntaxes of the command
final Collection<CommandSyntax> syntaxes = command.getSyntaxes();
// Contains all the fully validated syntaxes (we later find the one with the most amount of arguments)
List<CommandSyntax> validSyntaxes = new ArrayList<>();
// Contains the raw string value of each argument that has been validated
// CommandSyntax - (Argument index/Raw string value)
Map<CommandSyntax, String[]> syntaxesValues = new HashMap<>();
List<ValidSyntaxHolder> validSyntaxes = new ArrayList<>();
// Contains all the syntaxes that are not fully correct, used to later, retrieve the "most correct syntax"
// Number of correct argument - The data about the failing argument
TreeMap<Integer, CommandSuggestionHolder> syntaxesSuggestions = new TreeMap<>(Collections.reverseOrder());
Int2ObjectRBTreeMap<CommandSuggestionHolder> syntaxesSuggestions = new Int2ObjectRBTreeMap<>(Collections.reverseOrder());
for (CommandSyntax syntax : syntaxes) {
final Argument<?>[] arguments = syntax.getArguments();
final String[] argsValues = new String[Byte.MAX_VALUE];
final List<Object> argsValues = new ArrayList<>(arguments.length);
boolean syntaxCorrect = true;
// The current index in the raw command string arguments
int argIndex = 0;
int splitIndex = 0;
boolean useRemaining = false;
// Check the validity of the arguments...
for (int argCount = 0; argCount < syntax.getArguments().length; argCount++) {
final Argument<?> argument = syntax.getArguments()[argCount];
for (int argCount = 0; argCount < arguments.length; argCount++) {
final boolean lastArgumentIteration = argCount + 1 == arguments.length;
final Argument<?> argument = arguments[argCount];
useRemaining = argument.useRemaining();
// the correction result of the argument
int correctionResult = Argument.SUCCESS;
// the parsed argument value, null if incorrect
Object parsedValue;
// the argument exception, null if the input is correct
ArgumentSyntaxException argumentSyntaxException = null;
// true if the arg is valid, false otherwise
boolean correct = false;
// the raw string representing the correct argument syntax
StringBuilder argValue = new StringBuilder();
if (useRemaining) {
final boolean hasArgs = args.length > argIndex;
final boolean hasArgs = args.length > splitIndex;
// Verify if there is any string part available
if (hasArgs) {
// Argument is supposed to take the rest of the command input
for (int i = argIndex; i < args.length; i++) {
for (int i = splitIndex; i < args.length; i++) {
final String arg = args[i];
if (argValue.length() > 0)
argValue.append(" ");
argValue.append(StringUtils.SPACE);
argValue.append(arg);
}
final String argValueString = argValue.toString();
correctionResult = argument.getCorrectionResult(argValueString);
if (correctionResult == Argument.SUCCESS) {
try {
parsedValue = argument.parse(argValueString);
correct = true;
argsValues[argCount] = argValueString;
argsValues.add(parsedValue);
} catch (ArgumentSyntaxException exception) {
argumentSyntaxException = exception;
}
}
} else {
// Argument is either single-word or can accept optional delimited space(s)
for (int i = argIndex; i < args.length; i++) {
for (int i = splitIndex; i < args.length; i++) {
final String rawArg = args[i];
argValue.append(rawArg);
final String argValueString = argValue.toString();
correctionResult = argument.getCorrectionResult(argValueString);
if (correctionResult == Argument.SUCCESS) {
try {
parsedValue = argument.parse(argValueString);
// Prevent quitting the parsing too soon if the argument
// does not allow space
if (lastArgumentIteration && i + 1 < args.length) {
if (!argument.allowSpace())
break;
argValue.append(StringUtils.SPACE);
continue;
}
correct = true;
argsValues[argCount] = argValueString;
argIndex = i + 1;
argsValues.add(parsedValue);
splitIndex = i + 1;
break;
} else {
} catch (ArgumentSyntaxException exception) {
argumentSyntaxException = exception;
if (!argument.allowSpace())
break;
argValue.append(" ");
argValue.append(StringUtils.SPACE);
}
}
}
@ -204,8 +222,7 @@ public class CommandDispatcher {
syntaxCorrect = false;
CommandSuggestionHolder suggestionHolder = new CommandSuggestionHolder();
suggestionHolder.syntax = syntax;
suggestionHolder.argValue = argValue.toString();
suggestionHolder.correctionResult = correctionResult;
suggestionHolder.argumentSyntaxException = argumentSyntaxException;
suggestionHolder.argIndex = argCount;
syntaxesSuggestions.put(argCount, suggestionHolder);
break;
@ -214,9 +231,12 @@ public class CommandDispatcher {
// Add the syntax to the list of valid syntaxes if correct
if (syntaxCorrect) {
if (args.length == argIndex || useRemaining) {
validSyntaxes.add(syntax);
syntaxesValues.put(syntax, argsValues);
if (arguments.length == argsValues.size() || useRemaining) {
ValidSyntaxHolder validSyntaxHolder = new ValidSyntaxHolder();
validSyntaxHolder.syntax = syntax;
validSyntaxHolder.argumentsValue = argsValues;
validSyntaxes.add(validSyntaxHolder);
}
}
}
@ -224,7 +244,7 @@ public class CommandDispatcher {
// Check if there is at least one correct syntax
if (!validSyntaxes.isEmpty()) {
// Search the syntax with all perfect args
final CommandSyntax finalSyntax = findMostCorrectSyntax(validSyntaxes, syntaxesValues, executorArgs);
final CommandSyntax finalSyntax = findMostCorrectSyntax(validSyntaxes, executorArgs);
if (finalSyntax != null) {
// A fully correct syntax has been found, use it
result.syntax = finalSyntax;
@ -233,54 +253,25 @@ public class CommandDispatcher {
return result;
}
// Otherwise, search for the first syntax with an incorrect argument
for (CommandSyntax syntax : validSyntaxes) {
final Argument[] arguments = syntax.getArguments();
final String[] argsValues = syntaxesValues.get(syntax);
for (int i = 0; i < arguments.length; i++) {
final Argument argument = arguments[i];
final String argValue = argsValues[i];
// Finally parse it
final Object parsedValue = argument.parse(argValue);
final int conditionResult = argument.getConditionResult(parsedValue);
if (conditionResult != Argument.SUCCESS) {
// Condition of an argument not correct, use the argument callback if any
if (argument.hasErrorCallback()) {
result.callback = argument.getCallback();
result.value = argValue;
result.error = conditionResult;
return result;
}
}
}
}
}
// No all-correct syntax, find the closest one to use the argument callback
{
// Get closest valid syntax
if (!syntaxesSuggestions.isEmpty()) {
final int max = syntaxesSuggestions.firstKey(); // number of correct arguments
// Check if at least 1 argument of the syntax is correct
if (max > 0) {
// Get the data of the closest syntax
final CommandSuggestionHolder suggestionHolder = syntaxesSuggestions.get(max);
final CommandSyntax syntax = suggestionHolder.syntax;
final String argValue = suggestionHolder.argValue;
final int correctionResult = suggestionHolder.correctionResult;
final int argIndex = suggestionHolder.argIndex;
final int max = syntaxesSuggestions.firstIntKey(); // number of correct arguments in the most correct syntax
final CommandSuggestionHolder suggestionHolder = syntaxesSuggestions.get(max);
final CommandSyntax syntax = suggestionHolder.syntax;
final ArgumentSyntaxException argumentSyntaxException = suggestionHolder.argumentSyntaxException;
final int argIndex = suggestionHolder.argIndex;
// Found the closest syntax with at least 1 correct argument
final Argument<?> argument = syntax.getArguments()[argIndex];
if (argument.hasErrorCallback()) {
result.callback = argument.getCallback();
result.value = argValue;
result.error = correctionResult;
// Found the closest syntax with at least 1 correct argument
final Argument<?> argument = syntax.getArguments()[argIndex];
if (argument.hasErrorCallback()) {
result.callback = argument.getCallback();
result.argumentSyntaxException = argumentSyntaxException;
return result;
}
return result;
}
}
}
@ -296,60 +287,67 @@ public class CommandDispatcher {
* Retrieves from the valid syntax map the arguments condition result and get the one with the most
* valid arguments.
*
* @param validSyntaxes the list containing all the valid syntaxes
* @param syntaxesValues the map containing the argument raw string values
* @param executorArgs the recipient of the argument parsed values
* @param validSyntaxes the list containing all the valid syntaxes
* @param executorArgs the recipient of the argument parsed values
* @return the command syntax with all of its arguments correct and with the most arguments count, null if not any
*/
@Nullable
private CommandSyntax findMostCorrectSyntax(@NotNull List<CommandSyntax> validSyntaxes,
@NotNull Map<CommandSyntax, String[]> syntaxesValues,
private CommandSyntax findMostCorrectSyntax(@NotNull List<ValidSyntaxHolder> validSyntaxes,
@NotNull Arguments executorArgs) {
Map<CommandSyntax, Arguments> argumentsValueMap = new HashMap<>();
CommandSyntax finalSyntax = null;
int maxArguments = 0;
for (CommandSyntax syntax : validSyntaxes) {
Arguments syntaxValues = new Arguments();
boolean fullyCorrect = true;
Arguments finalArguments = null;
for (ValidSyntaxHolder validSyntaxHolder : validSyntaxes) {
final CommandSyntax syntax = validSyntaxHolder.syntax;
final Argument<?>[] arguments = syntax.getArguments();
final String[] argsValues = syntaxesValues.get(syntax);
for (int i = 0; i < arguments.length; i++) {
final Argument argument = arguments[i];
final String argValue = argsValues[i];
// Finally parse it
final Object parsedValue = argument.parse(argValue);
final int conditionResult = argument.getConditionResult(parsedValue);
if (conditionResult == Argument.SUCCESS) {
syntaxValues.setArg(argument.getId(), parsedValue);
} else {
// One argument is incorrect, stop the whole syntax check
fullyCorrect = false;
break;
}
}
final int argumentsCount = arguments.length;
final List<Object> argsValues = validSyntaxHolder.argumentsValue;
final int argumentLength = arguments.length;
if (fullyCorrect && argumentLength > maxArguments) {
final int argsSize = argsValues.size();
if (argsSize > maxArguments) {
finalSyntax = syntax;
maxArguments = argumentLength;
argumentsValueMap.put(syntax, syntaxValues);
maxArguments = argsSize;
// Fill arguments map
Arguments syntaxValues = new Arguments();
for (int i = 0; i < argumentsCount; i++) {
final Argument<?> argument = arguments[i];
final Object argumentValue = argsValues.get(i);
syntaxValues.setArg(argument.getId(), argumentValue);
}
finalArguments = syntaxValues;
}
}
// Get the arguments values
if (finalSyntax != null) {
executorArgs.copy(argumentsValueMap.get(finalSyntax));
executorArgs.copy(finalArguments);
}
return finalSyntax;
}
/**
* Holds the data of a validated syntax.
*/
private static class ValidSyntaxHolder {
private CommandSyntax syntax;
/**
* (Argument index/Argument parsed object)
*/
private List<Object> argumentsValue;
}
/**
* Holds the data of an invalidated syntax.
*/
private static class CommandSuggestionHolder {
private CommandSyntax syntax;
private String argValue;
private int correctionResult;
private ArgumentSyntaxException argumentSyntaxException;
private int argIndex;
}
@ -369,8 +367,7 @@ public class CommandDispatcher {
// Argument Callback
private ArgumentCallback callback;
private String value;
private int error;
private ArgumentSyntaxException argumentSyntaxException;
/**
* Executes the command for the given source.
@ -399,16 +396,17 @@ public class CommandDispatcher {
// The executor is from a syntax
final CommandCondition commandCondition = syntax.getCommandCondition();
if (commandCondition == null || commandCondition.canUse(source, commandString)) {
arguments.retrieveDefaultValues(syntax.getDefaultValuesMap());
executor.apply(source, arguments);
}
} else {
// The executor is probably the default one
executor.apply(source, arguments);
}
} else if (callback != null) {
} else if (callback != null && argumentSyntaxException != null) {
// No syntax has been validated but the faulty argument with a callback has been found
// Execute the faulty argument callback
callback.apply(source, value, error);
callback.apply(source, argumentSyntaxException);
}
}

View File

@ -6,6 +6,8 @@ import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
/**
* Represents a syntax in {@link Command}
* which is initialized with {@link Command#addSyntax(CommandExecutor, Argument[])}.
@ -14,16 +16,27 @@ public class CommandSyntax {
private CommandCondition commandCondition;
private CommandExecutor executor;
private final Map<String, Object> defaultValuesMap;
private final Argument<?>[] args;
protected CommandSyntax(@Nullable CommandCondition commandCondition,
@NotNull CommandExecutor commandExecutor,
@Nullable Map<String, Object> defaultValuesMap,
@NotNull Argument<?>... args) {
this.commandCondition = commandCondition;
this.executor = commandExecutor;
this.defaultValuesMap = defaultValuesMap;
this.args = args;
}
protected CommandSyntax(@Nullable CommandCondition commandCondition,
@NotNull CommandExecutor commandExecutor,
@NotNull Argument<?>... args) {
this(commandCondition, commandExecutor, null, args);
}
/**
* Gets the condition to use this syntax.
*
@ -66,6 +79,11 @@ public class CommandSyntax {
this.executor = executor;
}
@Nullable
protected Map<String, Object> getDefaultValuesMap() {
return defaultValuesMap;
}
/**
* Gets all the required {@link Argument} for this syntax.
*

View File

@ -3,6 +3,7 @@ package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.builder.ArgumentCallback;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandExecutor;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -11,24 +12,20 @@ import org.jetbrains.annotations.Nullable;
* <p>
* You can create your own with your own special conditions.
* <p>
* Here in order, how is parsed an argument: {@link #getCorrectionResult(String)} to check
* if the syntax is correct, {@link #parse(String)} to convert the correct argument
* and {@link #getConditionResult(Object)} to verify that the parsed object validate the additional
* conditions.
* Arguments are parsed using {@link #parse(String)}.
*
* @param <T> the type of this parsed argument
*/
public abstract class Argument<T> {
public static final int SUCCESS = 0;
public static final int UNDEFINED_ERROR = -1;
private final String id;
private final boolean allowSpace;
private final boolean useRemaining;
private ArgumentCallback callback;
private T defaultValue;
/**
* Creates a new argument.
*
@ -62,38 +59,15 @@ public abstract class Argument<T> {
}
/**
* First method called to check the validity of an input.
* <p>
* If {@link #allowSpace()} is enabled, the value will be incremented by the next word until it returns {@link #SUCCESS},
* meaning that you need to be sure to check the inexpensive operations first (eg the number of brackets, the first and last char, etc...).
* Parses the given input, and throw an {@link ArgumentSyntaxException}
* if the input cannot be convert to {@code T}
*
* @param value The received argument
* @return the error code or {@link #SUCCESS}
*/
public abstract int getCorrectionResult(@NotNull String value);
/**
* Called after {@link #getCorrectionResult(String)} returned {@link #SUCCESS}.
* <p>
* The correction being correct means that {@code value} shouldn't be verified again, you can assume that no exception will occur
* when converting it to the correct type.
*
* @param value The correct argument which does not need to be verified again
* @return The parsed argument
* @param input the argument to parse
* @return the parsed argument
* @throws ArgumentSyntaxException if {@code value} is not valid
*/
@NotNull
public abstract T parse(@NotNull String value);
/**
* Called after {@link #parse(String)} meaning that {@code value} should already represent a valid representation of the input.
* <p>
* The condition result has for goal to check the optional conditions that are user configurable (eg min/max values for a number, a specific material
* for an item, etc...).
*
* @param value The parsed argument
* @return the error code or {@link #SUCCESS}
*/
public abstract int getConditionResult(@NotNull T value);
public abstract T parse(@NotNull String input) throws ArgumentSyntaxException;
/**
* Gets the ID of the argument, showed in-game above the chat bar
@ -146,6 +120,43 @@ public abstract class Argument<T> {
this.callback = callback;
}
/**
* Gets if this argument is 'optional'.
* <p>
* Optional means that this argument can be put at the end of a syntax
* and obtains a default value ({@link #getDefaultValue()}).
*
* @return true if this argument is considered optional
*/
public boolean isOptional() {
return defaultValue != null;
}
/**
* Gets the default value of this argument.
*
* @return the argument default value, null if the argument is not optional
*/
@Nullable
public T getDefaultValue() {
return defaultValue;
}
/**
* Sets the default value of the argument.
* <p>
* A non-null value means that the argument can be put at the end of a syntax
* to act as an optional one.
*
* @param defaultValue the default argument value, null to make the argument non-optional
* @return 'this' for chaining
*/
@NotNull
public Argument<T> setDefaultValue(@Nullable T defaultValue) {
this.defaultValue = defaultValue;
return this;
}
/**
* Gets if the argument has any error callback.
*

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
/**
@ -15,21 +16,15 @@ public class ArgumentBoolean extends Argument<Boolean> {
super(id);
}
@Override
public int getCorrectionResult(@NotNull String value) {
return (value.equalsIgnoreCase("true")
|| value.equalsIgnoreCase("false")) ? SUCCESS : NOT_BOOLEAN_ERROR;
}
@NotNull
@Override
public Boolean parse(@NotNull String value) {
return Boolean.parseBoolean(value);
}
public Boolean parse(@NotNull String input) throws ArgumentSyntaxException {
if (input.equalsIgnoreCase("true"))
return true;
if (input.equalsIgnoreCase("false"))
return false;
@Override
public int getConditionResult(@NotNull Boolean value) {
return SUCCESS;
throw new ArgumentSyntaxException("Not a boolean", input, NOT_BOOLEAN_ERROR);
}
}

View File

@ -1,14 +1,15 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.callback.validator.StringArrayValidator;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Pattern;
/**
* Same as {@link ArgumentStringArray} with the exception
* that this argument can trigger {@link net.minestom.server.command.builder.Command#onDynamicWrite(String)}.
* that this argument can trigger {@link net.minestom.server.command.builder.Command#onDynamicWrite(CommandSender, String)}.
*/
public class ArgumentDynamicStringArray extends Argument<String[]> {
@ -20,28 +21,19 @@ public class ArgumentDynamicStringArray extends Argument<String[]> {
super(id, true, true);
}
@Override
public int getCorrectionResult(@NotNull String value) {
return SUCCESS;
}
@NotNull
@Override
public String[] parse(@NotNull String value) {
return value.split(Pattern.quote(" "));
}
@Override
public int getConditionResult(@NotNull String[] value) {
public String[] parse(@NotNull String input) throws ArgumentSyntaxException {
final String[] value = input.split(StringUtils.SPACE);
// true if 'value' is valid based on the dynamic restriction
final boolean restrictionCheck = dynamicRestriction == null || dynamicRestriction.isValid(value);
if (!restrictionCheck) {
return RESTRICTION_ERROR;
throw new ArgumentSyntaxException("Argument does not respect the dynamic restriction", input, RESTRICTION_ERROR);
}
return SUCCESS;
return value;
}
/**

View File

@ -1,13 +1,16 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.arguments.minecraft.SuggestionType;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.callback.validator.StringValidator;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Same as {@link ArgumentWord} with the exception
* that this argument can trigger {@link net.minestom.server.command.builder.Command#onDynamicWrite(String)}
* that this argument can trigger {@link net.minestom.server.command.builder.Command#onDynamicWrite(CommandSender, String)}
* when the suggestion type is {@link SuggestionType#ASK_SERVER}, or any other suggestions available in the enum.
*/
public class ArgumentDynamicWord extends Argument<String> {
@ -24,31 +27,20 @@ public class ArgumentDynamicWord extends Argument<String> {
this.suggestionType = suggestionType;
}
@Override
public int getCorrectionResult(@NotNull String value) {
if (value.contains(" "))
return SPACE_ERROR;
return SUCCESS;
}
@NotNull
@Override
public String parse(@NotNull String value) {
return value;
}
@Override
public int getConditionResult(@NotNull String value) {
public String parse(@NotNull String input) throws ArgumentSyntaxException {
if (input.contains(StringUtils.SPACE))
throw new ArgumentSyntaxException("Word cannot contain space characters", input, SPACE_ERROR);
// true if 'value' is valid based on the dynamic restriction
final boolean restrictionCheck = dynamicRestriction == null || dynamicRestriction.isValid(value);
final boolean restrictionCheck = dynamicRestriction == null || dynamicRestriction.isValid(input);
if (!restrictionCheck) {
return RESTRICTION_ERROR;
throw new ArgumentSyntaxException("Word does not respect the dynamic restriction", input, RESTRICTION_ERROR);
}
return SUCCESS;
return input;
}
/**

View File

@ -1,5 +1,7 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
/**
@ -15,45 +17,35 @@ public class ArgumentString extends Argument<String> {
super(id, true);
}
@NotNull
@Override
public int getCorrectionResult(@NotNull String value) {
public String parse(@NotNull String input) throws ArgumentSyntaxException {
return staticParse(input);
}
@NotNull
public static String staticParse(@NotNull String input) throws ArgumentSyntaxException {
// Check if value start and end with quote
final char first = value.charAt(0);
final char last = value.charAt(value.length() - 1);
final char first = input.charAt(0);
final char last = input.charAt(input.length() - 1);
final boolean quote = first == '\"' && last == '\"';
if (!quote)
return QUOTE_ERROR;
throw new ArgumentSyntaxException("String argument needs to start and end with quotes", input, QUOTE_ERROR);
for (int i = 1; i < value.length(); i++) {
final char c = value.charAt(i);
// Remove first and last characters (quotes)
input = input.substring(1, input.length() - 1);
// Verify backslashes
for (int i = 1; i < input.length(); i++) {
final char c = input.charAt(i);
if (c == '\"') {
final char lastChar = value.charAt(i - 1);
if (lastChar == '\\') {
continue;
} else if (i == value.length() - 1) {
return SUCCESS;
final char lastChar = input.charAt(i - 1);
if (lastChar != '\\') {
throw new ArgumentSyntaxException("Non-escaped quote", input, QUOTE_ERROR);
}
}
}
// Last quote is written like \"
return QUOTE_ERROR;
}
@NotNull
@Override
public String parse(@NotNull String value) {
// Remove first and last characters (quote)
value = value.substring(1, value.length() - 1);
// Remove all backslashes
value = value.replace("\\", "");
return value;
}
@Override
public int getConditionResult(@NotNull String value) {
return SUCCESS;
return StringEscapeUtils.unescapeJava(input);
}
}

View File

@ -1,5 +1,7 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Pattern;
@ -15,19 +17,9 @@ public class ArgumentStringArray extends Argument<String[]> {
super(id, true, true);
}
@Override
public int getCorrectionResult(@NotNull String value) {
return SUCCESS;
}
@NotNull
@Override
public String[] parse(@NotNull String value) {
return value.split(Pattern.quote(" "));
}
@Override
public int getConditionResult(@NotNull String[] value) {
return SUCCESS;
public String[] parse(@NotNull String input) throws ArgumentSyntaxException {
return input.split(Pattern.quote(StringUtils.SPACE));
}
}

View File

@ -100,9 +100,8 @@ public class ArgumentType {
return new ArgumentFloatRange(id);
}
@Deprecated
public static ArgumentEntities Entities(@NotNull String id) {
return new ArgumentEntities(id);
public static ArgumentEntity Entities(@NotNull String id) {
return new ArgumentEntity(id);
}
public static ArgumentItemStack ItemStack(@NotNull String id) {

View File

@ -1,5 +1,8 @@
package net.minestom.server.command.builder.arguments;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.validate.Check;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -28,41 +31,41 @@ public class ArgumentWord extends Argument<String> {
* <p>
* WARNING: having an array too long would result in a packet too big or the client being stuck during login.
*
* @param restrictions the accepted words
* @return 'this'
* @param restrictions the accepted words,
* can be null but if an array is passed
* you need to ensure that it is filled with non-null values
* @return 'this' for chaining
* @throws NullPointerException if {@code restrictions} is not null but contains null value(s)
*/
@NotNull
public ArgumentWord from(@Nullable String... restrictions) {
if (restrictions != null) {
for (String restriction : restrictions) {
Check.notNull(restriction,
"ArgumentWord restriction cannot be null, you can pass 'null' instead of an empty array");
}
}
this.restrictions = restrictions;
return this;
}
@Override
public int getCorrectionResult(@NotNull String value) {
if (value.contains(" "))
return SPACE_ERROR;
return SUCCESS;
}
@NotNull
@Override
public String parse(@NotNull String value) {
return value;
}
public String parse(@NotNull String input) throws ArgumentSyntaxException {
if (input.contains(StringUtils.SPACE))
throw new ArgumentSyntaxException("Word cannot contain space character", input, SPACE_ERROR);
@Override
public int getConditionResult(@NotNull String value) {
// Check restrictions
// Check restrictions (acting as literal)
if (hasRestrictions()) {
for (String r : restrictions) {
if (value.equalsIgnoreCase(r))
return SUCCESS;
if (input.equalsIgnoreCase(r))
return input;
}
return RESTRICTION_ERROR;
throw new ArgumentSyntaxException("Word needs to be in the restriction list", input, RESTRICTION_ERROR);
}
return SUCCESS;
return input;
}
/**

View File

@ -2,6 +2,7 @@ package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.chat.ChatColor;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
/**
@ -17,20 +18,13 @@ public class ArgumentColor extends Argument<ChatColor> {
super(id);
}
@Override
public int getCorrectionResult(@NotNull String value) {
final ChatColor color = ChatColor.fromName(value);
return color == ChatColor.NO_COLOR ? UNDEFINED_COLOR : SUCCESS;
}
@NotNull
@Override
public ChatColor parse(@NotNull String value) {
return ChatColor.fromName(value);
}
public ChatColor parse(@NotNull String input) throws ArgumentSyntaxException {
final ChatColor color = ChatColor.fromName(input);
if (color == ChatColor.NO_COLOR)
throw new ArgumentSyntaxException("Undefined color", input, UNDEFINED_COLOR);
@Override
public int getConditionResult(@NotNull ChatColor value) {
return SUCCESS;
return color;
}
}

View File

@ -1,134 +0,0 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.entity.Entity;
import net.minestom.server.network.ConnectionManager;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// TODO
/**
* Represents the target selector argument.
* https://minecraft.gamepedia.com/Commands#Target_selectors
*/
public class ArgumentEntities extends Argument<ArrayList<Entity>> {
public static final int INVALID_SYNTAX = -2;
public static final int ONLY_SINGLE_ENTITY_ERROR = -3;
public static final int ONLY_PLAYERS_ERROR = -4;
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private static final List<String> selectorVariables = Arrays.asList("@p", "@r", "@a", "@e", "@s");
private static final List<String> playersOnlySelector = Arrays.asList("@p", "@r", "@a", "@s");
private static final List<String> singleOnlySelector = Arrays.asList("@p", "@r", "@s");
private static final List<String> validArguments = Arrays.asList("x", "y", "z",
"distance", "dx", "dy", "dz",
"scores", "tag", "team", "limit", "sort", "level", "gamemode", "name",
"x_rotation", "y_rotation", "type", "nbt", "advancements", "predicate");
private boolean onlySingleEntity;
private boolean onlyPlayers;
public ArgumentEntities(String id) {
super(id, true);
}
public ArgumentEntities singleEntity(boolean singleEntity) {
this.onlySingleEntity = singleEntity;
return this;
}
public ArgumentEntities onlyPlayers(boolean onlyPlayers) {
this.onlyPlayers = onlyPlayers;
return this;
}
@Override
public int getCorrectionResult(@NotNull String value) {
System.out.println("check: " + value);
// Check for raw player name
if (value.length() <= 16) {
if (CONNECTION_MANAGER.getPlayer(value) != null)
return SUCCESS;
}
// The minimum size is always 0 (for the selector variable, ex: @p)
if (value.length() < 2)
return INVALID_SYNTAX;
// The target selector variable always start by '@'
if (!value.startsWith("@"))
return INVALID_SYNTAX;
final String selectorVariable = value.substring(0, 2);
// Check if the selector variable used exists
if (!selectorVariables.contains(selectorVariable))
return INVALID_SYNTAX;
// Check if it should only select single entity and if the selector variable valid the condition
if (onlySingleEntity && !singleOnlySelector.contains(selectorVariable))
return ONLY_SINGLE_ENTITY_ERROR;
// Check if it should only select players and if the selector variable valid the condition
if (onlyPlayers && !playersOnlySelector.contains(selectorVariable))
return ONLY_PLAYERS_ERROR;
// The selector is a single selector variable which verify all the conditions
if (value.length() == 2)
return SUCCESS;
// START PARSING THE STRUCTURE
final String structure = value.substring(2);
// The structure isn't opened or closed properly
if (!structure.startsWith("[") || !structure.endsWith("]"))
return INVALID_SYNTAX;
final String structureData = structure.substring(1, structure.length() - 1);
String currentArgument = "";
for (int i = 0; i < structureData.length(); i++) {
final char c = structureData.charAt(i);
if (c == '=') {
i = retrieveArgument(structureData, currentArgument, i);
} else {
currentArgument += c;
}
}
return 0;
}
private int retrieveArgument(String structureData, String argument, int index) {
int finalIndex = index;
for (int i = index + 1; i < structureData.length(); i++) {
System.out.println("char: " + structureData.charAt(i));
System.out.println("retrieve: " + argument);
}
return finalIndex;
}
@NotNull
@Override
public ArrayList<Entity> parse(@NotNull String value) {
return null;
}
@Override
public int getConditionResult(@NotNull ArrayList<Entity> value) {
return SUCCESS;
}
public boolean isOnlySingleEntity() {
return onlySingleEntity;
}
public boolean isOnlyPlayers() {
return onlyPlayers;
}
}

View File

@ -0,0 +1,251 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.GameMode;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.registry.Registries;
import net.minestom.server.utils.entity.EntityFinder;
import net.minestom.server.utils.math.IntRange;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
// TODO
/**
* Represents the target selector argument.
* https://minecraft.gamepedia.com/Commands#Target_selectors
*/
public class ArgumentEntity extends Argument<EntityFinder> {
public static final int INVALID_SYNTAX = -2;
public static final int ONLY_SINGLE_ENTITY_ERROR = -3;
public static final int ONLY_PLAYERS_ERROR = -4;
public static final int INVALID_ARGUMENT_NAME = -5;
public static final int INVALID_ARGUMENT_VALUE = -6;
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private static final List<String> selectorVariables = Arrays.asList("@p", "@r", "@a", "@e", "@s");
private static final List<String> playersOnlySelector = Arrays.asList("@p", "@r", "@a", "@s");
private static final List<String> singleOnlySelector = Arrays.asList("@p", "@r", "@s");
// List with all the valid arguments
private static final List<String> validArguments = Arrays.asList(
"x", "y", "z",
"distance", "dx", "dy", "dz",
"scores", "tag", "team", "limit", "sort", "level", "gamemode", "name",
"x_rotation", "y_rotation", "type", "nbt", "advancements", "predicate");
// List with all the easily parsable arguments which only require reading until a specific character (comma)
private static final List<String> simpleArguments = Arrays.asList(
"x", "y", "z",
"distance", "dx", "dy", "dz",
"scores", "tag", "team", "limit", "sort", "level", "gamemode",
"x_rotation", "y_rotation", "type", "advancements", "predicate");
private boolean onlySingleEntity;
private boolean onlyPlayers;
public ArgumentEntity(String id) {
super(id, true);
}
public ArgumentEntity singleEntity(boolean singleEntity) {
this.onlySingleEntity = singleEntity;
return this;
}
public ArgumentEntity onlyPlayers(boolean onlyPlayers) {
this.onlyPlayers = onlyPlayers;
return this;
}
@NotNull
@Override
public EntityFinder parse(@NotNull String input) throws ArgumentSyntaxException {
return staticParse(input, onlySingleEntity, onlyPlayers);
}
@NotNull
public static EntityFinder staticParse(@NotNull String input,
boolean onlySingleEntity, boolean onlyPlayers) throws ArgumentSyntaxException {
// Check for raw player name
if (input.length() <= 16) {
if (CONNECTION_MANAGER.getPlayer(input) != null) {
return new EntityFinder()
.setTargetSelector(EntityFinder.TargetSelector.ALL_PLAYERS)
.setName(input, EntityFinder.ToggleableType.INCLUDE);
}
}
// The minimum size is always 0 (for the selector variable, ex: @p)
if (input.length() < 2)
throw new ArgumentSyntaxException("Length needs to be > 1", input, INVALID_SYNTAX);
// The target selector variable always start by '@'
if (!input.startsWith("@"))
throw new ArgumentSyntaxException("Target selector needs to start with @", input, INVALID_SYNTAX);
final String selectorVariable = input.substring(0, 2);
// Check if the selector variable used exists
if (!selectorVariables.contains(selectorVariable))
throw new ArgumentSyntaxException("Invalid selector variable", input, INVALID_SYNTAX);
// Check if it should only select single entity and if the selector variable valid the condition
if (onlySingleEntity && !singleOnlySelector.contains(selectorVariable))
throw new ArgumentSyntaxException("Argument requires only a single entity", input, ONLY_SINGLE_ENTITY_ERROR);
// Check if it should only select players and if the selector variable valid the condition
if (onlyPlayers && !playersOnlySelector.contains(selectorVariable))
throw new ArgumentSyntaxException("Argument requires only players", input, ONLY_PLAYERS_ERROR);
// Create the EntityFinder which will be used for the rest of the parsing
final EntityFinder entityFinder = new EntityFinder()
.setTargetSelector(toTargetSelector(selectorVariable));
// The selector is a single selector variable which verify all the conditions
if (input.length() == 2)
return entityFinder;
// START PARSING THE STRUCTURE
final String structure = input.substring(2);
return parseStructure(input, entityFinder, structure);
}
@NotNull
private static EntityFinder parseStructure(@NotNull String input,
@NotNull EntityFinder entityFinder,
@NotNull String structure) throws ArgumentSyntaxException {
// The structure isn't opened or closed properly
if (!structure.startsWith("[") || !structure.endsWith("]"))
throw new ArgumentSyntaxException("Target selector needs to start and end with brackets", input, INVALID_SYNTAX);
// Remove brackets
final String structureData = structure.substring(1, structure.length() - 1);
//System.out.println("structure data: " + structureData);
String currentArgument = "";
for (int i = 0; i < structureData.length(); i++) {
final char c = structureData.charAt(i);
if (c == '=') {
// Replace all unnecessary spaces
currentArgument = currentArgument.trim();
if (!validArguments.contains(currentArgument))
throw new ArgumentSyntaxException("Argument name '" + currentArgument + "' does not exist", input, INVALID_ARGUMENT_NAME);
i = parseArgument(entityFinder, currentArgument, input, structureData, i);
currentArgument = ""; // Reset current argument
} else {
currentArgument += c;
}
}
return entityFinder;
}
private static int parseArgument(@NotNull EntityFinder entityFinder,
@NotNull String argumentName,
@NotNull String input,
@NotNull String structureData, int beginIndex) throws ArgumentSyntaxException {
final char comma = ',';
final boolean isSimple = simpleArguments.contains(argumentName);
int finalIndex = beginIndex + 1;
StringBuilder valueBuilder = new StringBuilder();
for (; finalIndex < structureData.length(); finalIndex++) {
final char c = structureData.charAt(finalIndex);
// Command is parsed
if (isSimple && c == comma)
break;
valueBuilder.append(c);
}
final String value = valueBuilder.toString().trim();
//System.out.println("value: " + value);
switch (argumentName) {
case "type": {
final boolean include = !value.startsWith("!");
final String entityName = include ? value : value.substring(1);
final EntityType entityType = Registries.getEntityType(entityName);
if (entityType == null)
throw new ArgumentSyntaxException("Invalid entity name", input, INVALID_ARGUMENT_VALUE);
entityFinder.setEntity(entityType, include ? EntityFinder.ToggleableType.INCLUDE : EntityFinder.ToggleableType.EXCLUDE);
break;
}
case "gamemode": {
final boolean include = !value.startsWith("!");
final String gameModeName = include ? value : value.substring(1);
try {
final GameMode gameMode = GameMode.valueOf(gameModeName);
entityFinder.setGameMode(gameMode, include ? EntityFinder.ToggleableType.INCLUDE : EntityFinder.ToggleableType.EXCLUDE);
} catch (IllegalArgumentException e) {
throw new ArgumentSyntaxException("Invalid entity game mode", input, INVALID_ARGUMENT_VALUE);
}
break;
}
case "limit":
try {
final int limit = Integer.parseInt(value);
entityFinder.setLimit(limit);
} catch (NumberFormatException e) {
throw new ArgumentSyntaxException("Invalid limit number", input, INVALID_ARGUMENT_VALUE);
}
break;
case "sort":
try {
EntityFinder.EntitySort entitySort = EntityFinder.EntitySort.valueOf(value.toUpperCase());
entityFinder.setEntitySort(entitySort);
} catch (IllegalArgumentException e) {
throw new ArgumentSyntaxException("Invalid entity sort", input, INVALID_ARGUMENT_VALUE);
}
break;
case "level":
try {
final IntRange level = ArgumentIntRange.staticParse(value);
entityFinder.setLevel(level);
} catch (ArgumentSyntaxException e) {
throw new ArgumentSyntaxException("Invalid level number", input, INVALID_ARGUMENT_VALUE);
}
break;
case "distance":
try {
final IntRange distance = ArgumentIntRange.staticParse(value);
entityFinder.setDistance(distance);
} catch (ArgumentSyntaxException e) {
throw new ArgumentSyntaxException("Invalid level number", input, INVALID_ARGUMENT_VALUE);
}
break;
}
return finalIndex;
}
public boolean isOnlySingleEntity() {
return onlySingleEntity;
}
public boolean isOnlyPlayers() {
return onlyPlayers;
}
private static EntityFinder.TargetSelector toTargetSelector(@NotNull String selectorVariable) {
if (selectorVariable.equals("@p"))
return EntityFinder.TargetSelector.NEAREST_PLAYER;
if (selectorVariable.equals("@r"))
return EntityFinder.TargetSelector.RANDOM_PLAYER;
if (selectorVariable.equals("@a"))
return EntityFinder.TargetSelector.ALL_PLAYERS;
if (selectorVariable.equals("@e"))
return EntityFinder.TargetSelector.ALL_ENTITIES;
if (selectorVariable.equals("@s"))
return EntityFinder.TargetSelector.SELF;
throw new IllegalStateException("Weird selector variable: " + selectorVariable);
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.math.FloatRange;
import org.jetbrains.annotations.NotNull;
@ -16,63 +17,39 @@ public class ArgumentFloatRange extends ArgumentRange<FloatRange> {
super(id);
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
Float.valueOf(value);
return SUCCESS; // Is a single number
} catch (NumberFormatException e) {
String[] split = value.split(Pattern.quote(".."));
if (split.length == 1) {
try {
Float.valueOf(split[0]);
return SUCCESS;
} catch (NumberFormatException e2) {
return FORMAT_ERROR;
}
} else if (split.length == 2) {
try {
Float.valueOf(split[0]); // min
Float.valueOf(split[1]); // max
return SUCCESS;
} catch (NumberFormatException e2) {
return FORMAT_ERROR;
}
} else {
return FORMAT_ERROR;
}
}
}
@NotNull
@Override
public FloatRange parse(@NotNull String value) {
if (value.contains("..")) {
final int index = value.indexOf('.');
final String[] split = value.split(Pattern.quote(".."));
public FloatRange parse(@NotNull String input) throws ArgumentSyntaxException {
try {
if (input.contains("..")) {
final int index = input.indexOf('.');
final String[] split = input.split(Pattern.quote(".."));
final float min;
final float max;
if (index == 0) {
// Format ..NUMBER
min = Float.MIN_VALUE;
max = Float.parseFloat(split[0]);
} else {
if (split.length == 2) {
// Format NUMBER..NUMBER
min = Float.parseFloat(split[0]);
max = Float.parseFloat(split[1]);
final float min;
final float max;
if (index == 0) {
// Format ..NUMBER
min = Float.MIN_VALUE;
max = Float.parseFloat(split[0]);
} else {
// Format NUMBER..
min = Float.parseFloat(split[0]);
max = Float.MAX_VALUE;
if (split.length == 2) {
// Format NUMBER..NUMBER
min = Float.parseFloat(split[0]);
max = Float.parseFloat(split[1]);
} else {
// Format NUMBER..
min = Float.parseFloat(split[0]);
max = Float.MAX_VALUE;
}
}
}
return new FloatRange(min, max);
} else {
final float number = Float.parseFloat(value);
return new FloatRange(number);
return new FloatRange(min, max);
} else {
final float number = Float.parseFloat(input);
return new FloatRange(number);
}
} catch (NumberFormatException e2) {
throw new ArgumentSyntaxException("Invalid number", input, FORMAT_ERROR);
}
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.math.IntRange;
import org.jetbrains.annotations.NotNull;
@ -16,63 +17,44 @@ public class ArgumentIntRange extends ArgumentRange<IntRange> {
super(id);
}
@NotNull
@Override
public int getCorrectionResult(@NotNull String value) {
try {
Integer.valueOf(value);
return SUCCESS; // Is a single number
} catch (NumberFormatException e) {
String[] split = value.split(Pattern.quote(".."));
if (split.length == 1) {
try {
Integer.valueOf(split[0]);
return SUCCESS;
} catch (NumberFormatException e2) {
return FORMAT_ERROR;
}
} else if (split.length == 2) {
try {
Integer.valueOf(split[0]); // min
Integer.valueOf(split[1]); // max
return SUCCESS;
} catch (NumberFormatException e2) {
return FORMAT_ERROR;
}
} else {
return FORMAT_ERROR;
}
}
public IntRange parse(@NotNull String input) throws ArgumentSyntaxException {
return staticParse(input);
}
@NotNull
@Override
public IntRange parse(@NotNull String value) {
if (value.contains("..")) {
final int index = value.indexOf('.');
final String[] split = value.split(Pattern.quote(".."));
public static IntRange staticParse(@NotNull String input) throws ArgumentSyntaxException {
try {
if (input.contains("..")) {
final int index = input.indexOf('.');
final String[] split = input.split(Pattern.quote(".."));
final int min;
final int max;
if (index == 0) {
// Format ..NUMBER
min = Integer.MIN_VALUE;
max = Integer.parseInt(split[0]);
} else {
if (split.length == 2) {
// Format NUMBER..NUMBER
min = Integer.parseInt(split[0]);
max = Integer.parseInt(split[1]);
final int min;
final int max;
if (index == 0) {
// Format ..NUMBER
min = Integer.MIN_VALUE;
max = Integer.parseInt(split[0]);
} else {
// Format NUMBER..
min = Integer.parseInt(split[0]);
max = Integer.MAX_VALUE;
if (split.length == 2) {
// Format NUMBER..NUMBER
min = Integer.parseInt(split[0]);
max = Integer.parseInt(split[1]);
} else {
// Format NUMBER..
min = Integer.parseInt(split[0]);
max = Integer.MAX_VALUE;
}
}
}
return new IntRange(min, max);
} else {
final int number = Integer.parseInt(value);
return new IntRange(number);
return new IntRange(min, max);
} else {
final int number = Integer.parseInt(input);
return new IntRange(number);
}
} catch (NumberFormatException e2) {
throw new ArgumentSyntaxException("Invalid number", input, FORMAT_ERROR);
}
}
}

View File

@ -1,12 +1,12 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.registry.Registries;
import net.minestom.server.utils.NBTUtils;
import org.jetbrains.annotations.NotNull;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.jglrxavpok.hephaistos.nbt.NBTException;
import org.jglrxavpok.hephaistos.nbt.SNBTParser;
@ -29,63 +29,36 @@ public class ArgumentItemStack extends Argument<ItemStack> {
super(id, true);
}
@Override
public int getCorrectionResult(@NotNull String value) {
if (value.startsWith("{")) {
return NO_MATERIAL;
}
final int nbtIndex = value.indexOf("{");
if (nbtIndex == -1 && !value.contains(" ")) {
// Only item name
return SUCCESS;
} else {
// has nbt
final String sNBT = value.substring(nbtIndex);
try {
NBT nbt = new SNBTParser(new StringReader(sNBT)).parse();
return nbt instanceof NBTCompound ? SUCCESS : INVALID_NBT;
} catch (NBTException e) {
return INVALID_NBT;
}
}
}
@NotNull
@Override
public ItemStack parse(@NotNull String value) {
final int nbtIndex = value.indexOf("{");
public ItemStack parse(@NotNull String input) throws ArgumentSyntaxException {
final int nbtIndex = input.indexOf("{");
if (nbtIndex == 0)
throw new ArgumentSyntaxException("The item needs a material", input, NO_MATERIAL);
if (nbtIndex == -1) {
// Only item name
final Material material = Registries.getMaterial(value);
final Material material = Registries.getMaterial(input);
return new ItemStack(material, (byte) 1);
} else {
final String materialName = value.substring(0, nbtIndex);
final String materialName = input.substring(0, nbtIndex);
final Material material = Registries.getMaterial(materialName);
ItemStack itemStack = new ItemStack(material, (byte) 1);
final String sNBT = value.substring(nbtIndex).replace("\\\"", "\"");
final String sNBT = input.substring(nbtIndex).replace("\\\"", "\"");
NBTCompound compound = null;
NBTCompound compound;
try {
compound = (NBTCompound) new SNBTParser(new StringReader(sNBT)).parse();
} catch (NBTException e) {
e.printStackTrace();
throw new ArgumentSyntaxException("Item NBT is invalid", input, INVALID_NBT);
}
assert compound != null;
NBTUtils.loadDataIntoItem(itemStack, compound);
return itemStack;
}
}
@Override
public int getConditionResult(@NotNull ItemStack value) {
return SUCCESS;
}
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
@ -22,29 +23,18 @@ public class ArgumentNbtCompoundTag extends Argument<NBTCompound> {
super(id, true);
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
NBT nbt = new SNBTParser(new StringReader(value)).parse();
return nbt instanceof NBTCompound ? SUCCESS : INVALID_NBT;
} catch (NBTException e) {
return INVALID_NBT;
}
}
@NotNull
@Override
public NBTCompound parse(@NotNull String value) {
public NBTCompound parse(@NotNull String input) throws ArgumentSyntaxException {
try {
NBT nbt = new SNBTParser(new StringReader(value)).parse();
NBT nbt = new SNBTParser(new StringReader(input)).parse();
if (!(nbt instanceof NBTCompound))
throw new ArgumentSyntaxException("NBTCompound is invalid", input, INVALID_NBT);
return (NBTCompound) nbt;
} catch (NBTException e) {
return null;
throw new ArgumentSyntaxException("NBTCompound is invalid", input, INVALID_NBT);
}
}
@Override
public int getConditionResult(@NotNull NBTCompound value) {
return SUCCESS;
}
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTException;
@ -23,28 +24,13 @@ public class ArgumentNbtTag extends Argument<NBT> {
super(id, true);
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
NBT nbt = new SNBTParser(new StringReader(value)).parse();
return nbt != null ? SUCCESS : INVALID_NBT;
} catch (NBTException e) {
return INVALID_NBT;
}
}
@NotNull
@Override
public NBT parse(@NotNull String value) {
public NBT parse(@NotNull String input) throws ArgumentSyntaxException {
try {
return new SNBTParser(new StringReader(value)).parse();
return new SNBTParser(new StringReader(input)).parse();
} catch (NBTException e) {
return null;
throw new ArgumentSyntaxException("Invalid NBT", input, INVALID_NBT);
}
}
@Override
public int getConditionResult(@NotNull NBT value) {
return SUCCESS;
}
}

View File

@ -1,7 +1,6 @@
package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.arguments.Argument;
import org.jetbrains.annotations.NotNull;
/**
* Abstract class used by {@link ArgumentIntRange} and {@link ArgumentFloatRange}.
@ -15,9 +14,4 @@ public abstract class ArgumentRange<T> extends Argument<T> {
public ArgumentRange(String id) {
super(id);
}
@Override
public int getConditionResult(@NotNull T value) {
return SUCCESS;
}
}

View File

@ -3,6 +3,7 @@ package net.minestom.server.command.builder.arguments.minecraft;
import it.unimi.dsi.fastutil.chars.CharArrayList;
import it.unimi.dsi.fastutil.chars.CharList;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
import org.jetbrains.annotations.NotNull;
@ -23,43 +24,31 @@ public class ArgumentTime extends Argument<UpdateOption> {
super(id);
}
@Override
public int getCorrectionResult(@NotNull String value) {
final char lastChar = value.charAt(value.length() - 1);
if (!SUFFIXES.contains(lastChar))
return INVALID_TIME_FORMAT;
value = value.substring(0, value.length() - 1);
try {
// Check if value is a number
Integer.parseInt(value);
return SUCCESS;
} catch (NumberFormatException e) {
return NO_NUMBER;
}
}
@NotNull
@Override
public UpdateOption parse(@NotNull String value) {
final char lastChar = value.charAt(value.length() - 1);
TimeUnit timeUnit = null;
if (lastChar == 'd') {
timeUnit = TimeUnit.DAY;
} else if (lastChar == 's') {
timeUnit = TimeUnit.SECOND;
} else if (lastChar == 't') {
timeUnit = TimeUnit.TICK;
public UpdateOption parse(@NotNull String input) throws ArgumentSyntaxException {
final char lastChar = input.charAt(input.length() - 1);
if (!SUFFIXES.contains(lastChar))
throw new ArgumentSyntaxException("Time format is invalid", input, INVALID_TIME_FORMAT);
// Remove last char
input = input.substring(0, input.length() - 1);
try {
// Check if value is a number
final int time = Integer.parseInt(input);
TimeUnit timeUnit = null;
if (lastChar == 'd') {
timeUnit = TimeUnit.DAY;
} else if (lastChar == 's') {
timeUnit = TimeUnit.SECOND;
} else if (lastChar == 't') {
timeUnit = TimeUnit.TICK;
}
return new UpdateOption(time, timeUnit);
} catch (NumberFormatException e) {
throw new ArgumentSyntaxException("Time needs to be a number", input, NO_NUMBER);
}
value = value.substring(0, value.length() - 1);
final int time = Integer.parseInt(value);
return new UpdateOption(time, timeUnit);
}
@Override
public int getConditionResult(@NotNull UpdateOption value) {
return SUCCESS;
}
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.command.builder.arguments.minecraft.registry;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
public abstract class ArgumentRegistry<T> extends Argument<T> {
@ -13,19 +14,13 @@ public abstract class ArgumentRegistry<T> extends Argument<T> {
public abstract T getRegistry(@NotNull String value);
@Override
public int getCorrectionResult(@NotNull String value) {
return getRegistry(value) == null ? INVALID_NAME : SUCCESS;
}
@NotNull
@Override
public T parse(@NotNull String value) {
return getRegistry(value);
}
public T parse(@NotNull String input) throws ArgumentSyntaxException {
final T registryValue = getRegistry(input);
if (registryValue == null)
throw new ArgumentSyntaxException("Registry value is invalid", input, INVALID_NAME);
@Override
public int getConditionResult(@NotNull T value) {
return SUCCESS;
return registryValue;
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments.number;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
public class ArgumentDouble extends ArgumentNumber<Double> {
@ -10,44 +11,33 @@ public class ArgumentDouble extends ArgumentNumber<Double> {
this.max = Double.MAX_VALUE;
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
String parsed = parseValue(value);
int radix = getRadix(value);
if (radix != 10) {
Long.parseLong(parsed, radix);
} else {
Double.parseDouble(parsed);
}
return SUCCESS;
} catch (NumberFormatException | NullPointerException e) {
return NOT_NUMBER_ERROR;
}
}
@NotNull
@Override
public Double parse(@NotNull String value) {
String parsed = parseValue(value);
int radix = getRadix(value);
if (radix != 10) {
return (double) Long.parseLong(parsed, radix);
}
return Double.parseDouble(parsed);
}
public Double parse(@NotNull String input) throws ArgumentSyntaxException {
try {
final double value;
{
String parsed = parseValue(input);
int radix = getRadix(input);
if (radix != 10) {
value = (double) Long.parseLong(parsed, radix);
} else {
value = Double.parseDouble(parsed);
}
}
@Override
public int getConditionResult(@NotNull Double value) {
// Check range
if (hasMin && value < min) {
return RANGE_ERROR;
}
if (hasMax && value > max) {
return RANGE_ERROR;
}
// Check range
if (hasMin && value < min) {
throw new ArgumentSyntaxException("Input is lower than the minimum required value", input, RANGE_ERROR);
}
if (hasMax && value > max) {
throw new ArgumentSyntaxException("Input is higher than the minimum required value", input, RANGE_ERROR);
}
return SUCCESS;
return value;
} catch (NumberFormatException | NullPointerException e) {
throw new ArgumentSyntaxException("Input is not a number/long", input, NOT_NUMBER_ERROR);
}
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments.number;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
public class ArgumentFloat extends ArgumentNumber<Float> {
@ -10,44 +11,33 @@ public class ArgumentFloat extends ArgumentNumber<Float> {
this.max = Float.MAX_VALUE;
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
String parsed = parseValue(value);
int radix = getRadix(value);
if (radix != 10) {
Integer.parseInt(parsed, radix);
} else {
Float.parseFloat(parsed);
}
return SUCCESS;
} catch (NumberFormatException | NullPointerException e) {
return NOT_NUMBER_ERROR;
}
}
@NotNull
@Override
public Float parse(@NotNull String value) {
String parsed = parseValue(value);
int radix = getRadix(value);
if (radix != 10) {
return (float) Integer.parseInt(parsed, radix);
}
return Float.parseFloat(parsed);
}
public Float parse(@NotNull String input) throws ArgumentSyntaxException {
try {
final float value;
{
String parsed = parseValue(input);
int radix = getRadix(input);
if (radix != 10) {
value = (float) Integer.parseInt(parsed, radix);
} else {
value = Float.parseFloat(parsed);
}
}
@Override
public int getConditionResult(@NotNull Float value) {
// Check range
if (hasMin && value < min) {
return RANGE_ERROR;
}
if (hasMax && value > max) {
return RANGE_ERROR;
}
// Check range
if (hasMin && value < min) {
throw new ArgumentSyntaxException("Input is lower than the minimum required value", input, RANGE_ERROR);
}
if (hasMax && value > max) {
throw new ArgumentSyntaxException("Input is higher than the minimum required value", input, RANGE_ERROR);
}
return SUCCESS;
return value;
} catch (NumberFormatException | NullPointerException e) {
throw new ArgumentSyntaxException("Input is not a number/long", input, NOT_NUMBER_ERROR);
}
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments.number;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
public class ArgumentInteger extends ArgumentNumber<Integer> {
@ -10,33 +11,24 @@ public class ArgumentInteger extends ArgumentNumber<Integer> {
this.max = Integer.MAX_VALUE;
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
Integer.parseInt(parseValue(value), getRadix(value));
return SUCCESS;
} catch (NumberFormatException | NullPointerException e) {
return NOT_NUMBER_ERROR;
}
}
@NotNull
@Override
public Integer parse(@NotNull String value) {
return Integer.parseInt(parseValue(value), getRadix(value));
}
public Integer parse(@NotNull String input) throws ArgumentSyntaxException {
try {
final int value = Integer.parseInt(parseValue(input), getRadix(input));
@Override
public int getConditionResult(@NotNull Integer value) {
// Check range
if (hasMin && value < min) {
return RANGE_ERROR;
}
if (hasMax && value > max) {
return RANGE_ERROR;
}
// Check range
if (hasMin && value < min) {
throw new ArgumentSyntaxException("Input is lower than the minimum required value", input, RANGE_ERROR);
}
if (hasMax && value > max) {
throw new ArgumentSyntaxException("Input is higher than the minimum required value", input, RANGE_ERROR);
}
return SUCCESS;
return value;
} catch (NumberFormatException | NullPointerException e) {
throw new ArgumentSyntaxException("Input is not a number/long", input, NOT_NUMBER_ERROR);
}
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.command.builder.arguments.number;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull;
public class ArgumentLong extends ArgumentNumber<Long> {
@ -10,33 +11,24 @@ public class ArgumentLong extends ArgumentNumber<Long> {
this.max = Long.MAX_VALUE;
}
@Override
public int getCorrectionResult(@NotNull String value) {
try {
Long.parseLong(parseValue(value), getRadix(value));
return SUCCESS;
} catch (NumberFormatException | NullPointerException e) {
return NOT_NUMBER_ERROR;
}
}
@NotNull
@Override
public Long parse(@NotNull String value) {
return Long.parseLong(parseValue(value), getRadix(value));
}
public Long parse(@NotNull String input) throws ArgumentSyntaxException {
try {
final long value = Long.parseLong(parseValue(input), getRadix(input));
@Override
public int getConditionResult(@NotNull Long value) {
// Check range
if (hasMin && value < min) {
return RANGE_ERROR;
}
if (hasMax && value > max) {
return RANGE_ERROR;
}
// Check range
if (hasMin && value < min) {
throw new ArgumentSyntaxException("Input is lower than the minimum required value", input, RANGE_ERROR);
}
if (hasMax && value > max) {
throw new ArgumentSyntaxException("Input is higher than the minimum required value", input, RANGE_ERROR);
}
return SUCCESS;
return value;
} catch (NumberFormatException | NullPointerException e) {
throw new ArgumentSyntaxException("Input is not a number/long", input, NOT_NUMBER_ERROR);
}
}
}

View File

@ -81,6 +81,7 @@ public abstract class ArgumentNumber<T extends Number> extends Argument<T> {
return max;
}
@NotNull
protected String parseValue(@NotNull String value) {
if (value.startsWith("0b")) {
value = value.replaceFirst(Pattern.quote("0b"), "");

View File

@ -1,7 +1,9 @@
package net.minestom.server.command.builder.arguments.relative;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.location.RelativeBlockPosition;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
/**
@ -15,45 +17,16 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
super(id, 3);
}
@NotNull
@Override
public int getCorrectionResult(@NotNull String value) {
final String[] split = value.split(" ");
public RelativeBlockPosition parse(@NotNull String input) throws ArgumentSyntaxException {
final String[] split = input.split(StringUtils.SPACE);
// Check if the value has enough element to be correct
if (split.length != getNumberCount()) {
return INVALID_NUMBER_COUNT_ERROR;
throw new ArgumentSyntaxException("Invalid number of values", input, 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 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;
}
}
}
}
return SUCCESS;
}
@NotNull
@Override
public RelativeBlockPosition parse(@NotNull String value) {
final String[] split = value.split(" ");
BlockPosition blockPosition = new BlockPosition(0, 0, 0);
boolean relativeX = false;
boolean relativeY = false;
@ -72,8 +45,24 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
}
if (element.length() != RELATIVE_CHAR.length()) {
final String potentialNumber = element.substring(1);
final int number = Integer.parseInt(potentialNumber);
try {
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);
}
} catch (NumberFormatException e) {
throw new ArgumentSyntaxException("Invalid number", input, INVALID_NUMBER_ERROR);
}
}
} else {
try {
final int number = Integer.parseInt(element);
if (i == 0) {
blockPosition.setX(number);
} else if (i == 1) {
@ -81,25 +70,12 @@ public class ArgumentRelativeBlockPosition extends ArgumentRelative<RelativeBloc
} else if (i == 2) {
blockPosition.setZ(number);
}
}
} else {
final int number = Integer.parseInt(element);
if (i == 0) {
blockPosition.setX(number);
} else if (i == 1) {
blockPosition.setY(number);
} else if (i == 2) {
blockPosition.setZ(number);
} catch (NumberFormatException e) {
throw new ArgumentSyntaxException("Invalid number", input, INVALID_NUMBER_ERROR);
}
}
}
return new RelativeBlockPosition(blockPosition, relativeX, relativeY, relativeZ);
}
@Override
public int getConditionResult(@NotNull RelativeBlockPosition value) {
return SUCCESS;
}
}

View File

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

View File

@ -1,7 +1,9 @@
package net.minestom.server.command.builder.arguments.relative;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.location.RelativeVec;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
/**
@ -9,7 +11,7 @@ import org.jetbrains.annotations.NotNull;
* <p>
* Example: -1.2 ~
*/
public class ArgumentRelativeVec2 extends ArgumentRelativeVec {
public class ArgumentRelativeVec2 extends ArgumentRelative<RelativeVec> {
public ArgumentRelativeVec2(@NotNull String id) {
super(id, 2);
@ -17,8 +19,13 @@ public class ArgumentRelativeVec2 extends ArgumentRelativeVec {
@NotNull
@Override
public RelativeVec parse(@NotNull String value) {
final String[] split = value.split(" ");
public RelativeVec parse(@NotNull String input) throws ArgumentSyntaxException {
final String[] split = input.split(StringUtils.SPACE);
// Check if the value has enough element to be correct
if (split.length != getNumberCount()) {
throw new ArgumentSyntaxException("Invalid number of values", input, INVALID_NUMBER_COUNT_ERROR);
}
Vector vector = new Vector();
boolean relativeX = false;
@ -26,30 +33,34 @@ public class ArgumentRelativeVec2 extends ArgumentRelativeVec {
for (int i = 0; i < split.length; i++) {
final String element = split[i];
if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) {
relativeX = true;
} else if (i == 1) {
relativeZ = true;
}
try {
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 (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) {
vector.setX(number);
} else if (i == 1) {
vector.setZ(number);
}
}
} else {
final float number = Float.parseFloat(element);
if (i == 0) {
vector.setX(number);
} else if (i == 1) {
vector.setZ(number);
}
} catch (NumberFormatException e) {
throw new ArgumentSyntaxException("Invalid number", input, INVALID_NUMBER_ERROR);
}
}

View File

@ -1,7 +1,9 @@
package net.minestom.server.command.builder.arguments.relative;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.location.RelativeVec;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
/**
@ -9,7 +11,7 @@ import org.jetbrains.annotations.NotNull;
* <p>
* Example: -1.2 ~ 5
*/
public class ArgumentRelativeVec3 extends ArgumentRelativeVec {
public class ArgumentRelativeVec3 extends ArgumentRelative<RelativeVec> {
public ArgumentRelativeVec3(@NotNull String id) {
super(id, 3);
@ -17,8 +19,13 @@ public class ArgumentRelativeVec3 extends ArgumentRelativeVec {
@NotNull
@Override
public RelativeVec parse(@NotNull String value) {
final String[] split = value.split(" ");
public RelativeVec parse(@NotNull String input) throws ArgumentSyntaxException {
final String[] split = input.split(StringUtils.SPACE);
// Check if the value has enough element to be correct
if (split.length != getNumberCount()) {
throw new ArgumentSyntaxException("Invalid number of values", input, INVALID_NUMBER_COUNT_ERROR);
}
Vector vector = new Vector();
boolean relativeX = false;
@ -27,19 +34,30 @@ public class ArgumentRelativeVec3 extends ArgumentRelativeVec {
for (int i = 0; i < split.length; i++) {
final String element = split[i];
if (element.startsWith(RELATIVE_CHAR)) {
try {
if (element.startsWith(RELATIVE_CHAR)) {
if (i == 0) {
relativeX = true;
} else if (i == 1) {
relativeY = true;
} else if (i == 2) {
relativeZ = true;
}
if (i == 0) {
relativeX = true;
} else if (i == 1) {
relativeY = true;
} 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);
}
}
if (element.length() != RELATIVE_CHAR.length()) {
final String potentialNumber = element.substring(1);
final float number = Float.parseFloat(potentialNumber);
} else {
final float number = Float.parseFloat(element);
if (i == 0) {
vector.setX(number);
} else if (i == 1) {
@ -48,16 +66,8 @@ public class ArgumentRelativeVec3 extends ArgumentRelativeVec {
vector.setZ(number);
}
}
} else {
final float number = Float.parseFloat(element);
if (i == 0) {
vector.setX(number);
} else if (i == 1) {
vector.setY(number);
} else if (i == 2) {
vector.setZ(number);
}
} catch (NumberFormatException e) {
throw new ArgumentSyntaxException("Invalid number", input, INVALID_NUMBER_ERROR);
}
}

View File

@ -0,0 +1,48 @@
package net.minestom.server.command.builder.exception;
import net.minestom.server.command.builder.ArgumentCallback;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.Argument;
import org.jetbrains.annotations.NotNull;
/**
* Exception triggered when an {@link Argument} is wrongly parsed.
* <p>
* Retrieved in {@link ArgumentCallback} defined in {@link Command#setArgumentCallback(ArgumentCallback, Argument)}.
* <p>
* Be aware that the message returned by {@link #getMessage()} is only here for debugging purpose,
* you should refer to {@link #getErrorCode()} to identify the exceptions.
*/
public class ArgumentSyntaxException extends Exception {
private final String input;
private final int errorCode;
public ArgumentSyntaxException(@NotNull String message, @NotNull String input, int errorCode) {
super(message);
this.input = input;
this.errorCode = errorCode;
}
/**
* Gets the problematic command input.
*
* @return the command input which triggered the exception
*/
@NotNull
public String getInput() {
return input;
}
/**
* Gets the error code of the exception.
* <p>
* The code is decided arbitrary by the argument,
* check the argument class to know the meaning of each one.
*
* @return the argument error code
*/
public int getErrorCode() {
return errorCode;
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.data;
import net.minestom.server.MinecraftServer;
import net.minestom.server.utils.clone.PublicCloneable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -126,7 +127,7 @@ public abstract class Data implements PublicCloneable<Data> {
try {
return (Data) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
MinecraftServer.getExceptionManager().handleException(e);
throw new IllegalStateException("Weird thing happened");
}
}

View File

@ -183,7 +183,7 @@ public class SerializableDataImpl extends SerializableData {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
MinecraftServer.getExceptionManager().handleException(e);
return null;
}
});

View File

@ -3,17 +3,14 @@ package net.minestom.server.entity;
import com.google.common.collect.Queues;
import net.minestom.server.MinecraftServer;
import net.minestom.server.Viewable;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.collision.CollisionUtils;
import net.minestom.server.data.Data;
import net.minestom.server.data.DataContainer;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventCallback;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.entity.EntitySpawnEvent;
import net.minestom.server.event.entity.EntityTickEvent;
import net.minestom.server.event.entity.EntityVelocityEvent;
import net.minestom.server.event.entity.*;
import net.minestom.server.event.handler.EventHandler;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
@ -22,6 +19,9 @@ 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.potion.Potion;
import net.minestom.server.potion.PotionEffect;
import net.minestom.server.potion.TimedPotion;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
@ -41,6 +41,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@ -53,6 +54,7 @@ import java.util.function.Consumer;
public abstract class Entity implements Viewable, EventHandler, DataContainer, PermissionHandler {
private static final Map<Integer, Entity> entityById = new ConcurrentHashMap<>();
private static final Map<UUID, Entity> entityByUuid = new ConcurrentHashMap<>();
private static final AtomicInteger lastEntityId = new AtomicInteger();
// Metadata
@ -66,7 +68,14 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
protected static final byte METADATA_BOOLEAN = 7;
protected static final byte METADATA_ROTATION = 8;
protected static final byte METADATA_POSITION = 9;
protected static final byte METADATA_OPTPOSITION = 10;
protected static final byte METADATA_DIRECTION = 11;
protected static final byte METADATA_OPTUUID = 12;
protected static final byte METADATA_OPTBLOCKID = 13;
protected static final byte METADATA_NBT = 14;
protected static final byte METADATA_PARTICLE = 15;
protected static final byte METADATA_VILLAGERDATA = 16;
protected static final byte METADATA_OPTVARINT = 17;
protected static final byte METADATA_POSE = 18;
protected Instance instance;
@ -88,7 +97,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
protected float gravityAcceleration;
protected float gravityTerminalVelocity;
protected int gravityTickCount; // Number of tick where gravity tick was applied
protected float eyeHeight;
private boolean autoViewable;
private final int id;
@ -123,12 +131,14 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
protected boolean glowing;
protected boolean usingElytra;
protected int air = 300;
protected ColoredText customName;
protected JsonMessage customName;
protected boolean customNameVisible;
protected boolean silent;
protected boolean noGravity;
protected Pose pose = Pose.STANDING;
private final List<TimedPotion> effects = new CopyOnWriteArrayList<>();
// list of scheduled tasks to be executed during the next entity tick
protected final Queue<Consumer<Entity>> nextTick = Queues.newConcurrentLinkedQueue();
@ -136,17 +146,25 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
private long ticks;
private final EntityTickEvent tickEvent = new EntityTickEvent(this);
public Entity(@NotNull EntityType entityType, @NotNull Position spawnPosition) {
public Entity(@NotNull EntityType entityType, @NotNull UUID uuid, @NotNull Position spawnPosition) {
this.id = generateId();
this.entityType = entityType;
this.uuid = UUID.randomUUID();
this.uuid = uuid;
this.position = spawnPosition.clone();
this.lastX = spawnPosition.getX();
this.lastY = spawnPosition.getY();
this.lastZ = spawnPosition.getZ();
setBoundingBox(0, 0, 0);
setAutoViewable(true);
entityById.put(id, this);
Entity.entityById.put(id, this);
Entity.entityByUuid.put(uuid, this);
}
public Entity(@NotNull EntityType entityType, @NotNull Position spawnPosition) {
this(entityType, UUID.randomUUID(), spawnPosition);
}
public Entity(@NotNull EntityType entityType) {
@ -173,9 +191,21 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
*/
@Nullable
public static Entity getEntity(int id) {
return entityById.getOrDefault(id, null);
return Entity.entityById.getOrDefault(id, null);
}
/**
* Gets an entity based on its UUID (from {@link #getUuid()}).
*
* @param uuid the entity UUID
* @return the entity having the specified uuid, null if not found
*/
@Nullable
public static Entity getEntity(@NotNull UUID uuid) {
return Entity.entityByUuid.getOrDefault(uuid, null);
}
/**
* Generate and return a new unique entity id.
* <p>
@ -215,12 +245,13 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @throws IllegalStateException if you try to teleport an entity before settings its instance
*/
public void teleport(@NotNull Position position, @Nullable long[] chunks, @Nullable Runnable callback) {
Check.notNull(position, "Teleport position cannot be null");
Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!");
final Position teleportPosition = position.clone(); // Prevent synchronization issue
final ChunkCallback endCallback = (chunk) -> {
refreshPosition(position.getX(), position.getY(), position.getZ());
refreshView(position.getYaw(), position.getPitch());
refreshPosition(teleportPosition);
refreshView(teleportPosition.getYaw(), teleportPosition.getPitch());
sendSynchronization();
@ -228,7 +259,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
};
if (chunks == null || chunks.length == 0) {
instance.loadOptionalChunk(position, endCallback);
instance.loadOptionalChunk(teleportPosition, endCallback);
} else {
ChunkUtils.optionalLoadAll(instance, chunks, null, endCallback);
}
@ -302,7 +333,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
@Override
public boolean addViewer(@NotNull Player player) {
Check.notNull(player, "Viewer cannot be null");
boolean result = this.viewers.add(player);
if (!result)
return false;
@ -312,7 +342,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
@Override
public boolean removeViewer(@NotNull Player player) {
Check.notNull(player, "Viewer cannot be null");
if (!viewers.remove(player))
return false;
@ -432,6 +461,13 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
// Entity tick
{
// Cache the number of "gravity tick"
if (!onGround) {
gravityTickCount++;
} else {
gravityTickCount = 0;
}
// Velocity
boolean applyVelocity;
// Non-player entities with either velocity or gravity enabled
@ -446,45 +482,32 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
final float newZ = position.getZ() + velocity.getZ() / tps;
Position newPosition = new Position(newX, newY, newZ);
// Gravity
{
// Cache the number of "gravity tick"
if (!onGround) {
gravityTickCount++;
} else {
gravityTickCount = 0;
}
// Compute the gravity change (drag per tick + acceleration)
final float gravityY = Math.min(
gravityDragPerTick + (gravityAcceleration * (float) gravityTickCount),
gravityTerminalVelocity);
// Change velocity to apply gravity
if (!noGravity) {
velocity.setY(velocity.getY() - gravityY);
}
}
Vector newVelocityOut = new Vector();
// Gravity force
final float gravityY = !noGravity ? Math.min(
gravityDragPerTick + (gravityAcceleration * (float) gravityTickCount),
gravityTerminalVelocity) : 0f;
final Vector deltaPos = new Vector(
getVelocity().getX() / tps,
getVelocity().getY() / tps,
(getVelocity().getY() - gravityY) / tps,
getVelocity().getZ() / tps
);
this.onGround = CollisionUtils.handlePhysics(this, deltaPos, newPosition, newVelocityOut);
// Stop here if the position is the same
final boolean updatePosition = !newPosition.isSimilar(position);
// World border collision
final Position finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
final Chunk finalChunk = instance.getChunkAt(finalVelocityPosition);
// Check chunk
if (!ChunkUtils.isLoaded(instance, newPosition.getX(), newPosition.getZ())) {
// Entity shouldn't be updated when moving in an unloaded chunk
if (!ChunkUtils.isLoaded(finalChunk)) {
return;
}
// World border and apply the position
final Position finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
if (finalVelocityPosition != null && updatePosition) {
// Apply the position if changed
if (!newPosition.isSimilar(position)) {
refreshPosition(finalVelocityPosition);
}
@ -497,7 +520,10 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
float drag;
if (onGround) {
final BlockPosition blockPosition = position.toBlockPosition();
final CustomBlock customBlock = instance.getCustomBlock(blockPosition);
final CustomBlock customBlock = finalChunk.getCustomBlock(
blockPosition.getX(),
blockPosition.getY(),
blockPosition.getZ());
if (customBlock != null) {
// Custom drag
drag = customBlock.getDrag(instance, blockPosition);
@ -508,7 +534,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
// Stop player velocity
if (PlayerUtils.isNettyClient(this)) {
velocity.zero();
this.velocity.zero();
}
} else {
drag = 0.98f; // air drag
@ -522,7 +548,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
sendSynchronization();
// Verify if velocity packet has to be sent
if (hasVelocity() || gravityTickCount > 0) {
sendVelocityPacket();
sendPacketsToViewers(getVelocityPacket());
}
}
@ -564,6 +590,24 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
ticks++;
callEvent(EntityTickEvent.class, tickEvent); // reuse tickEvent to avoid recreating it each tick
// remove expired effects
{
this.effects.removeIf(timedPotion -> {
final long potionTime = (long) timedPotion.getPotion().getDuration() * MinecraftServer.TICK_MS;
// Remove if the potion should be expired
if (time >= timedPotion.getStartingTime() + potionTime) {
// Send the packet that the potion should no longer be applied
timedPotion.getPotion().sendRemovePacket(this);
callEvent(EntityPotionRemoveEvent.class, new EntityPotionRemoveEvent(
this,
timedPotion.getPotion()
));
return true;
}
return false;
});
}
}
// Scheduled synchronization
@ -577,13 +621,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
}
}
/**
* Equivalent to <code>sendPacketsToViewers(getVelocityPacket());</code>.
*/
public void sendVelocityPacket() {
sendPacketsToViewers(getVelocityPacket());
}
/**
* Gets the number of ticks this entity has been active for.
*
@ -643,7 +680,11 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
*
* @param uuid the new entity uuid
*/
protected void setUuid(@NotNull UUID uuid) {
public void setUuid(@NotNull UUID uuid) {
// Refresh internal map
Entity.entityByUuid.remove(this.uuid);
Entity.entityByUuid.put(uuid, this);
this.uuid = uuid;
}
@ -679,6 +720,17 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
this.boundingBox = new BoundingBox(this, x, y, z);
}
/**
* Changes the internal entity bounding box.
* <p>
* WARNING: this does not change the entity hit-box which is client-side.
*
* @param boundingBox the new bounding box
*/
public void setBoundingBox(BoundingBox boundingBox) {
this.boundingBox = boundingBox;
}
/**
* Convenient method to get the entity current chunk.
*
@ -707,7 +759,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @throws IllegalStateException if {@code instance} has not been registered in {@link InstanceManager}
*/
public void setInstance(@NotNull Instance instance) {
Check.notNull(instance, "instance cannot be null!");
Check.stateCondition(!instance.isRegistered(),
"Instances need to be registered, please use InstanceManager#registerInstance or InstanceManager#registerSharedInstance");
@ -816,7 +867,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @return the distance between this and {@code entity}
*/
public float getDistance(@NotNull Entity entity) {
Check.notNull(entity, "Entity cannot be null");
return getPosition().getDistance(entity.getPosition());
}
@ -838,7 +888,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @throws IllegalStateException if {@link #getInstance()} returns null
*/
public void addPassenger(@NotNull Entity entity) {
Check.notNull(entity, "Passenger cannot be null");
Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance");
if (entity.getVehicle() != null) {
@ -859,7 +908,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @throws IllegalStateException if {@link #getInstance()} returns null
*/
public void removePassenger(@NotNull Entity entity) {
Check.notNull(entity, "Passenger cannot be null");
Check.stateCondition(instance == null, "You need to set an instance using Entity#setInstance");
if (!passengers.remove(entity))
@ -980,7 +1028,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
*
* @return the custom name of the entity, null if there is not
*/
public ColoredText getCustomName() {
public JsonMessage getCustomName() {
return customName;
}
@ -989,7 +1037,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
*
* @param customName the custom name of the entity, null to remove it
*/
public void setCustomName(ColoredText customName) {
public void setCustomName(JsonMessage customName) {
this.customName = customName;
sendMetadataIndex(2);
}
@ -1055,9 +1103,6 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @param z new position Z
*/
public void refreshPosition(float x, float y, float z) {
this.lastX = position.getX();
this.lastY = position.getY();
this.lastZ = position.getZ();
position.setX(x);
position.setY(y);
position.setZ(z);
@ -1094,6 +1139,10 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
}
}
}
this.lastX = position.getX();
this.lastY = position.getY();
this.lastZ = position.getZ();
}
/**
@ -1129,9 +1178,33 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
* @param sneaking true to make the entity sneak
*/
public void setSneaking(boolean sneaking) {
this.crouched = sneaking;
this.pose = sneaking ? Pose.SNEAKING : Pose.STANDING;
sendMetadataIndex(0);
setPose(sneaking ? Pose.SNEAKING : Pose.STANDING);
sendMetadataIndex(0); // update the crouched metadata
}
/**
* Gets the current entity pose.
*
* @return the entity pose
*/
@NotNull
public Pose getPose() {
return pose;
}
/**
* Changes the entity pose.
* <p>
* The internal {@code crouched} and {@code swimming} field will be
* updated accordingly.
*
* @param pose the new entity pose
*/
@NotNull
public void setPose(@NotNull Pose pose) {
this.crouched = pose == Pose.SNEAKING;
this.swimming = pose == Pose.SWIMMING;
this.pose = pose;
sendMetadataIndex(6);
}
@ -1152,37 +1225,73 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
*
* @return the current position of the entity
*/
@NotNull
public Position getPosition() {
return position;
}
/**
* Gets the entity eye height.
* <p>
* Default to {@link BoundingBox#getHeight()}x0.85
*
* @return the entity eye height
*/
public float getEyeHeight() {
return eyeHeight;
return boundingBox.getHeight() * 0.85f;
}
/**
* Changes the entity eye height.
* Gets all the potion effect of this entity.
*
* @param eyeHeight the entity eye height
* @return an unmodifiable list of all this entity effects
*/
public void setEyeHeight(float eyeHeight) {
this.eyeHeight = eyeHeight;
@NotNull
public List<TimedPotion> getActiveEffects() {
return Collections.unmodifiableList(effects);
}
/**
* Removes effect from entity, if it has it.
*
* @param effect The effect to remove
*/
public void removeEffect(@NotNull PotionEffect effect) {
this.effects.removeIf(timedPotion -> {
if (timedPotion.getPotion().getEffect() == effect) {
timedPotion.getPotion().sendRemovePacket(this);
callEvent(EntityPotionRemoveEvent.class, new EntityPotionRemoveEvent(
this,
timedPotion.getPotion()
));
return true;
}
return false;
});
}
/**
* Adds an effect to an entity.
*
* @param potion The potion to add
*/
public void addEffect(@NotNull Potion potion) {
removeEffect(potion.getEffect());
this.effects.add(new TimedPotion(potion, System.currentTimeMillis()));
potion.sendAddPacket(this);
callEvent(EntityPotionAddEvent.class, new EntityPotionAddEvent(this, potion));
}
/**
* Removes the entity from the server immediately.
* <p>
* WARNING: this do not trigger the {@link EntityDeathEvent} event.
* WARNING: this does not trigger {@link EntityDeathEvent}.
*/
public void remove() {
this.removed = true;
this.shouldRemove = true;
entityById.remove(id);
Entity.entityById.remove(id);
Entity.entityByUuid.remove(uuid);
if (instance != null)
instance.UNSAFE_removeEntity(this);
}
@ -1377,7 +1486,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
protected void sendSynchronization() {
EntityTeleportPacket entityTeleportPacket = new EntityTeleportPacket();
entityTeleportPacket.entityId = getEntityId();
entityTeleportPacket.position = getPosition();
entityTeleportPacket.position = getPosition().clone();
entityTeleportPacket.onGround = isOnGround();
sendPacketToViewers(entityTeleportPacket);
}
@ -1389,7 +1498,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
this.lastAbsoluteSynchronizationTime = 0;
}
private enum Pose {
public enum Pose {
STANDING,
FALL_FLYING,
SLEEPING,

View File

@ -13,7 +13,6 @@ import net.minestom.server.event.item.ArmorEquipEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.item.ItemStack;
import net.minestom.server.network.packet.server.play.EntityEquipmentPacket;
import net.minestom.server.network.packet.server.play.EntityMovementPacket;
import net.minestom.server.network.packet.server.play.SpawnLivingEntityPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.Position;
@ -123,17 +122,13 @@ public abstract class EntityCreature extends LivingEntity implements NavigableEn
final PlayerConnection playerConnection = player.getPlayerConnection();
EntityMovementPacket entityMovementPacket = new EntityMovementPacket();
entityMovementPacket.entityId = getEntityId();
SpawnLivingEntityPacket spawnLivingEntityPacket = new SpawnLivingEntityPacket();
spawnLivingEntityPacket.entityId = getEntityId();
spawnLivingEntityPacket.entityUuid = getUuid();
spawnLivingEntityPacket.entityType = getEntityType().getId();
spawnLivingEntityPacket.position = getPosition();
spawnLivingEntityPacket.headPitch = 0;
spawnLivingEntityPacket.headPitch = getPosition().getYaw();
playerConnection.sendPacket(entityMovementPacket);
playerConnection.sendPacket(spawnLivingEntityPacket);
playerConnection.sendPacket(getVelocityPacket());
playerConnection.sendPacket(getMetadataPacket());
@ -224,7 +219,7 @@ public abstract class EntityCreature extends LivingEntity implements NavigableEn
/**
* Gets the entity target.
*
* @return the entity target
* @return the entity target, can be null if not any
*/
@Nullable
public Entity getTarget() {
@ -234,9 +229,9 @@ public abstract class EntityCreature extends LivingEntity implements NavigableEn
/**
* Changes the entity target.
*
* @param target the new entity target
* @param target the new entity target, null to remove
*/
public void setTarget(@NotNull Entity target) {
public void setTarget(@Nullable Entity target) {
this.target = target;
}

View File

@ -1,110 +0,0 @@
package net.minestom.server.entity;
import com.google.common.collect.Queues;
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.network.player.PlayerConnection;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.Queue;
import java.util.UUID;
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 Queue<Player> waitingPlayers = Queues.newConcurrentLinkedQueue();
/**
* Connects waiting players.
*/
public void updateWaitingPlayers() {
// Connect waiting players
waitingPlayersTick();
}
/**
* 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;
while ((waitingPlayer = waitingPlayers.poll()) != null) {
PlayerLoginEvent loginEvent = new PlayerLoginEvent(waitingPlayer);
waitingPlayer.callEvent(PlayerLoginEvent.class, loginEvent);
final Instance spawningInstance = loginEvent.getSpawningInstance();
Check.notNull(spawningInstance, "You need to specify a spawning instance in the PlayerLoginEvent");
waitingPlayer.init();
// Spawn the player at Player#getRespawnPoint during the next instance tick
spawningInstance.scheduleNextTick(waitingPlayer::setInstance);
}
}
/**
* 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,
* currently executed in {@link Player#Player(UUID, String, PlayerConnection)}.
*
* @param player the {@link Player} to add
*/
public void addWaitingPlayer(@NotNull Player player) {
// Init player (register events)
for (Consumer<Player> playerInitialization : MinecraftServer.getConnectionManager().getPlayerInitializations()) {
playerInitialization.accept(player);
}
// Call pre login event
PlayerPreLoginEvent playerPreLoginEvent = new PlayerPreLoginEvent(player, player.getUsername(), player.getUuid());
player.callEvent(PlayerPreLoginEvent.class, playerPreLoginEvent);
// Ignore the player if he has been disconnected (kick)
final boolean online = player.isOnline();
if (!online)
return;
// Add him to the list and change his username/uuid if changed
this.waitingPlayers.add(player);
final String username = playerPreLoginEvent.getUsername();
final UUID uuid = playerPreLoginEvent.getPlayerUuid();
player.setUsername(username);
player.setUuid(uuid);
}
}

View File

@ -3,18 +3,24 @@ package net.minestom.server.entity;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.server.play.SpawnExperienceOrbPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ExperienceOrb extends Entity {
private short experienceCount;
private Player target;
private long lastTargetUpdateTick;
public ExperienceOrb(short experienceCount, @NotNull Position spawnPosition) {
super(EntityType.EXPERIENCE_ORB, spawnPosition);
setGravity(0.02f, 0.04f, 1.96f);
setBoundingBox(0.5f, 0.5f, 0.5f);
//todo vanilla sets random velocity here?
this.experienceCount = experienceCount;
}
@ -28,7 +34,51 @@ public class ExperienceOrb extends Entity {
@Override
public void update(long time) {
// TODO slide toward nearest player
//todo water movement
if (hasNoGravity()) {
setVelocity(getVelocity().add(0, -0.3f, 0));
}
//todo lava
double d = 8.0;
if (lastTargetUpdateTick < time - 20 + getEntityId() % 100) {
if (target == null || target.getPosition().getDistanceSquared(getPosition()) > 64) {
this.target = getClosestPlayer(this, 8);
}
lastTargetUpdateTick = time;
}
if (target != null && target.getGameMode() == GameMode.SPECTATOR) {
target = null;
}
if (target != null) {
Position pos = getPosition();
Position targetPos = target.getPosition();
Vector toTarget = new Vector(targetPos.getX() - pos.getX(), targetPos.getY() + (target.getEyeHeight() / 2) - pos.getY(), targetPos.getZ() - pos.getZ());
double e = toTarget.length(); //could really be lengthSquared
if (e < 8) {
double f = 1 - (e / 8);
setVelocity(getVelocity().add(toTarget.normalize().multiply(f * f * 0.1)));
}
}
// Move should be called here
float g = 0.98f;
if (this.onGround) {
// g = 2f;
g = 0.6f * 0.98f;
}
// apply slipperiness
setVelocity(getVelocity().multiply(new Vector(g, 0.98f, g)));
if (isOnGround())
setVelocity(getVelocity().multiply(new Vector(1, -0.9f, 1)));
}
@Override
@ -77,4 +127,15 @@ public class ExperienceOrb extends Entity {
getViewers().forEach(this::addViewer);
}
private Player getClosestPlayer(Entity entity, float maxDistance) {
Player closest = entity.getInstance()
.getPlayers()
.stream()
.min((a, b) -> Float.compare(a.getDistance(entity), b.getDistance(entity)))
.orElse(null);
if (closest == null) return null;
if (closest.getDistance(entity) > maxDistance) return null;
return closest;
}
}

View File

@ -10,6 +10,7 @@ import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.entity.EntityFireEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.block.Block;
import net.minestom.server.inventory.EquipmentHandler;
import net.minestom.server.item.ItemStack;
import net.minestom.server.network.packet.server.play.CollectItemPacket;
@ -19,8 +20,10 @@ import net.minestom.server.network.packet.server.play.SoundEffectPacket;
import net.minestom.server.scoreboard.Team;
import net.minestom.server.sound.Sound;
import net.minestom.server.sound.SoundCategory;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.block.BlockIterator;
import net.minestom.server.utils.time.CooldownUtils;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
@ -28,8 +31,7 @@ import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
@ -270,7 +272,6 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
* @return true if damage has been applied, false if it didn't
*/
public boolean damage(@NotNull DamageType type, float value) {
Check.notNull(type, "The damage type cannot be null!");
if (isDead())
return false;
if (isInvulnerable() || isImmune(type)) {
@ -501,12 +502,15 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
*/
@NotNull
protected EntityPropertiesPacket getPropertiesPacket() {
// Get all the attributes which should be sent to the client
final AttributeInstance[] instances = attributeModifiers.values().stream()
.filter(i -> i.getAttribute().isShared())
.toArray(AttributeInstance[]::new);
EntityPropertiesPacket propertiesPacket = new EntityPropertiesPacket();
propertiesPacket.entityId = getEntityId();
AttributeInstance[] instances = attributeModifiers.values().stream()
.filter(i -> i.getAttribute().isShared())
.toArray(AttributeInstance[]::new);
EntityPropertiesPacket.Property[] properties = new EntityPropertiesPacket.Property[instances.length];
for (int i = 0; i < properties.length; ++i) {
EntityPropertiesPacket.Property property = new EntityPropertiesPacket.Property();
@ -529,7 +533,8 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
*/
private void setupAttributes() {
for (Attribute attribute : Attribute.values()) {
attributeModifiers.put(attribute.getKey(), new AttributeInstance(attribute, this::onAttributeChanged));
final AttributeInstance attributeInstance = new AttributeInstance(attribute, this::onAttributeChanged);
this.attributeModifiers.put(attribute.getKey(), attributeInstance);
}
}
@ -597,4 +602,36 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
public Team getTeam() {
return team;
}
/**
* Gets the line of sight in {@link BlockPosition} of the entity.
*
* @param maxDistance The max distance to scan
* @return A list of {@link BlockPosition} in this entities line of sight
*/
public List<BlockPosition> getLineOfSight(int maxDistance) {
List<BlockPosition> blocks = new ArrayList<>();
Iterator<BlockPosition> it = new BlockIterator(this, maxDistance);
while (it.hasNext()) {
BlockPosition position = it.next();
if (Block.fromStateId(getInstance().getBlockStateId(position)) != Block.AIR) blocks.add(position);
}
return blocks;
}
/**
* Gets the target (not-air) {@link BlockPosition} of the entity.
*
* @param maxDistance The max distance to scan before returning null
* @return The {@link BlockPosition} targeted by this entity, null if non are found
*/
public BlockPosition getTargetBlockPosition(int maxDistance) {
Iterator<BlockPosition> it = new BlockIterator(this, maxDistance);
while (it.hasNext()) {
BlockPosition position = it.next();
if (Block.fromStateId(getInstance().getBlockStateId(position)) != Block.AIR) return position;
}
return null;
}
}

View File

@ -29,12 +29,15 @@ import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.metadata.WrittenBookMeta;
import net.minestom.server.listener.PlayerDiggingListener;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.PlayerProvider;
import net.minestom.server.network.packet.client.ClientPlayPacket;
import net.minestom.server.network.packet.client.play.ClientChatMessagePacket;
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.*;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
@ -51,6 +54,7 @@ import net.minestom.server.utils.binary.BinaryWriter;
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.entity.EntityUtils;
import net.minestom.server.utils.instance.InstanceUtils;
import net.minestom.server.utils.time.CooldownUtils;
import net.minestom.server.utils.time.TimeUnit;
@ -60,6 +64,7 @@ import net.minestom.server.world.DimensionType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
@ -120,7 +125,7 @@ public class Player extends LivingEntity implements CommandSender {
protected final Set<Entity> viewableEntities = new CopyOnWriteArraySet<>();
private int latency;
private ColoredText displayName;
private JsonMessage displayName;
private PlayerSkin skin;
private DimensionType dimensionType;
@ -128,14 +133,13 @@ public class Player extends LivingEntity implements CommandSender {
protected final Set<Chunk> viewableChunks = new CopyOnWriteArraySet<>();
private final AtomicInteger teleportId = new AtomicInteger();
protected boolean onGround;
private final Queue<ClientPlayPacket> packets = Queues.newConcurrentLinkedQueue();
private final boolean levelFlat;
private final PlayerSettings settings;
private float exp;
private int level;
private final PlayerInventory inventory;
protected PlayerInventory inventory;
private Inventory openInventory;
// Used internally to allow the closing of inventory within the inventory listener
private boolean didCloseInventory;
@ -199,7 +203,7 @@ public class Player extends LivingEntity implements CommandSender {
this.username = username;
this.playerConnection = playerConnection;
setBoundingBox(0.69f, 1.8f, 0.69f);
setBoundingBox(0.6f, 1.8f, 0.6f);
setRespawnPoint(new Position(0, 0, 0));
@ -212,14 +216,12 @@ public class Player extends LivingEntity implements CommandSender {
refreshAnswerKeepAlive(true);
this.gameMode = GameMode.SURVIVAL;
this.dimensionType = DimensionType.OVERWORLD;
this.dimensionType = DimensionType.OVERWORLD; // Default dimension
this.levelFlat = true;
getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.1f);
// FakePlayer init its connection there
playerConnectionInit();
MinecraftServer.getEntityManager().addWaitingPlayer(this);
}
/**
@ -227,8 +229,13 @@ public class Player extends LivingEntity implements CommandSender {
* Init the player and spawn him.
* <p>
* WARNING: executed in the main update thread
* UNSAFE: Only meant to be used when a netty player connects through the server.
*
* @param spawnInstance the player spawn instance (defined in {@link PlayerLoginEvent})
*/
protected void init() {
public void UNSAFE_init(@NotNull Instance spawnInstance) {
this.dimensionType = spawnInstance.getDimensionType();
JoinGamePacket joinGamePacket = new JoinGamePacket();
joinGamePacket.entityId = getEntityId();
joinGamePacket.gameMode = gameMode;
@ -456,13 +463,8 @@ public class Player extends LivingEntity implements CommandSender {
position, new Position(lastPlayerSyncX, lastPlayerSyncY, lastPlayerSyncZ), onGround);
} else {
// View changed
EntityRotationPacket entityRotationPacket = new EntityRotationPacket();
entityRotationPacket.entityId = getEntityId();
entityRotationPacket.yaw = position.getYaw();
entityRotationPacket.pitch = position.getPitch();
entityRotationPacket.onGround = onGround;
updatePacket = entityRotationPacket;
updatePacket = EntityRotationPacket.getPacket(getEntityId(),
position.getYaw(), position.getPitch(), onGround);
}
if (viewChanged) {
@ -500,43 +502,52 @@ public class Player extends LivingEntity implements CommandSender {
public void kill() {
if (!isDead()) {
// send death screen text to the killed player
JsonMessage deathText;
JsonMessage chatMessage;
// get death screen text to the killed player
{
ColoredText deathText;
if (lastDamageSource != null) {
deathText = lastDamageSource.buildDeathScreenText(this);
} else { // may happen if killed by the server without applying damage
deathText = ColoredText.of("Killed by poor programming.");
}
// #buildDeathScreenText can return null, check here
if (deathText != null) {
CombatEventPacket deathPacket = CombatEventPacket.death(this, Optional.empty(), deathText);
playerConnection.sendPacket(deathPacket);
}
}
// send death message to chat
// get death message to chat
{
JsonMessage chatMessage;
if (lastDamageSource != null) {
chatMessage = lastDamageSource.buildDeathMessage(this);
} else { // may happen if killed by the server without applying damage
chatMessage = ColoredText.of(getUsername() + " was killed by poor programming.");
}
// #buildDeathMessage can return null, check here
if (chatMessage != null) {
MinecraftServer.getConnectionManager().broadcastMessage(chatMessage);
}
}
// Call player death event
PlayerDeathEvent playerDeathEvent = new PlayerDeathEvent(this, deathText, chatMessage);
callEvent(PlayerDeathEvent.class, playerDeathEvent);
deathText = playerDeathEvent.getDeathText();
chatMessage = playerDeathEvent.getChatMessage();
// #buildDeathScreenText can return null, check here
if (deathText != null) {
CombatEventPacket deathPacket = CombatEventPacket.death(this, null, deathText);
playerConnection.sendPacket(deathPacket);
}
// #buildDeathMessage can return null, check here
if (chatMessage != null) {
MinecraftServer.getConnectionManager().broadcastMessage(chatMessage);
}
}
super.kill();
}
/**
* Respawns the player by sending a {@link RespawnPacket} to the player and teleporting him
* to {@link #getRespawnPoint()}. It also resets fire and his health
* to {@link #getRespawnPoint()}. It also resets fire and health.
*/
public void respawn() {
if (!isDead())
@ -652,29 +663,24 @@ public class Player extends LivingEntity implements CommandSender {
* it is possible for this method to be non-blocking when retrieving chunks is required.
*
* @param instance the new player instance
* @param spawnPosition the new position of the player,
* can be null or {@link #getPosition()} if you do not want to teleport the player
* @param spawnPosition the new position of the player
*/
public void setInstance(@NotNull Instance instance, @Nullable Position spawnPosition) {
Check.notNull(instance, "instance cannot be null!");
public void setInstance(@NotNull Instance instance, @NotNull Position spawnPosition) {
Check.argCondition(this.instance == instance, "Instance should be different than the current one");
// true if the chunks need to be sent to the client, can be false if the instances share the same chunks (eg SharedInstance)
final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance);
final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance) ||
!spawnPosition.inSameChunk(this.position);
if (needWorldRefresh) {
final boolean firstSpawn = this.instance == null; // TODO: Handle player reconnections, must be false in that case too
// Remove all previous viewable chunks (from the previous instance)
for (Chunk viewableChunk : viewableChunks) {
viewableChunk.removeViewer(this);
}
// Send the new dimension
if (this.instance != null) {
// Send the new dimension if player isn't in any instance or if the dimension is different
{
final DimensionType instanceDimensionType = instance.getDimensionType();
if (dimensionType != instanceDimensionType)
if (dimensionType != instanceDimensionType) {
sendDimension(instanceDimensionType);
}
}
// Load all the required chunks
@ -693,7 +699,7 @@ public class Player extends LivingEntity implements CommandSender {
final ChunkCallback endCallback = chunk -> {
// This is the last chunk to be loaded , spawn player
spawnPlayer(instance, spawnPosition, firstSpawn);
spawnPlayer(instance, spawnPosition, firstSpawn, true);
};
// Chunk 0;0 always needs to be loaded
@ -704,13 +710,13 @@ public class Player extends LivingEntity implements CommandSender {
} else {
// The player already has the good version of all the chunks.
// We just need to refresh his entity viewing list and add him to the instance
spawnPlayer(instance, spawnPosition, false);
spawnPlayer(instance, spawnPosition, false, false);
}
}
/**
* Changes the player instance without changing its position (defaulted to {@link #getRespawnPoint()}
* if the player is not in any instance.
* if the player is not in any instance).
*
* @param instance the new player instance
* @see #setInstance(Instance, Position)
@ -730,16 +736,29 @@ public class Player extends LivingEntity implements CommandSender {
* @param spawnPosition the position to teleport the player
* @param firstSpawn true if this is the player first spawn
*/
private void spawnPlayer(@NotNull Instance instance, @Nullable Position spawnPosition, boolean firstSpawn) {
private void spawnPlayer(@NotNull Instance instance, @Nullable Position spawnPosition,
boolean firstSpawn, boolean updateChunks) {
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
super.setInstance(instance);
// Runnable used to send newly visible chunks to player once spawned in the instance
final Runnable refreshRunnable = () -> {
if (!updateChunks)
return;
// Remove all previous viewable chunks (from the previous instance)
this.viewableChunks.forEach(chunk -> chunk.removeViewer(this));
final Chunk chunk = getChunk();
if (chunk != null) {
refreshVisibleChunks(chunk);
}
};
if (spawnPosition != null && !position.isSimilar(spawnPosition)) {
teleport(spawnPosition,
position.inSameChunk(spawnPosition) ? () -> refreshVisibleChunks(getChunk()) : null);
teleport(spawnPosition, refreshRunnable);
} else {
refreshVisibleChunks(getChunk());
refreshRunnable.run();
}
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
@ -785,18 +804,15 @@ public class Player extends LivingEntity implements CommandSender {
/**
* Sends a plugin message to the player.
* <p>
* Message encoded to UTF-8.
*
* @param channel the message channel
* @param message the message
*/
public void sendPluginMessage(@NotNull String channel, @NotNull String message) {
// Write the data
BinaryWriter writer = new BinaryWriter();
writer.writeSizedString(message);
// Retrieve the data
final byte[] data = writer.toByteArray();
sendPluginMessage(channel, data);
final byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
sendPluginMessage(channel, bytes);
}
@Override
@ -950,7 +966,7 @@ public class Player extends LivingEntity implements CommandSender {
* @param header the header text, null to set empty
* @param footer the footer text, null to set empty
*/
public void sendHeaderFooter(@Nullable ColoredText header, @Nullable ColoredText footer) {
public void sendHeaderFooter(@Nullable JsonMessage header, @Nullable JsonMessage footer) {
PlayerListHeaderAndFooterPacket playerListHeaderAndFooterPacket = new PlayerListHeaderAndFooterPacket();
playerListHeaderAndFooterPacket.emptyHeader = header == null;
playerListHeaderAndFooterPacket.emptyFooter = footer == null;
@ -967,7 +983,7 @@ public class Player extends LivingEntity implements CommandSender {
* @param action the action of the title (where to show it)
* @see #sendTitleTime(int, int, int) to specify the display time
*/
private void sendTitle(@NotNull ColoredText text, @NotNull TitlePacket.Action action) {
private void sendTitle(@NotNull JsonMessage text, @NotNull TitlePacket.Action action) {
TitlePacket titlePacket = new TitlePacket();
titlePacket.action = action;
@ -995,7 +1011,7 @@ public class Player extends LivingEntity implements CommandSender {
* @param subtitle the subtitle message
* @see #sendTitleTime(int, int, int) to specify the display time
*/
public void sendTitleSubtitleMessage(@NotNull ColoredText title, @NotNull ColoredText subtitle) {
public void sendTitleSubtitleMessage(@NotNull JsonMessage title, @NotNull JsonMessage subtitle) {
sendTitle(title, TitlePacket.Action.SET_TITLE);
sendTitle(subtitle, TitlePacket.Action.SET_SUBTITLE);
}
@ -1006,7 +1022,7 @@ public class Player extends LivingEntity implements CommandSender {
* @param title the title message
* @see #sendTitleTime(int, int, int) to specify the display time
*/
public void sendTitleMessage(@NotNull ColoredText title) {
public void sendTitleMessage(@NotNull JsonMessage title) {
sendTitle(title, TitlePacket.Action.SET_TITLE);
}
@ -1016,7 +1032,7 @@ public class Player extends LivingEntity implements CommandSender {
* @param subtitle the subtitle message
* @see #sendTitleTime(int, int, int) to specify the display time
*/
public void sendSubtitleMessage(@NotNull ColoredText subtitle) {
public void sendSubtitleMessage(@NotNull JsonMessage subtitle) {
sendTitle(subtitle, TitlePacket.Action.SET_SUBTITLE);
}
@ -1026,7 +1042,7 @@ public class Player extends LivingEntity implements CommandSender {
* @param actionBar the action bar message
* @see #sendTitleTime(int, int, int) to specify the display time
*/
public void sendActionBarMessage(@NotNull ColoredText actionBar) {
public void sendActionBarMessage(@NotNull JsonMessage actionBar) {
sendTitle(actionBar, TitlePacket.Action.SET_ACTION_BAR);
}
@ -1064,6 +1080,30 @@ public class Player extends LivingEntity implements CommandSender {
playerConnection.sendPacket(titlePacket);
}
/**
* Opens a book ui for the player with the given book metadata.
*
* @param bookMeta The metadata of the book to open
*/
public void openBook(@NotNull WrittenBookMeta bookMeta) {
// Set book in offhand
final ItemStack writtenBook = new ItemStack(Material.WRITTEN_BOOK, (byte) 1);
writtenBook.setItemMeta(bookMeta);
final SetSlotPacket setSlotPacket = new SetSlotPacket();
setSlotPacket.windowId = 0;
setSlotPacket.slot = 45;
setSlotPacket.itemStack = writtenBook;
this.playerConnection.sendPacket(setSlotPacket);
// Open the book
final OpenBookPacket openBookPacket = new OpenBookPacket();
openBookPacket.hand = Hand.OFF;
this.playerConnection.sendPacket(openBookPacket);
// Update inventory to remove book (which the actual inventory does not have)
this.inventory.update();
}
@Override
public boolean isImmune(@NotNull DamageType type) {
if (!getGameMode().canTakeDamage()) {
@ -1073,9 +1113,12 @@ public class Player extends LivingEntity implements CommandSender {
}
@Override
protected void onAttributeChanged(@NotNull final AttributeInstance instance) {
if (instance.getAttribute().isShared() && playerConnection != null)
protected void onAttributeChanged(@NotNull final AttributeInstance attributeInstance) {
if (attributeInstance.getAttribute().isShared() &&
playerConnection != null &&
playerConnection.getConnectionState() == ConnectionState.PLAY) {
playerConnection.sendPacket(getPropertiesPacket());
}
}
@Override
@ -1173,7 +1216,7 @@ public class Player extends LivingEntity implements CommandSender {
* @return the player display name, null means that {@link #getUsername()} is displayed
*/
@Nullable
public ColoredText getDisplayName() {
public JsonMessage getDisplayName() {
return displayName;
}
@ -1184,7 +1227,7 @@ public class Player extends LivingEntity implements CommandSender {
*
* @param displayName the display name, null to display the username
*/
public void setDisplayName(@Nullable ColoredText displayName) {
public void setDisplayName(@Nullable JsonMessage displayName) {
this.displayName = displayName;
PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_DISPLAY_NAME);
@ -1276,12 +1319,12 @@ public class Player extends LivingEntity implements CommandSender {
}
/**
* Changes the internal player name, used for the {@link PlayerPreLoginEvent}
* Changes the internal player name, used for the {@link AsyncPlayerPreLoginEvent}
* mostly unsafe outside of it.
*
* @param username the new player name
*/
protected void setUsername(@NotNull String username) {
public void setUsernameField(@NotNull String username) {
this.username = username;
}
@ -1316,7 +1359,6 @@ public class Player extends LivingEntity implements CommandSender {
* @param resourcePack the resource pack
*/
public void setResourcePack(@NotNull ResourcePack resourcePack) {
Check.notNull(resourcePack, "The resource pack cannot be null");
final String url = resourcePack.getUrl();
final String hash = resourcePack.getHash();
@ -1551,7 +1593,7 @@ public class Player extends LivingEntity implements CommandSender {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
instance.loadOptionalChunk(chunkX, chunkZ, chunk -> {
this.instance.loadOptionalChunk(chunkX, chunkZ, chunk -> {
if (chunk == null) {
// Cannot load chunk (auto load is not enabled)
return;
@ -1586,24 +1628,15 @@ public class Player extends LivingEntity implements CommandSender {
});
// Manage entities in unchecked chunks
final long[] chunksInRange = ChunkUtils.getChunksInRange(newChunk.toPosition(), entityViewDistance);
EntityUtils.forEachRange(instance, newChunk.toPosition(), entityViewDistance, entity -> {
if (entity.isAutoViewable() && !entity.viewers.contains(this)) {
entity.addViewer(this);
}
for (long chunkIndex : chunksInRange) {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null)
continue;
instance.getChunkEntities(chunk).forEach(entity -> {
if (isAutoViewable() && !entity.viewers.contains(this)) {
entity.addViewer(this);
}
if (entity instanceof Player && entity.isAutoViewable() && !viewers.contains(entity)) {
addViewer((Player) entity);
}
});
}
if (entity instanceof Player && isAutoViewable() && !viewers.contains(entity)) {
addViewer((Player) entity);
}
});
}
@ -1698,13 +1731,16 @@ public class Player extends LivingEntity implements CommandSender {
* @param gameMode the new player GameMode
*/
public void setGameMode(@NotNull GameMode gameMode) {
Check.notNull(gameMode, "GameMode cannot be null");
this.gameMode = gameMode;
sendChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.getId());
PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_GAMEMODE);
infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateGamemode(getUuid(), gameMode));
sendPacketToViewersAndSelf(infoPacket);
// Condition to prevent sending the packets before spawning the player
if (isActive()) {
sendChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.getId());
PlayerInfoPacket infoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_GAMEMODE);
infoPacket.playerInfos.add(new PlayerInfoPacket.UpdateGamemode(getUuid(), gameMode));
sendPacketToViewersAndSelf(infoPacket);
}
}
/**
@ -1723,7 +1759,6 @@ public class Player extends LivingEntity implements CommandSender {
* @param dimensionType the new player dimension
*/
protected void sendDimension(@NotNull DimensionType dimensionType) {
Check.notNull(dimensionType, "Dimension cannot be null!");
Check.argCondition(dimensionType.equals(getDimensionType()), "The dimension needs to be different than the current one!");
this.dimensionType = dimensionType;
@ -1739,11 +1774,18 @@ public class Player extends LivingEntity implements CommandSender {
*
* @param text the kick reason
*/
public void kick(@NotNull ColoredText text) {
DisconnectPacket disconnectPacket = new DisconnectPacket();
disconnectPacket.message = text;
public void kick(@NotNull JsonMessage text) {
final ConnectionState connectionState = playerConnection.getConnectionState();
// Packet type depends on the current player connection state
final ServerPacket disconnectPacket;
if (connectionState == ConnectionState.LOGIN) {
disconnectPacket = new LoginDisconnectPacket(text);
} else {
disconnectPacket = new DisconnectPacket(text);
}
playerConnection.sendPacket(disconnectPacket);
playerConnection.disconnect();
playerConnection.refreshOnline(false);
}
@ -1850,7 +1892,6 @@ public class Player extends LivingEntity implements CommandSender {
* @return true if the inventory has been opened/sent to the player, false otherwise (cancelled by event)
*/
public boolean openInventory(@NotNull Inventory inventory) {
Check.notNull(inventory, "Inventory cannot be null, use Player#closeInventory() to close current");
InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this);

View File

@ -3,12 +3,11 @@ package net.minestom.server.entity;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.minestom.server.utils.url.URLUtils;
import net.minestom.server.utils.mojang.MojangUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Objects;
/**
* Contains all the data required to store a skin.
@ -26,6 +25,43 @@ public class PlayerSkin {
this.signature = signature;
}
/**
* Gets a skin from a Mojang UUID.
*
* @param uuid Mojang UUID
* @return a player skin based on the UUID, null if not found
*/
@Nullable
public static PlayerSkin fromUuid(@NotNull String uuid) {
final JsonObject jsonObject = MojangUtils.fromUuid(uuid);
final JsonArray propertiesArray = jsonObject.get("properties").getAsJsonArray();
for (JsonElement jsonElement : propertiesArray) {
final JsonObject propertyObject = jsonElement.getAsJsonObject();
final String name = propertyObject.get("name").getAsString();
if (!name.equals("textures"))
continue;
final String textureValue = propertyObject.get("value").getAsString();
final String signatureValue = propertyObject.get("signature").getAsString();
return new PlayerSkin(textureValue, signatureValue);
}
return null;
}
/**
* Gets a skin from a Minecraft username.
*
* @param username the Minecraft username
* @return a skin based on a Minecraft username, null if not found
*/
@Nullable
public static PlayerSkin fromUsername(@NotNull String username) {
final JsonObject jsonObject = MojangUtils.fromUsername(username);
final String uuid = jsonObject.get("id").getAsString();
// Retrieve the skin data from the mojang uuid
return fromUuid(uuid);
}
/**
* Gets the skin textures value.
*
@ -44,58 +80,32 @@ public class PlayerSkin {
return signature;
}
/**
* Gets a skin from a Mojang UUID.
*
* @param uuid Mojang UUID
* @return a player skin based on the UUID, null if not found
*/
@Nullable
public static PlayerSkin fromUuid(@NotNull String uuid) {
final String url = "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false";
try {
final String response = URLUtils.getText(url);
final JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject();
final JsonArray propertiesArray = jsonObject.get("properties").getAsJsonArray();
for (JsonElement jsonElement : propertiesArray) {
final JsonObject propertyObject = jsonElement.getAsJsonObject();
final String name = propertyObject.get("name").getAsString();
if (!name.equals("textures"))
continue;
final String textureValue = propertyObject.get("value").getAsString();
final String signatureValue = propertyObject.get("signature").getAsString();
return new PlayerSkin(textureValue, signatureValue);
}
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
@Override
public String toString() {
return "PlayerSkin{" +
"textures='" + textures + '\'' +
", signature='" + signature + '\'' +
'}';
}
/**
* Gets a skin from a Minecraft username.
*
* @param username the Minecraft username
* @return a skin based on a Minecraft username, null if not found
* {@inheritDoc}
*/
@Nullable
public static PlayerSkin fromUsername(@NotNull String username) {
final String url = "https://api.mojang.com/users/profiles/minecraft/" + username;
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
PlayerSkin that = (PlayerSkin) object;
return Objects.equals(textures, that.textures) &&
Objects.equals(signature, that.signature);
}
try {
// Retrieve the mojang uuid from the name
final String response = URLUtils.getText(url);
final JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject();
final String uuid = jsonObject.get("id").getAsString();
// Retrieve the skin data from the mojang uuid
return fromUuid(uuid);
} catch (IOException e) {
e.printStackTrace();
return null;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hash(textures, signature);
}
}

View File

@ -1,18 +1,18 @@
package net.minestom.server.entity.ai.goal;
import it.unimi.dsi.fastutil.shorts.Short2ShortArrayMap;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.BlockPosition;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Random;
public class EatBlockGoal extends GoalSelector {
private static final Random RANDOM = new Random();
private final Map<Short, Short> eatBelowMap;
private final Map<Short, Short> eatInMap;
private final Short2ShortArrayMap eatBelowMap;
private final Short2ShortArrayMap eatInMap;
private final int chancePerTick;
private int eatAnimationTick;
@ -24,8 +24,8 @@ public class EatBlockGoal extends GoalSelector {
*/
public EatBlockGoal(
@NotNull EntityCreature entityCreature,
@NotNull Map<Short, Short> eatInMap,
@NotNull Map<Short, Short> eatBelowMap,
@NotNull Short2ShortArrayMap eatInMap,
@NotNull Short2ShortArrayMap eatBelowMap,
int chancePerTick) {
super(entityCreature);
this.eatInMap = eatInMap;
@ -39,7 +39,14 @@ public class EatBlockGoal extends GoalSelector {
if (RANDOM.nextInt(chancePerTick) != 0) {
return false;
}
final Instance instance = entityCreature.getInstance();
// An entity shouldn't be eating blocks on null instances.
if (instance == null) {
return false;
}
final BlockPosition blockPosition = entityCreature.getPosition().toBlockPosition();
final short blockStateIdIn = instance.getBlockStateId(blockPosition.clone().subtract(0, 1, 0));
final short blockStateIdBelow = instance.getBlockStateId(blockPosition.clone().subtract(0, 2, 0));

View File

@ -20,17 +20,20 @@ public class MeleeAttackGoal extends GoalSelector {
private long lastHit;
private final int delay;
private final TimeUnit timeUnit;
private final int range;
private boolean stop;
/**
* @param entityCreature the entity to add the goal to
* @param delay the delay between each attacks
* @param range the allowed range the entity can attack others.
* @param timeUnit the unit of the delay
*/
public MeleeAttackGoal(@NotNull EntityCreature entityCreature, int delay, @NotNull TimeUnit timeUnit) {
public MeleeAttackGoal(@NotNull EntityCreature entityCreature, int delay, int range, @NotNull TimeUnit timeUnit) {
super(entityCreature);
this.delay = delay;
this.range = range;
this.timeUnit = timeUnit;
}
@ -56,7 +59,7 @@ public class MeleeAttackGoal extends GoalSelector {
if (!stop) {
// Attack the target entity
if (entityCreature.getBoundingBox().intersect(target)) {
if (entityCreature.getDistance(target) <= range) {
if (!CooldownUtils.hasCooldown(time, lastHit, timeUnit, delay)) {
entityCreature.attack(target, true);
this.lastHit = time;

View File

@ -56,6 +56,11 @@ public class ClosestEntityTarget extends TargetSelector {
continue;
}
if (ent.isRemoved()) {
// Entity not valid
return null;
}
// Check if the entity type can be targeted
final Class<? extends Entity> clazz = ent.getClass();
boolean correct = false;

View File

@ -110,7 +110,7 @@ public class DamageType implements DataContainer {
* @return the death screen text, null to do not send anything
*/
@Nullable
public ColoredText buildDeathScreenText(@NotNull Player killed) {
public JsonMessage buildDeathScreenText(@NotNull Player killed) {
return ColoredText.of("{@death." + identifier + "}");
}

View File

@ -3,6 +3,7 @@ package net.minestom.server.entity.fakeplayer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerSpawnEvent;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.player.FakePlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.time.TimeUnit;
@ -22,23 +23,41 @@ import java.util.function.Consumer;
*/
public class FakePlayer extends Player {
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private final FakePlayerOption option;
private final FakePlayerController fakePlayerController;
private FakePlayer(@NotNull UUID uuid, @NotNull String username, @NotNull FakePlayerOption option) {
/**
* Initializes a new {@link FakePlayer} with the given {@code uuid}, {@code username} and {@code option}'s.
*
* @param uuid The unique identifier for the fake player.
* @param username The username for the fake player.
* @param option Any option for the fake player.
*/
private FakePlayer(@NotNull UUID uuid, @NotNull String username,
@NotNull FakePlayerOption option,
@Nullable Consumer<FakePlayer> spawnCallback) {
super(uuid, username, new FakePlayerConnection());
this.option = option;
this.fakePlayerController = new FakePlayerController(this);
if (option.isRegistered()) {
MinecraftServer.getConnectionManager().createPlayer(this);
if (spawnCallback != null) {
addEventCallback(PlayerSpawnEvent.class,
event -> {
if (event.isFirstSpawn()) {
spawnCallback.accept(this);
}
});
}
CONNECTION_MANAGER.startPlayState(this, option.isRegistered());
}
/**
* Inits a new {@link FakePlayer}.
* Initializes a new {@link FakePlayer}.
*
* @param uuid the FakePlayer uuid
* @param username the FakePlayer username
@ -46,20 +65,11 @@ public class FakePlayer extends Player {
*/
public static void initPlayer(@NotNull UUID uuid, @NotNull String username,
@NotNull FakePlayerOption option, @Nullable Consumer<FakePlayer> spawnCallback) {
final FakePlayer fakePlayer = new FakePlayer(uuid, username, option);
if (spawnCallback != null) {
fakePlayer.addEventCallback(PlayerSpawnEvent.class,
event -> {
if (event.isFirstSpawn()) {
spawnCallback.accept(fakePlayer);
}
});
}
new FakePlayer(uuid, username, option, spawnCallback);
}
/**
* Inits a new {@link FakePlayer} without adding it in cache.
* Initializes 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)}.
@ -82,11 +92,19 @@ public class FakePlayer extends Player {
return option;
}
/**
* Retrieves the controller for the fake player.
*
* @return The fake player's controller.
*/
@NotNull
public FakePlayerController getController() {
return fakePlayerController;
}
/**
* {@inheritDoc}
*/
@Override
protected void showPlayer(@NotNull PlayerConnection connection) {
super.showPlayer(connection);

View File

@ -12,8 +12,11 @@ import net.minestom.server.network.packet.client.play.*;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.play.KeepAlivePacket;
import net.minestom.server.network.packet.server.play.PlayerPositionAndLookPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
/**
@ -25,10 +28,24 @@ public class FakePlayerController {
private final FakePlayer fakePlayer;
/**
* Initializes a new {@link FakePlayerController} with the given {@link FakePlayer}.
*
* @param fakePlayer The fake player that should used the controller.
*/
public FakePlayerController(@NotNull FakePlayer fakePlayer) {
this.fakePlayer = fakePlayer;
}
/**
* Simulates a click in a window.
*
* @param playerInventory {@code true} if the window a {@link PlayerInventory}, otherwise {@code false}.
* @param slot The slot where the fake player should click on.
* @param button The mouse button that the fake player should used.
* @param action The action that the fake player should perform.
* @param mode The inventory operation mode that the fake player should used.
*/
public void clickWindow(boolean playerInventory, short slot, byte button, short action, int mode) {
Inventory inventory = playerInventory ? null : fakePlayer.getOpenInventory();
InventoryModifier inventoryModifier = inventory == null ? fakePlayer.getInventory() : inventory;
@ -48,6 +65,9 @@ public class FakePlayerController {
addToQueue(clickWindowPacket);
}
/**
* Closes the current opened inventory if there is any.
*/
public void closeWindow() {
Inventory openInventory = fakePlayer.getOpenInventory();
@ -56,6 +76,12 @@ public class FakePlayerController {
addToQueue(closeWindow);
}
/**
* Sends a plugin message to the player.
*
* @param channel The channel of the message.
* @param message The message data.
*/
public void sendPluginMessage(String channel, byte[] message) {
ClientPluginMessagePacket pluginMessagePacket = new ClientPluginMessagePacket();
pluginMessagePacket.channel = channel;
@ -63,10 +89,21 @@ public class FakePlayerController {
addToQueue(pluginMessagePacket);
}
/**
* Sends a plugin message to the player.
*
* @param channel The channel of the message.
* @param message The message data.
*/
public void sendPluginMessage(String channel, String message) {
sendPluginMessage(channel, message.getBytes());
}
/**
* Attacks the given {@code entity}.
*
* @param entity The entity that is to be attacked.
*/
public void attackEntity(Entity entity) {
ClientInteractEntityPacket interactEntityPacket = new ClientInteractEntityPacket();
interactEntityPacket.targetId = entity.getEntityId();
@ -74,6 +111,11 @@ public class FakePlayerController {
addToQueue(interactEntityPacket);
}
/**
* Respawns the player.
*
* @see Player#respawn()
*/
public void respawn() {
// Sending the respawn packet for some reason
// Is related to FakePlayer#showPlayer and the tablist option (probably because of the scheduler)
@ -83,24 +125,48 @@ public class FakePlayerController {
fakePlayer.respawn();
}
/**
* Changes the current held slot for the player.
*
* @param slot The slot that the player has to held.
* @throws IllegalArgumentException If {@code slot} is not between {@code 0} and {@code 8}.
*/
public void setHeldItem(short slot) {
Check.argCondition(!MathUtils.isBetween(slot, 0, 8), "Slot has to be between 0 and 8!");
ClientHeldItemChangePacket heldItemChangePacket = new ClientHeldItemChangePacket();
heldItemChangePacket.slot = slot;
addToQueue(heldItemChangePacket);
}
/**
* Sends an animation packet that animates the specified arm.
*
* @param hand The hand of the arm to be animated.
*/
public void sendArmAnimation(Player.Hand hand) {
ClientAnimationPacket animationPacket = new ClientAnimationPacket();
animationPacket.hand = hand;
addToQueue(animationPacket);
}
/**
* Uses the item in the given {@code hand}.
*
* @param hand The hand in which an ite mshould be.
*/
public void useItem(Player.Hand hand) {
ClientUseItemPacket useItemPacket = new ClientUseItemPacket();
useItemPacket.hand = hand;
addToQueue(useItemPacket);
}
/**
* Rotates the fake player.
*
* @param yaw The new yaw for the fake player.
* @param pitch The new pitch for the fake player.
*/
public void rotate(float yaw, float pitch) {
ClientPlayerRotationPacket playerRotationPacket = new ClientPlayerRotationPacket();
playerRotationPacket.yaw = yaw;
@ -109,34 +175,52 @@ public class FakePlayerController {
addToQueue(playerRotationPacket);
}
public void startDigging(BlockPosition blockPosition) {
/**
* Starts the digging process of the fake player.
*
* @param blockPosition The position of the block to be excavated.
* @param blockFace From where the block is struck.
*/
public void startDigging(BlockPosition blockPosition, BlockFace blockFace) {
ClientPlayerDiggingPacket playerDiggingPacket = new ClientPlayerDiggingPacket();
playerDiggingPacket.status = ClientPlayerDiggingPacket.Status.STARTED_DIGGING;
playerDiggingPacket.blockPosition = blockPosition;
playerDiggingPacket.blockFace = BlockFace.BOTTOM; // TODO not hardcode
playerDiggingPacket.blockFace = blockFace;
addToQueue(playerDiggingPacket);
}
public void stopDigging(BlockPosition blockPosition) {
/**
* Stops the digging process of the fake player.
*
* @param blockPosition The position of the block to be excavated.
* @param blockFace From where the block is struck.
*/
public void stopDigging(BlockPosition blockPosition, BlockFace blockFace) {
ClientPlayerDiggingPacket playerDiggingPacket = new ClientPlayerDiggingPacket();
playerDiggingPacket.status = ClientPlayerDiggingPacket.Status.CANCELLED_DIGGING;
playerDiggingPacket.blockPosition = blockPosition;
playerDiggingPacket.blockFace = BlockFace.BOTTOM; // TODO not hardcode
playerDiggingPacket.blockFace = blockFace;
addToQueue(playerDiggingPacket);
}
public void finishDigging(BlockPosition blockPosition) {
/**
* Finishes the digging process of the fake player.
*
* @param blockPosition The position of the block to be excavated.
* @param blockFace From where the block is struck.
*/
public void finishDigging(BlockPosition blockPosition, BlockFace blockFace) {
ClientPlayerDiggingPacket playerDiggingPacket = new ClientPlayerDiggingPacket();
playerDiggingPacket.status = ClientPlayerDiggingPacket.Status.FINISHED_DIGGING;
playerDiggingPacket.blockPosition = blockPosition;
playerDiggingPacket.blockFace = BlockFace.BOTTOM; // TODO not hardcode
playerDiggingPacket.blockFace = blockFace;
addToQueue(playerDiggingPacket);
}
/**
* Makes the player receives a packet
* WARNING: pretty much unsafe, used internally to redirect packets here,
* you should instead use {@link net.minestom.server.network.player.PlayerConnection#sendPacket(ServerPacket)}
* you should instead use {@link PlayerConnection#sendPacket(ServerPacket)}
*
* @param serverPacket the packet to consume
*/
@ -152,6 +236,12 @@ public class FakePlayerController {
}
}
/**
* All packets in the queue are executed in the {@link Player#update(long)} method. It is used internally to add all
* received packet from the client. Could be used to "simulate" a received packet, but to use at your own risk!
*
* @param clientPlayPacket The packet to add in the queue.
*/
private void addToQueue(ClientPlayPacket clientPlayPacket) {
this.fakePlayer.addPacketToQueue(clientPlayPacket);
}

View File

@ -2,6 +2,9 @@ package net.minestom.server.entity.fakeplayer;
import net.minestom.server.network.ConnectionManager;
/**
* Represents any options for a {@link FakePlayer}.
*/
public class FakePlayerOption {
private boolean registered = false;

View File

@ -2,6 +2,7 @@ package net.minestom.server.entity.hologram;
import net.minestom.server.Viewable;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.type.decoration.EntityArmorStand;
import net.minestom.server.instance.Instance;
@ -12,7 +13,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Set;
/**
* Represents an invisible armor stand showing a {@link ColoredText}.
* Represents an invisible armor stand showing a {@link JsonMessage}.
*/
public class Hologram implements Viewable {
@ -21,11 +22,19 @@ public class Hologram implements Viewable {
private final HologramEntity entity;
private Position position;
private ColoredText text;
private JsonMessage text;
private boolean removed;
public Hologram(Instance instance, Position spawnPosition, ColoredText text, boolean autoViewable) {
/**
* Constructs a new {@link Hologram} with the given parameters.
*
* @param instance The instance where the hologram should be spawned.
* @param spawnPosition The spawn position of this hologram.
* @param text The text of this hologram.
* @param autoViewable {@code true}if the hologram should be visible automatically, otherwise {@code false}.
*/
public Hologram(Instance instance, Position spawnPosition, JsonMessage text, boolean autoViewable) {
this.entity = new HologramEntity(spawnPosition.clone().add(0, OFFSET_Y, 0));
this.entity.setInstance(instance);
this.entity.setAutoViewable(autoViewable);
@ -34,7 +43,14 @@ public class Hologram implements Viewable {
setText(text);
}
public Hologram(Instance instance, Position spawnPosition, ColoredText text) {
/**
* Constructs a new {@link Hologram} with the given parameters.
*
* @param instance The instance where the hologram should be spawned.
* @param spawnPosition The spawn position of this hologram.
* @param text The text of this hologram.
*/
public Hologram(Instance instance, Position spawnPosition, JsonMessage text) {
this(instance, spawnPosition, text, true);
}
@ -64,7 +80,7 @@ public class Hologram implements Viewable {
*
* @return the hologram text
*/
public ColoredText getText() {
public JsonMessage getText() {
return text;
}
@ -73,7 +89,7 @@ public class Hologram implements Viewable {
*
* @param text the new hologram text
*/
public void setText(ColoredText text) {
public void setText(JsonMessage text) {
checkRemoved();
this.text = text;
this.entity.setCustomName(text);
@ -105,29 +121,46 @@ public class Hologram implements Viewable {
return entity;
}
/**
* {@inheritDoc}
*/
@Override
public boolean addViewer(@NotNull Player player) {
return entity.addViewer(player);
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeViewer(@NotNull Player player) {
return entity.removeViewer(player);
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public Set<Player> getViewers() {
return entity.getViewers();
}
/**
* @see #isRemoved()
*/
private void checkRemoved() {
Check.stateCondition(isRemoved(), "You cannot interact with a removed Hologram");
}
private static class HologramEntity extends EntityArmorStand {
public static class HologramEntity extends EntityArmorStand {
/**
* Constructs a new {@link HologramEntity} with the given {@code spawnPosition}.
*
* @param spawnPosition The spawn position of this hologram entity.
*/
public HologramEntity(Position spawnPosition) {
super(spawnPosition);
setSmall(true);

View File

@ -30,7 +30,6 @@ public interface NavigableEntity {
* @param speed define how far the entity will move
*/
default void moveTowards(@NotNull Position direction, float speed) {
Check.notNull(direction, "The direction cannot be null");
final Position position = getNavigableEntity().getPosition();
@ -86,10 +85,12 @@ public interface NavigableEntity {
* The position is cloned, if you want the entity to continually follow this position object
* you need to call this when you want the path to update.
*
* @param position the position to find the path to, null to reset the pathfinder
* @param position the position to find the path to, null to reset the pathfinder
* @param bestEffort whether to use the best-effort algorithm to the destination,
* if false then this method is more likely to return immediately
* @return true if a path has been found
*/
default boolean setPathTo(@Nullable Position position) {
default boolean setPathTo(@Nullable Position position, boolean bestEffort) {
if (position != null && getPathPosition() != null && position.isSimilar(getPathPosition())) {
// Tried to set path to the same target position
return false;
@ -108,6 +109,11 @@ public interface NavigableEntity {
return false;
}
// Can't path with a null instance.
if (instance == null) {
return false;
}
// Can't path outside of the world border
final WorldBorder worldBorder = instance.getWorldBorder();
if (!worldBorder.isInside(position)) {
@ -122,7 +128,11 @@ public interface NavigableEntity {
final Position targetPosition = position.clone();
final IPath path = pathFinder.initiatePathTo(targetPosition.getX(), targetPosition.getY(), targetPosition.getZ());
final IPath path = pathFinder.initiatePathTo(
targetPosition.getX(),
targetPosition.getY(),
targetPosition.getZ(),
bestEffort);
setPath(path);
final boolean success = path != null;
@ -131,6 +141,13 @@ public interface NavigableEntity {
return success;
}
/**
* @see #setPathTo(Position, boolean) with {@code bestEffort} sets to {@code true}.
*/
default boolean setPathTo(@Nullable Position position) {
return setPathTo(position, true);
}
default void pathFindingTick(float speed) {
final Position pathPosition = getPathPosition();
if (pathPosition != null) {

View File

@ -9,6 +9,5 @@ public class EntityCaveSpider extends EntityCreature implements Monster {
public EntityCaveSpider(Position spawnPosition) {
super(EntityType.CAVE_SPIDER, spawnPosition);
setBoundingBox(0.7f, 0.5f, 0.7f);
setEyeHeight(0.45f);
}
}

View File

@ -26,6 +26,8 @@ public class EntityCreeper extends EntityCreature implements Monster {
return packet -> {
super.getMetadataConsumer().accept(packet);
fillMetadataIndex(packet, 15);
fillMetadataIndex(packet, 16);
fillMetadataIndex(packet, 17);
};
}

View File

@ -9,6 +9,5 @@ public class EntityEndermite extends EntityCreature implements Monster {
public EntityEndermite(Position spawnPosition) {
super(EntityType.ENDERMITE, spawnPosition);
setBoundingBox(0.4f, 0.3f, 0.4f);
setEyeHeight(0.13f);
}
}

View File

@ -16,7 +16,6 @@ public class EntityGhast extends EntityCreature implements Monster {
public EntityGhast(Position spawnPosition) {
super(EntityType.GHAST, spawnPosition);
setBoundingBox(4, 4, 4);
setEyeHeight(2.6f);
}
@NotNull

View File

@ -9,6 +9,5 @@ public class EntitySilverfish extends EntityCreature implements Monster {
public EntitySilverfish(Position spawnPosition) {
super(EntityType.SILVERFISH, spawnPosition);
setBoundingBox(0.4f, 0.3f, 0.4f);
setEyeHeight(0.13f);
}
}

View File

@ -16,7 +16,6 @@ public class EntitySpider extends EntityCreature implements Monster {
public EntitySpider(Position spawnPosition) {
super(EntityType.SPIDER, spawnPosition);
setBoundingBox(1.4f, 0.9f, 1.4f);
setEyeHeight(0.65f);
}
@NotNull

View File

@ -16,7 +16,6 @@ public class EntityWitch extends EntityCreature implements Monster {
public EntityWitch(Position spawnPosition) {
super(EntityType.WITCH, spawnPosition);
setBoundingBox(0.6f, 1.95f, 0.6f);
setEyeHeight(1.62f);
}
@NotNull

View File

@ -0,0 +1,76 @@
package net.minestom.server.entity.type.other;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ObjectEntity;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
public class EntityEndCrystal extends ObjectEntity {
private BlockPosition beamTarget;
private boolean showBottom;
public EntityEndCrystal(@NotNull Position spawnPosition) {
super(EntityType.END_CRYSTAL, spawnPosition);
setBoundingBox(2f, 2f, 2f);
}
@NotNull
@Override
public Consumer<BinaryWriter> getMetadataConsumer() {
return packet -> {
super.getMetadataConsumer().accept(packet);
fillMetadataIndex(packet, 7);
fillMetadataIndex(packet, 8);
};
}
@Override
protected void fillMetadataIndex(@NotNull BinaryWriter packet, int index) {
super.fillMetadataIndex(packet, index);
if (index == 7) {
final boolean hasTarget = beamTarget != null;
packet.writeByte((byte) 7);
packet.writeByte(METADATA_OPTPOSITION);
packet.writeBoolean(hasTarget);
if (hasTarget) {
packet.writeBlockPosition(beamTarget);
}
} else if (index == 8) {
packet.writeByte((byte) 8);
packet.writeByte(METADATA_BOOLEAN);
packet.writeBoolean(showBottom);
}
}
@Nullable
public BlockPosition getBeamTarget() {
return beamTarget;
}
public void setBeamTarget(@Nullable BlockPosition beamTarget) {
this.beamTarget = beamTarget;
sendMetadataIndex(7);
}
public boolean showBottom() {
return showBottom;
}
public void setShowBottom(boolean showBottom) {
this.showBottom = showBottom;
sendMetadataIndex(8);
}
@Override
public int getObjectData() {
return 0;
}
}

View File

@ -63,6 +63,6 @@ public class EntityDamageEvent extends EntityEvent implements CancellableEvent {
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancelled;
this.cancelled = cancel;
}
}

View File

@ -0,0 +1,26 @@
package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.EntityEvent;
import net.minestom.server.potion.Potion;
import org.jetbrains.annotations.NotNull;
public class EntityPotionAddEvent extends EntityEvent {
private final Potion potion;
public EntityPotionAddEvent(@NotNull Entity entity, @NotNull Potion potion) {
super(entity);
this.potion = potion;
}
/**
* Returns the potion that was added.
*
* @return the added potion.
*/
@NotNull
public Potion getPotion() {
return potion;
}
}

View File

@ -0,0 +1,26 @@
package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.EntityEvent;
import net.minestom.server.potion.Potion;
import org.jetbrains.annotations.NotNull;
public class EntityPotionRemoveEvent extends EntityEvent {
private final Potion potion;
public EntityPotionRemoveEvent(@NotNull Entity entity, @NotNull Potion potion) {
super(entity);
this.potion = potion;
}
/**
* Returns the potion that was removed.
*
* @return the removed potion.
*/
@NotNull
public Potion getPotion() {
return potion;
}
}

View File

@ -37,8 +37,6 @@ public interface EventHandler {
* @return true if the callback collection changed as a result of the call
*/
default <E extends Event> boolean addEventCallback(@NotNull Class<E> eventClass, @NotNull EventCallback<E> eventCallback) {
Check.notNull(eventClass, "Event class cannot be null");
Check.notNull(eventCallback, "Event callback cannot be null");
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
return callbacks.add(eventCallback);
}
@ -52,8 +50,6 @@ public interface EventHandler {
* @return true if the callback was removed as a result of this call
*/
default <E extends Event> boolean removeEventCallback(@NotNull Class<E> eventClass, @NotNull EventCallback<E> eventCallback) {
Check.notNull(eventClass, "Event class cannot be null");
Check.notNull(eventCallback, "Event callback cannot be null");
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
return callbacks.remove(eventCallback);
}
@ -67,7 +63,6 @@ public interface EventHandler {
*/
@NotNull
default <E extends Event> Collection<EventCallback> getEventCallbacks(@NotNull Class<E> eventClass) {
Check.notNull(eventClass, "Event class cannot be null");
return getEventCallbacksMap().computeIfAbsent(eventClass, clazz -> new CopyOnWriteArraySet<>());
}

View File

@ -2,7 +2,6 @@ package net.minestom.server.event.player;
import net.minestom.server.entity.Player;
import net.minestom.server.event.PlayerEvent;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@ -11,12 +10,12 @@ import java.util.UUID;
* Called before the player initialization, it can be used to kick the player before any connection
* or to change his final username/uuid.
*/
public class PlayerPreLoginEvent extends PlayerEvent {
public class AsyncPlayerPreLoginEvent extends PlayerEvent {
private String username;
private UUID playerUuid;
public PlayerPreLoginEvent(@NotNull Player player, @NotNull String username, @NotNull UUID playerUuid) {
public AsyncPlayerPreLoginEvent(@NotNull Player player, @NotNull String username, @NotNull UUID playerUuid) {
super(player);
this.username = username;
this.playerUuid = playerUuid;
@ -38,7 +37,6 @@ public class PlayerPreLoginEvent extends PlayerEvent {
* @param username the new player username
*/
public void setUsername(@NotNull String username) {
Check.notNull(username, "The player username cannot be null");
this.username = username;
}
@ -58,7 +56,6 @@ public class PlayerPreLoginEvent extends PlayerEvent {
* @param playerUuid the new player uuid
*/
public void setPlayerUuid(@NotNull UUID playerUuid) {
Check.notNull(playerUuid, "The player uuid cannot be null");
this.playerUuid = playerUuid;
}
}

View File

@ -1,13 +1,17 @@
package net.minestom.server.event.player;
import net.minestom.server.MinecraftServer;
import net.minestom.server.data.Data;
import net.minestom.server.entity.Player;
import net.minestom.server.event.CancellableEvent;
import net.minestom.server.event.PlayerEvent;
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.utils.BlockPosition;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Called when a player tries placing a block.
@ -18,6 +22,7 @@ public class PlayerBlockPlaceEvent extends PlayerEvent implements CancellableEve
private short blockStateId;
private short customBlockId;
private Data blockData;
private final BlockPosition blockPosition;
private final Player.Hand hand;
@ -25,11 +30,10 @@ public class PlayerBlockPlaceEvent extends PlayerEvent implements CancellableEve
private boolean cancelled;
public PlayerBlockPlaceEvent(@NotNull Player player, short blockStateId, short customBlockId,
public PlayerBlockPlaceEvent(@NotNull Player player, @NotNull Block block,
@NotNull BlockPosition blockPosition, @NotNull Player.Hand hand) {
super(player);
this.blockStateId = blockStateId;
this.customBlockId = customBlockId;
this.blockStateId = block.getBlockId();
this.blockPosition = blockPosition;
this.hand = hand;
this.consumeBlock = true;
@ -52,6 +56,7 @@ public class PlayerBlockPlaceEvent extends PlayerEvent implements CancellableEve
*/
public void setCustomBlock(short customBlockId) {
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
Check.notNull(customBlock, "The custom block with the id '" + customBlockId + "' does not exist");
setCustomBlock(customBlock);
}
@ -62,6 +67,7 @@ public class PlayerBlockPlaceEvent extends PlayerEvent implements CancellableEve
*/
public void setCustomBlock(@NotNull String customBlockId) {
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
Check.notNull(customBlock, "The custom block with the identifier '" + customBlockId + "' does not exist");
setCustomBlock(customBlock);
}
@ -104,6 +110,25 @@ public class PlayerBlockPlaceEvent extends PlayerEvent implements CancellableEve
this.blockStateId = blockStateId;
}
/**
* Gets the data that the (not placed yet) block should have
*
* @return the block data, null if not any
*/
@Nullable
public Data getBlockData() {
return blockData;
}
/**
* Sets the data of the block to place.
*
* @param blockData the block data, null if not any
*/
public void setBlockData(@Nullable Data blockData) {
this.blockData = blockData;
}
/**
* Gets the block position.
*

View File

@ -1,6 +1,6 @@
package net.minestom.server.event.player;
import net.minestom.server.chat.RichMessage;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.entity.Player;
import net.minestom.server.event.CancellableEvent;
import net.minestom.server.event.PlayerEvent;
@ -19,7 +19,7 @@ public class PlayerChatEvent extends PlayerEvent implements CancellableEvent {
private final Collection<Player> recipients;
private String message;
private Function<PlayerChatEvent, RichMessage> chatFormat;
private Function<PlayerChatEvent, JsonMessage> chatFormat;
private boolean cancelled;
@ -34,7 +34,7 @@ public class PlayerChatEvent extends PlayerEvent implements CancellableEvent {
*
* @param chatFormat the custom chat format, null to use the default one
*/
public void setChatFormat(@Nullable Function<PlayerChatEvent, RichMessage> chatFormat) {
public void setChatFormat(@Nullable Function<PlayerChatEvent, JsonMessage> chatFormat) {
this.chatFormat = chatFormat;
}
@ -77,7 +77,7 @@ public class PlayerChatEvent extends PlayerEvent implements CancellableEvent {
* @return the chat format which will be used, null if this is the default one
*/
@Nullable
public Function<PlayerChatEvent, RichMessage> getChatFormatFunction() {
public Function<PlayerChatEvent, JsonMessage> getChatFormatFunction() {
return chatFormat;
}

View File

@ -0,0 +1,61 @@
package net.minestom.server.event.player;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.chat.JsonMessage;
import net.minestom.server.entity.Player;
import net.minestom.server.event.PlayerEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Called when a player die in {@link Player#kill()}.
*/
public class PlayerDeathEvent extends PlayerEvent {
private JsonMessage deathText;
private JsonMessage chatMessage;
public PlayerDeathEvent(@NotNull Player player, JsonMessage deathText, JsonMessage chatMessage) {
super(player);
this.deathText = deathText;
this.chatMessage = chatMessage;
}
/**
* Gets the text displayed in the death screen.
*
* @return the death text, can be null
*/
@Nullable
public JsonMessage getDeathText() {
return deathText;
}
/**
* Changes the text displayed in the death screen.
*
* @param deathText the death text to display, null to remove
*/
public void setDeathText(@Nullable JsonMessage deathText) {
this.deathText = deathText;
}
/**
* Gets the message sent to chat.
*
* @return the death chat message
*/
@Nullable
public JsonMessage getChatMessage() {
return chatMessage;
}
/**
* Changes the text sent in chat
*
* @param chatMessage the death message to send, null to remove
*/
public void setChatMessage(@Nullable JsonMessage chatMessage) {
this.chatMessage = chatMessage;
}
}

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