Merge branch 'master' into area

This commit is contained in:
themode 2021-11-09 02:37:40 +01:00
commit 9a390aadf1
298 changed files with 5843 additions and 4894 deletions

4
.github/README.md vendored
View File

@ -4,7 +4,7 @@
[![license](https://img.shields.io/github/license/Minestom/Minestom?style=for-the-badge&color=b2204c)](../LICENSE) [![license](https://img.shields.io/github/license/Minestom/Minestom?style=for-the-badge&color=b2204c)](../LICENSE)
[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=for-the-badge)](https://github.com/RichardLitt/standard-readme) [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=for-the-badge)](https://github.com/RichardLitt/standard-readme)
[![javadocs](https://img.shields.io/badge/documentation-javadocs-4d7a97?style=for-the-badge)](https://minestom.github.io/Minestom/) [![javadocs](https://img.shields.io/badge/documentation-javadocs-4d7a97?style=for-the-badge)](https://minestom.github.io/Minestom/)
[![wiki](https://img.shields.io/badge/documentation-wiki-74aad6?style=for-the-badge)](https://wiki.minestom.com/) [![wiki](https://img.shields.io/badge/documentation-wiki-74aad6?style=for-the-badge)](https://wiki.minestom.net/)
[![discord-banner](https://img.shields.io/discord/706185253441634317?label=discord&style=for-the-badge&color=7289da)](https://discord.gg/pkFRvqB) [![discord-banner](https://img.shields.io/discord/706185253441634317?label=discord&style=for-the-badge&color=7289da)](https://discord.gg/pkFRvqB)
Minestom is a complete rewrite of Minecraft server software, open-source and without any code from Mojang. Minestom is a complete rewrite of Minecraft server software, open-source and without any code from Mojang.
@ -33,7 +33,7 @@ This means you need to add Minestom as a dependency, add your code and compile b
# Usage # Usage
An example of how to use the Minestom library is available [here](/src/test/java/demo). An example of how to use the Minestom library is available [here](/src/test/java/demo).
Alternatively you can check the official [wiki](https://wiki.minestom.com/) or the [javadocs](https://minestom.github.io/Minestom/). Alternatively you can check the official [wiki](https://wiki.minestom.net/) or the [javadocs](https://minestom.github.io/Minestom/).
# Why Minestom? # Why Minestom?
Minecraft has evolved a lot since its release, most of the servers today do not take advantage of vanilla features and even have to struggle because of them. Our target audience is those who want to make a completely different server compared to vanilla Minecraft such as survival or creative building. Minecraft has evolved a lot since its release, most of the servers today do not take advantage of vanilla features and even have to struggle because of them. Our target audience is those who want to make a completely different server compared to vanilla Minecraft such as survival or creative building.

View File

@ -11,10 +11,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 17
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 17
- name: Run java checkstyle - name: Run java checkstyle
uses: nikitasavinov/checkstyle-action@0.3.1 uses: nikitasavinov/checkstyle-action@0.3.1
with: with:

View File

@ -11,10 +11,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 17
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 17
- name: Build javadoc - name: Build javadoc
run: gradle javadoc run: gradle javadoc

View File

@ -13,11 +13,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 16 - name: Set up JDK 17
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
distribution: 'adopt' distribution: 'zulu'
java-version: 16 java-version: 17
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Setup gradle cache - name: Setup gradle cache

View File

@ -0,0 +1,12 @@
name: Trigger Jitpack Build
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger Jitpack Build
run: curl "https://jitpack.io/com/github/Minestom/Minestom/${GITHUB_SHA:0:10}/build.log"

View File

@ -3,14 +3,14 @@ import org.gradle.internal.os.OperatingSystem
plugins { plugins {
id 'java-library' id 'java-library'
id 'maven-publish' id 'maven-publish'
id 'org.jetbrains.kotlin.jvm' version '1.5.0' id 'org.jetbrains.kotlin.jvm' version '1.5.31'
id 'checkstyle' //id 'checkstyle'
} }
group 'net.minestom.server' group 'net.minestom.server'
version '1.0' version '1.0'
sourceCompatibility = 11 sourceCompatibility = 17
project.ext.lwjglVersion = "3.2.3" project.ext.lwjglVersion = "3.2.3"
switch (OperatingSystem.current()) { switch (OperatingSystem.current()) {
@ -55,10 +55,10 @@ allprojects {
} }
} }
checkstyle { //checkstyle {
toolVersion "8.42" // toolVersion "8.42"
configFile file("${projectDir}/minestom_checks.xml") // configFile file("${projectDir}/minestom_checks.xml")
} //}
} }
sourceSets { sourceSets {
@ -100,17 +100,17 @@ tasks.withType(Zip).configureEach {
dependencies { dependencies {
// Junit Testing Framework // Junit Testing Framework
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.7.2') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.1')
// Only here to ensure J9 module support for extensions and our classloaders // Only here to ensure J9 module support for extensions and our classloaders
testCompileOnly 'org.mockito:mockito-core:3.11.1' testCompileOnly 'org.mockito:mockito-core:4.0.0'
// https://mvnrepository.com/artifact/it.unimi.dsi/fastutil // https://mvnrepository.com/artifact/it.unimi.dsi/fastutil
api 'it.unimi.dsi:fastutil:8.5.4' api 'it.unimi.dsi:fastutil:8.5.6'
// https://mvnrepository.com/artifact/com.google.code.gson/gson // https://mvnrepository.com/artifact/com.google.code.gson/gson
api 'com.google.code.gson:gson:2.8.7' api 'com.google.code.gson:gson:2.8.9'
// Noise library for terrain generation // Noise library for terrain generation
// https://jitpack.io/#Articdive/Jnoise // https://jitpack.io/#Articdive/Jnoise
@ -126,10 +126,13 @@ dependencies {
// https://mvnrepository.com/artifact/org.jline/jline-terminal-jansi // https://mvnrepository.com/artifact/org.jline/jline-terminal-jansi
implementation group: 'org.jline', name: 'jline-terminal-jansi', version: '3.20.0' implementation group: 'org.jline', name: 'jline-terminal-jansi', version: '3.20.0'
implementation 'com.github.ben-manes.caffeine:caffeine:3.0.3' implementation 'com.github.ben-manes.caffeine:caffeine:3.0.4'
// https://mvnrepository.com/artifact/com.zaxxer/SparseBitSet
implementation group: 'com.zaxxer', name: 'SparseBitSet', version: '1.2'
// Guava 21.0+ required for Mixin // Guava 21.0+ required for Mixin
api 'com.google.guava:guava:30.1.1-jre' api 'com.google.guava:guava:31.0.1-jre'
// Code modification // Code modification
api "org.ow2.asm:asm:${asmVersion}" api "org.ow2.asm:asm:${asmVersion}"
@ -172,7 +175,7 @@ dependencies {
lwjglApi "org.lwjgl:lwjgl-opengles" lwjglApi "org.lwjgl:lwjgl-opengles"
lwjglApi "org.lwjgl:lwjgl-glfw" lwjglApi "org.lwjgl:lwjgl-glfw"
lwjglApi "org.lwjgl:lwjgl-glfw" lwjglApi "org.lwjgl:lwjgl-glfw"
lwjglApi 'org.joml:joml:1.9.25' lwjglApi 'org.joml:joml:1.10.2'
lwjglRuntimeOnly "org.lwjgl:lwjgl::$lwjglNatives" lwjglRuntimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
lwjglRuntimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" lwjglRuntimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
lwjglRuntimeOnly "org.lwjgl:lwjgl-opengles::$lwjglNatives" lwjglRuntimeOnly "org.lwjgl:lwjgl-opengles::$lwjglNatives"

View File

@ -7,7 +7,7 @@ plugins {
group 'net.minestom.server' group 'net.minestom.server'
version '1.0' version '1.0'
sourceCompatibility = 1.11 sourceCompatibility = 17
application { application {
mainClass.set("net.minestom.codegen.Generators") mainClass.set("net.minestom.codegen.Generators")

View File

@ -17,15 +17,15 @@ public class Generators {
} }
File outputFolder = new File(args[0]); File outputFolder = new File(args[0]);
var generator = new CodeGenerator(outputFolder); var generator = new CodeGenerator(outputFolder);
generator.generate(resource("blocks.json"), "net.minestom.server.instance.block", "Block", "BlockImpl", "BlockConstants"); generator.generate(resource("blocks.json"), "net.minestom.server.instance.block", "Block", "BlockImpl", "Blocks");
generator.generate(resource("items.json"), "net.minestom.server.item", "Material", "MaterialImpl", "MaterialConstants"); generator.generate(resource("items.json"), "net.minestom.server.item", "Material", "MaterialImpl", "Materials");
generator.generate(resource("entities.json"), "net.minestom.server.entity", "EntityType", "EntityTypeImpl", "EntityTypeConstants"); generator.generate(resource("entities.json"), "net.minestom.server.entity", "EntityType", "EntityTypeImpl", "EntityTypes");
generator.generate(resource("enchantments.json"), "net.minestom.server.item", "Enchantment", "EnchantmentImpl", "EnchantmentConstants"); generator.generate(resource("enchantments.json"), "net.minestom.server.item", "Enchantment", "EnchantmentImpl", "Enchantments");
generator.generate(resource("potion_effects.json"), "net.minestom.server.potion", "PotionEffect", "PotionEffectImpl", "PotionEffectConstants"); generator.generate(resource("potion_effects.json"), "net.minestom.server.potion", "PotionEffect", "PotionEffectImpl", "PotionEffects");
generator.generate(resource("potions.json"), "net.minestom.server.potion", "PotionType", "PotionTypeImpl", "PotionTypeConstants"); generator.generate(resource("potions.json"), "net.minestom.server.potion", "PotionType", "PotionTypeImpl", "PotionTypes");
generator.generate(resource("particles.json"), "net.minestom.server.particle", "Particle", "ParticleImpl", "ParticleConstants"); generator.generate(resource("particles.json"), "net.minestom.server.particle", "Particle", "ParticleImpl", "Particles");
generator.generate(resource("sounds.json"), "net.minestom.server.sound", "SoundEvent", "SoundEventImpl", "SoundEventConstants"); generator.generate(resource("sounds.json"), "net.minestom.server.sound", "SoundEvent", "SoundEventImpl", "SoundEvents");
generator.generate(resource("custom_statistics.json"), "net.minestom.server.statistic", "StatisticType", "StatisticTypeImpl", "StatisticTypeConstants"); generator.generate(resource("custom_statistics.json"), "net.minestom.server.statistic", "StatisticType", "StatisticTypeImpl", "StatisticTypes");
// Generate fluids // Generate fluids
new FluidGenerator(resource("fluids.json"), outputFolder).generate(); new FluidGenerator(resource("fluids.json"), outputFolder).generate();

View File

@ -1,8 +1,8 @@
# Update this version with every release. It is purely used for the code generator and data dependency. # Update this version with every release. It is purely used for the code generator and data dependency.
mcVersion = 1.17 mcVersion = 1.17
asmVersion=9.0 asmVersion=9.2
mixinVersion=0.8.1 mixinVersion=0.8.4
hephaistosVersion=v1.1.8 hephaistosVersion=v1.1.8
kotlinVersion=1.5.0 kotlinVersion=1.5.31
adventureVersion=4.8.1 adventureVersion=4.9.3

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-rc-2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

269
gradlew vendored Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright ? 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,67 +17,101 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions <20>á$var<61>â, <20>á${var}<7D>â, <20>á${var:-default}<7D>â, <20>á${var+SET}<7D>â,
# <20>á${var#prefix}<7D>â, <20>á${var%suffix}<7D>â, and <20>á$( cmd )<29>â;
# * compound commands having a testable exit status, especially <20>ácase<73>â;
# * various built-in commands including <20>ácommand<6E>â, <20>áset<65>â, and <20>áulimit<69>â.
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@ -106,80 +140,95 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD=$( ulimit -H -n ) ||
MAX_FD="$MAX_FD_LIMIT" warn "Could not query maximum file descriptor limit"
fi esac
ulimit -n $MAX_FD case $MAX_FD in #(
if [ $? -ne 0 ] ; then '' | soft) :;; #(
warn "Could not set maximum file descriptor limit: $MAX_FD" *)
fi ulimit -n "$MAX_FD" ||
else warn "Could not set maximum file descriptor limit to $MAX_FD"
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# 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
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
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" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

View File

@ -1,2 +1,5 @@
jdk: before_install:
- openjdk11 - source "$HOME/.sdkman/bin/sdkman-init.sh"
- sdk update
- sdk install java 17-open
- sdk use java 17-open

View File

@ -4,7 +4,7 @@ package net.minestom.server.entity;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface EntityTypeConstants { interface EntityTypes {
EntityType AREA_EFFECT_CLOUD = EntityTypeImpl.get("minecraft:area_effect_cloud"); EntityType AREA_EFFECT_CLOUD = EntityTypeImpl.get("minecraft:area_effect_cloud");
EntityType ARMOR_STAND = EntityTypeImpl.get("minecraft:armor_stand"); EntityType ARMOR_STAND = EntityTypeImpl.get("minecraft:armor_stand");

View File

@ -4,7 +4,7 @@ package net.minestom.server.instance.block;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface BlockConstants { interface Blocks {
Block AIR = BlockImpl.get("minecraft:air"); Block AIR = BlockImpl.get("minecraft:air");
Block STONE = BlockImpl.get("minecraft:stone"); Block STONE = BlockImpl.get("minecraft:stone");

View File

@ -4,7 +4,7 @@ package net.minestom.server.item;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface EnchantmentConstants { interface Enchantments {
Enchantment PROTECTION = EnchantmentImpl.get("minecraft:protection"); Enchantment PROTECTION = EnchantmentImpl.get("minecraft:protection");
Enchantment FIRE_PROTECTION = EnchantmentImpl.get("minecraft:fire_protection"); Enchantment FIRE_PROTECTION = EnchantmentImpl.get("minecraft:fire_protection");

View File

@ -4,7 +4,7 @@ package net.minestom.server.item;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface MaterialConstants { interface Materials {
Material AIR = MaterialImpl.get("minecraft:air"); Material AIR = MaterialImpl.get("minecraft:air");
Material STONE = MaterialImpl.get("minecraft:stone"); Material STONE = MaterialImpl.get("minecraft:stone");

View File

@ -4,7 +4,7 @@ package net.minestom.server.particle;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface ParticleConstants { interface Particles {
Particle AMBIENT_ENTITY_EFFECT = ParticleImpl.get("minecraft:ambient_entity_effect"); Particle AMBIENT_ENTITY_EFFECT = ParticleImpl.get("minecraft:ambient_entity_effect");
Particle ANGRY_VILLAGER = ParticleImpl.get("minecraft:angry_villager"); Particle ANGRY_VILLAGER = ParticleImpl.get("minecraft:angry_villager");

View File

@ -4,7 +4,7 @@ package net.minestom.server.potion;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface PotionEffectConstants { interface PotionEffects {
PotionEffect SPEED = PotionEffectImpl.get("minecraft:speed"); PotionEffect SPEED = PotionEffectImpl.get("minecraft:speed");
PotionEffect SLOWNESS = PotionEffectImpl.get("minecraft:slowness"); PotionEffect SLOWNESS = PotionEffectImpl.get("minecraft:slowness");

View File

@ -4,7 +4,7 @@ package net.minestom.server.potion;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface PotionTypeConstants { interface PotionTypes {
PotionType EMPTY = PotionTypeImpl.get("minecraft:empty"); PotionType EMPTY = PotionTypeImpl.get("minecraft:empty");
PotionType WATER = PotionTypeImpl.get("minecraft:water"); PotionType WATER = PotionTypeImpl.get("minecraft:water");

View File

@ -4,7 +4,7 @@ package net.minestom.server.sound;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface SoundEventConstants { interface SoundEvents {
SoundEvent AMBIENT_CAVE = SoundEventImpl.get("minecraft:ambient.cave"); SoundEvent AMBIENT_CAVE = SoundEventImpl.get("minecraft:ambient.cave");
SoundEvent AMBIENT_BASALT_DELTAS_ADDITIONS = SoundEventImpl.get("minecraft:ambient.basalt_deltas.additions"); SoundEvent AMBIENT_BASALT_DELTAS_ADDITIONS = SoundEventImpl.get("minecraft:ambient.basalt_deltas.additions");

View File

@ -4,7 +4,7 @@ package net.minestom.server.statistic;
* Code autogenerated, do not edit! * Code autogenerated, do not edit!
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
interface StatisticTypeConstants { interface StatisticTypes {
StatisticType LEAVE_GAME = StatisticTypeImpl.get("minecraft:leave_game"); StatisticType LEAVE_GAME = StatisticTypeImpl.get("minecraft:leave_game");
StatisticType PLAY_TIME = StatisticTypeImpl.get("minecraft:play_time"); StatisticType PLAY_TIME = StatisticTypeImpl.get("minecraft:play_time");

View File

@ -4,6 +4,7 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.map.Framebuffer; import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.MapColors; import net.minestom.server.map.MapColors;
import net.minestom.server.timer.Task; import net.minestom.server.timer.Task;
import net.minestom.server.utils.thread.ThreadBindingExecutor;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
import org.lwjgl.PointerBuffer; import org.lwjgl.PointerBuffer;
import org.lwjgl.glfw.GLFWErrorCallback; import org.lwjgl.glfw.GLFWErrorCallback;
@ -27,6 +28,8 @@ public abstract class GLFWCapableBuffer {
private final ByteBuffer colorsBuffer; private final ByteBuffer colorsBuffer;
private boolean onlyMapColors; private boolean onlyMapColors;
private static ThreadBindingExecutor threadBindingPool;
protected GLFWCapableBuffer(int width, int height) { protected GLFWCapableBuffer(int width, int height) {
this(width, height, GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API); this(width, height, GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API);
} }
@ -60,6 +63,12 @@ public abstract class GLFWCapableBuffer {
throw new RuntimeException("("+errcode+") Failed to create GLFW Window."); throw new RuntimeException("("+errcode+") Failed to create GLFW Window.");
} }
} }
synchronized(GLFWCapableBuffer.class) {
if(threadBindingPool == null) {
threadBindingPool = new ThreadBindingExecutor(MinecraftServer.THREAD_COUNT_SCHEDULER, MinecraftServer.THREAD_NAME_SCHEDULER);
}
}
} }
public GLFWCapableBuffer unbindContextFromThread() { public GLFWCapableBuffer unbindContextFromThread() {
@ -80,14 +89,17 @@ public abstract class GLFWCapableBuffer {
return MinecraftServer.getSchedulerManager() return MinecraftServer.getSchedulerManager()
.buildTask(new Runnable() { .buildTask(new Runnable() {
private boolean first = true; private boolean first = true;
private final Runnable subAction = () -> {
@Override
public void run() {
if(first) { if(first) {
changeRenderingThreadToCurrent(); changeRenderingThreadToCurrent();
first = false; first = false;
} }
render(rendering); render(rendering);
};
@Override
public void run() {
threadBindingPool.execute(subAction);
} }
}) })
.repeat(period) .repeat(period)

View File

@ -6,14 +6,12 @@ import net.minestom.server.command.CommandManager;
import net.minestom.server.data.DataManager; import net.minestom.server.data.DataManager;
import net.minestom.server.data.DataType; import net.minestom.server.data.DataType;
import net.minestom.server.data.SerializableData; import net.minestom.server.data.SerializableData;
import net.minestom.server.entity.Player;
import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.exception.ExceptionManager; import net.minestom.server.exception.ExceptionManager;
import net.minestom.server.extensions.Extension; import net.minestom.server.extensions.Extension;
import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.extensions.ExtensionManager;
import net.minestom.server.fluid.Fluid; import net.minestom.server.fluid.Fluid;
import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.gamedata.tags.TagManager;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.block.BlockManager; import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.rule.BlockPlacementRule; import net.minestom.server.instance.block.rule.BlockPlacementRule;
@ -23,7 +21,6 @@ import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.packet.server.play.PluginMessagePacket; import net.minestom.server.network.packet.server.play.PluginMessagePacket;
import net.minestom.server.network.packet.server.play.ServerDifficultyPacket; import net.minestom.server.network.packet.server.play.ServerDifficultyPacket;
import net.minestom.server.network.packet.server.play.UpdateViewDistancePacket;
import net.minestom.server.network.socket.Server; import net.minestom.server.network.socket.Server;
import net.minestom.server.ping.ResponseDataConsumer; import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.recipe.RecipeManager; import net.minestom.server.recipe.RecipeManager;
@ -31,10 +28,10 @@ import net.minestom.server.scoreboard.TeamManager;
import net.minestom.server.storage.StorageLocation; import net.minestom.server.storage.StorageLocation;
import net.minestom.server.storage.StorageManager; import net.minestom.server.storage.StorageManager;
import net.minestom.server.terminal.MinestomTerminal; import net.minestom.server.terminal.MinestomTerminal;
import net.minestom.server.thread.MinestomThreadPool;
import net.minestom.server.timer.SchedulerManager; import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.Difficulty; import net.minestom.server.world.Difficulty;
import net.minestom.server.world.DimensionTypeManager; import net.minestom.server.world.DimensionTypeManager;
@ -118,10 +115,10 @@ public final class MinecraftServer {
// Data // Data
private static boolean initialized; private static boolean initialized;
private static boolean started; private static boolean started;
private static boolean stopping; private static volatile boolean stopping;
private static int chunkViewDistance = 8; private static int chunkViewDistance = Integer.getInteger("minestom.chunk-view-distance", 8);
private static int entityViewDistance = 5; private static int entityViewDistance = Integer.getInteger("minestom.entity-view-distance", 5);
private static int compressionThreshold = 256; private static int compressionThreshold = 256;
private static boolean groupedPacket = true; private static boolean groupedPacket = true;
private static boolean terminalEnabled = System.getProperty("minestom.terminal.disabled") == null; private static boolean terminalEnabled = System.getProperty("minestom.terminal.disabled") == null;
@ -337,6 +334,7 @@ public final class MinecraftServer {
* *
* @return the data manager * @return the data manager
*/ */
@Deprecated
public static DataManager getDataManager() { public static DataManager getDataManager() {
checkInitStatus(dataManager); checkInitStatus(dataManager);
return dataManager; return dataManager;
@ -446,20 +444,14 @@ public final class MinecraftServer {
* *
* @param chunkViewDistance the new chunk view distance * @param chunkViewDistance the new chunk view distance
* @throws IllegalArgumentException if {@code chunkViewDistance} is not between 2 and 32 * @throws IllegalArgumentException if {@code chunkViewDistance} is not between 2 and 32
* @deprecated should instead be defined with a java property
*/ */
@Deprecated
public static void setChunkViewDistance(int chunkViewDistance) { public static void setChunkViewDistance(int chunkViewDistance) {
Check.stateCondition(started, "You cannot change the chunk view distance after the server has been started.");
Check.argCondition(!MathUtils.isBetween(chunkViewDistance, 2, 32), Check.argCondition(!MathUtils.isBetween(chunkViewDistance, 2, 32),
"The chunk view distance must be between 2 and 32"); "The chunk view distance must be between 2 and 32");
MinecraftServer.chunkViewDistance = chunkViewDistance; MinecraftServer.chunkViewDistance = chunkViewDistance;
if (started) {
for (final Player player : connectionManager.getOnlinePlayers()) {
final Chunk playerChunk = player.getChunk();
if (playerChunk != null) {
player.getPlayerConnection().sendPacket(new UpdateViewDistancePacket(player.getChunkRange()));
player.refreshVisibleChunks(playerChunk);
}
}
}
} }
/** /**
@ -476,19 +468,14 @@ public final class MinecraftServer {
* *
* @param entityViewDistance the new entity view distance * @param entityViewDistance the new entity view distance
* @throws IllegalArgumentException if {@code entityViewDistance} is not between 0 and 32 * @throws IllegalArgumentException if {@code entityViewDistance} is not between 0 and 32
* @deprecated should instead be defined with a java property
*/ */
@Deprecated
public static void setEntityViewDistance(int entityViewDistance) { public static void setEntityViewDistance(int entityViewDistance) {
Check.stateCondition(started, "You cannot change the entity view distance after the server has been started.");
Check.argCondition(!MathUtils.isBetween(entityViewDistance, 0, 32), Check.argCondition(!MathUtils.isBetween(entityViewDistance, 0, 32),
"The entity view distance must be between 0 and 32"); "The entity view distance must be between 0 and 32");
MinecraftServer.entityViewDistance = entityViewDistance; MinecraftServer.entityViewDistance = entityViewDistance;
if (started) {
for (final Player player : connectionManager.getOnlinePlayers()) {
final Chunk playerChunk = player.getChunk();
if (playerChunk != null) {
player.refreshVisibleEntities(playerChunk);
}
}
}
} }
/** /**
@ -673,9 +660,9 @@ public final class MinecraftServer {
updateManager.start(); updateManager.start();
// Init & start the TCP server // Init server
try { try {
server.start(new InetSocketAddress(address, port)); server.init(new InetSocketAddress(address, port));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -695,17 +682,24 @@ public final class MinecraftServer {
LOGGER.warn("Extension loadOnStartup option is set to false, extensions are therefore neither loaded or initialized."); LOGGER.warn("Extension loadOnStartup option is set to false, extensions are therefore neither loaded or initialized.");
} }
// Start server
server.start();
LOGGER.info("Minestom server started successfully."); LOGGER.info("Minestom server started successfully.");
if (terminalEnabled) { if (terminalEnabled) {
MinestomTerminal.start(); MinestomTerminal.start();
} }
// Stop the server on SIGINT
Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly));
} }
/** /**
* Stops this server properly (saves if needed, kicking players, etc.) * Stops this server properly (saves if needed, kicking players, etc.)
*/ */
public static void stopCleanly() { public static void stopCleanly() {
if (stopping) return;
stopping = true; stopping = true;
LOGGER.info("Stopping Minestom server."); LOGGER.info("Stopping Minestom server.");
extensionManager.unloadAllExtensions(); extensionManager.unloadAllExtensions();
@ -719,7 +713,7 @@ public final class MinecraftServer {
LOGGER.info("Shutting down all thread pools."); LOGGER.info("Shutting down all thread pools.");
benchmarkManager.disable(); benchmarkManager.disable();
MinestomTerminal.stop(); MinestomTerminal.stop();
MinestomThread.shutdownAll(); MinestomThreadPool.shutdownAll();
LOGGER.info("Minestom server stopped successfully."); LOGGER.info("Minestom server stopped successfully.");
} }

View File

@ -1,14 +1,15 @@
package net.minestom.server; package net.minestom.server;
import net.minestom.server.acquirable.Acquirable; import net.minestom.server.acquirable.Acquirable;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.InstanceManager;
import net.minestom.server.monitoring.TickMonitor; import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.thread.SingleThreadProvider; import net.minestom.server.thread.MinestomThread;
import net.minestom.server.thread.ThreadProvider; import net.minestom.server.thread.ThreadDispatcher;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.async.AsyncUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@ -22,14 +23,13 @@ import java.util.function.LongConsumer;
/** /**
* Manager responsible for the server ticks. * Manager responsible for the server ticks.
* <p> * <p>
* The {@link ThreadProvider} manages the multi-thread aspect of chunk ticks. * The {@link ThreadDispatcher} manages the multi-thread aspect of chunk ticks.
*/ */
public final class UpdateManager { public final class UpdateManager {
private volatile boolean stopRequested; private volatile boolean stopRequested;
// TODO make configurable // TODO make configurable
private ThreadProvider threadProvider = new SingleThreadProvider(); private final ThreadDispatcher threadDispatcher = ThreadDispatcher.singleThread();
private final Queue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>(); private final Queue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>();
private final Queue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>(); private final Queue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
@ -45,144 +45,60 @@ public final class UpdateManager {
* Starts the server loop in the update thread. * Starts the server loop in the update thread.
*/ */
void start() { void start() {
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager(); new TickSchedulerThread().start();
new Thread(() -> {
while (!stopRequested) {
try {
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);
// Monitoring
if (!tickMonitors.isEmpty()) {
final double acquisitionTimeMs = Acquirable.getAcquiringTime() / 1e6D;
final double tickTimeMs = tickTime / 1e6D;
final TickMonitor tickMonitor = new TickMonitor(tickTimeMs, acquisitionTimeMs);
this.tickMonitors.forEach(consumer -> consumer.accept(tickMonitor));
Acquirable.resetAcquiringTime();
}
// Flush all waiting packets
for (Player player : connectionManager.getOnlinePlayers()) {
player.getPlayerConnection().flush();
}
// Disable thread until next tick
LockSupport.parkNanos((long) ((MinecraftServer.TICK_MS * 1e6) - tickTime));
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
this.threadProvider.shutdown();
}, MinecraftServer.THREAD_NAME_TICK_SCHEDULER).start();
} }
/** /**
* Executes a server tick and returns only once all the futures are completed. * Gets the current {@link ThreadDispatcher}.
*
* @param tickStart the time of the tick in milliseconds
*/
private void serverTick(long tickStart) {
// Tick all instances
MinecraftServer.getInstanceManager().getInstances().forEach(instance -> {
try {
instance.tick(tickStart);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
});
// Tick all chunks (and entities inside)
this.threadProvider.updateAndAwait(tickStart);
// Clear removed entities & update threads
long tickTime = System.currentTimeMillis() - tickStart;
this.threadProvider.refreshThreads(tickTime);
}
/**
* Used to execute tick-related callbacks.
*
* @param callbacks the callbacks to execute
* @param value the value to give to the consumers
*/
private void doTickCallback(Queue<LongConsumer> callbacks, long value) {
if (!callbacks.isEmpty()) {
LongConsumer callback;
while ((callback = callbacks.poll()) != null) {
callback.accept(value);
}
}
}
/**
* Gets the current {@link ThreadProvider}.
* *
* @return the current thread provider * @return the current thread provider
*/ */
public @NotNull ThreadProvider getThreadProvider() { public @NotNull ThreadDispatcher getThreadProvider() {
return threadProvider; return threadDispatcher;
} }
/** /**
* Signals the {@link ThreadProvider} that an instance has been created. * Signals the {@link ThreadDispatcher} that an instance has been created.
* <p> * <p>
* WARNING: should be automatically done by the {@link InstanceManager}. * WARNING: should be automatically done by the {@link InstanceManager}.
* *
* @param instance the instance * @param instance the instance
*/ */
public void signalInstanceCreate(Instance instance) { public void signalInstanceCreate(Instance instance) {
this.threadProvider.onInstanceCreate(instance); this.threadDispatcher.onInstanceCreate(instance);
} }
/** /**
* Signals the {@link ThreadProvider} that an instance has been deleted. * Signals the {@link ThreadDispatcher} that an instance has been deleted.
* <p> * <p>
* WARNING: should be automatically done by the {@link InstanceManager}. * WARNING: should be automatically done by the {@link InstanceManager}.
* *
* @param instance the instance * @param instance the instance
*/ */
public void signalInstanceDelete(Instance instance) { public void signalInstanceDelete(Instance instance) {
this.threadProvider.onInstanceDelete(instance); this.threadDispatcher.onInstanceDelete(instance);
} }
/** /**
* Signals the {@link ThreadProvider} that a chunk has been loaded. * Signals the {@link ThreadDispatcher} that a chunk has been loaded.
* <p> * <p>
* WARNING: should be automatically done by the {@link Instance} implementation. * WARNING: should be automatically done by the {@link Instance} implementation.
* *
* @param chunk the loaded chunk * @param chunk the loaded chunk
*/ */
public void signalChunkLoad(@NotNull Chunk chunk) { public void signalChunkLoad(@NotNull Chunk chunk) {
this.threadProvider.onChunkLoad(chunk); this.threadDispatcher.onChunkLoad(chunk);
} }
/** /**
* Signals the {@link ThreadProvider} that a chunk has been unloaded. * Signals the {@link ThreadDispatcher} that a chunk has been unloaded.
* <p> * <p>
* WARNING: should be automatically done by the {@link Instance} implementation. * WARNING: should be automatically done by the {@link Instance} implementation.
* *
* @param chunk the unloaded chunk * @param chunk the unloaded chunk
*/ */
public void signalChunkUnload(@NotNull Chunk chunk) { public void signalChunkUnload(@NotNull Chunk chunk) {
this.threadProvider.onChunkUnload(chunk); this.threadDispatcher.onChunkUnload(chunk);
} }
/** /**
@ -239,4 +155,94 @@ public final class UpdateManager {
public void stop() { public void stop() {
this.stopRequested = true; this.stopRequested = true;
} }
private final class TickSchedulerThread extends MinestomThread {
private final ThreadDispatcher threadDispatcher = UpdateManager.this.threadDispatcher;
TickSchedulerThread() {
super(MinecraftServer.THREAD_NAME_TICK_SCHEDULER);
}
@Override
public void run() {
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
while (!stopRequested) {
try {
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);
// Flush all waiting packets
PacketUtils.flush();
AsyncUtils.runAsync(() -> MinecraftServer.getConnectionManager()
.getOnlinePlayers()
.parallelStream()
.forEach(player -> player.getPlayerConnection().flush()));
// the time that the tick took in nanoseconds
final long tickTime = System.nanoTime() - currentTime;
// Tick end callbacks
doTickCallback(tickEndCallbacks, tickTime);
// Monitoring
if (!tickMonitors.isEmpty()) {
final double acquisitionTimeMs = Acquirable.getAcquiringTime() / 1e6D;
final double tickTimeMs = tickTime / 1e6D;
final TickMonitor tickMonitor = new TickMonitor(tickTimeMs, acquisitionTimeMs);
for (Consumer<TickMonitor> consumer : tickMonitors) {
consumer.accept(tickMonitor);
}
Acquirable.resetAcquiringTime();
}
// Disable thread until next tick
LockSupport.parkNanos((long) ((MinecraftServer.TICK_MS * 1e6) - tickTime));
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
this.threadDispatcher.shutdown();
}
/**
* Executes a server tick and returns only once all the futures are completed.
*
* @param tickStart the time of the tick in milliseconds
*/
private void serverTick(long tickStart) {
// Tick all instances
for (Instance instance : MinecraftServer.getInstanceManager().getInstances()) {
try {
instance.tick(tickStart);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
// Tick all chunks (and entities inside)
this.threadDispatcher.updateAndAwait(tickStart);
// Clear removed entities & update threads
final long tickTime = System.currentTimeMillis() - tickStart;
this.threadDispatcher.refreshThreads(tickTime);
}
private void doTickCallback(Queue<LongConsumer> callbacks, long value) {
LongConsumer callback;
while ((callback = callbacks.poll()) != null) {
callback.accept(value);
}
}
}
} }

View File

@ -3,8 +3,10 @@ package net.minestom.server;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.minestom.server.adventure.audience.PacketGroupingAudience; import net.minestom.server.adventure.audience.PacketGroupingAudience;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.FramedPacket;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Set; import java.util.Set;
@ -35,8 +37,7 @@ public interface Viewable {
* *
* @return A Set containing all the element's viewers * @return A Set containing all the element's viewers
*/ */
@NotNull @NotNull Set<@NotNull Player> getViewers();
Set<Player> getViewers();
/** /**
* Gets if a player is seeing this viewable object. * Gets if a player is seeing this viewable object.
@ -60,6 +61,13 @@ public interface Viewable {
PacketUtils.sendGroupedPacket(getViewers(), packet); PacketUtils.sendGroupedPacket(getViewers(), packet);
} }
@ApiStatus.Experimental
default void sendPacketToViewers(@NotNull FramedPacket framedPacket) {
for (Player viewer : getViewers()) {
viewer.sendPacket(framedPacket);
}
}
/** /**
* Sends multiple packets to all viewers. * Sends multiple packets to all viewers.
* <p> * <p>
@ -85,6 +93,11 @@ public interface Viewable {
sendPacketToViewers(packet); sendPacketToViewers(packet);
} }
@ApiStatus.Experimental
default void sendPacketToViewersAndSelf(@NotNull FramedPacket framedPacket) {
sendPacketToViewers(framedPacket);
}
/** /**
* Gets the result of {@link #getViewers()} as an Adventure Audience. * Gets the result of {@link #getViewers()} as an Adventure Audience.
* *

View File

@ -1,14 +1,12 @@
package net.minestom.server.acquirable; package net.minestom.server.acquirable;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.thread.ThreadProvider; import net.minestom.server.thread.ThreadDispatcher;
import net.minestom.server.thread.TickThread; import net.minestom.server.thread.TickThread;
import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.async.AsyncUtils;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -26,19 +24,12 @@ public interface Acquirable<T> {
* @return the entities ticked in the current thread * @return the entities ticked in the current thread
*/ */
static @NotNull Stream<@NotNull Entity> currentEntities() { static @NotNull Stream<@NotNull Entity> currentEntities() {
return AcquirableImpl.ENTRIES.get().stream() final Thread currentThread = Thread.currentThread();
.flatMap(chunkEntry -> chunkEntry.getEntities().stream()); if (currentThread instanceof TickThread) {
} return ((TickThread) currentThread).entries().stream()
.flatMap(chunkEntry -> chunkEntry.entities().stream());
/** }
* Mostly for internal use, external calls are unrecommended as they could lead return Stream.empty();
* to unexpected behavior.
*
* @param entries the new chunk entries
*/
@ApiStatus.Internal
static void refreshEntries(@NotNull Collection<ThreadProvider.ChunkEntry> entries) {
AcquirableImpl.ENTRIES.set(entries);
} }
/** /**
@ -85,8 +76,7 @@ public interface Acquirable<T> {
* @see #sync(Consumer) for auto-closeable capability * @see #sync(Consumer) for auto-closeable capability
*/ */
default @NotNull Acquired<T> lock() { default @NotNull Acquired<T> lock() {
var optional = local(); return new Acquired<>(unwrap(), getHandler().getTickThread());
return optional.map(Acquired::local).orElseGet(() -> Acquired.locked(this));
} }
/** /**
@ -100,14 +90,19 @@ public interface Acquirable<T> {
* {@link Optional#empty()} otherwise * {@link Optional#empty()} otherwise
*/ */
default @NotNull Optional<T> local() { default @NotNull Optional<T> local() {
final Thread currentThread = Thread.currentThread(); if (isLocal()) return Optional.of(unwrap());
final TickThread tickThread = getHandler().getTickThread();
if (Objects.equals(currentThread, tickThread)) {
return Optional.of(unwrap());
}
return Optional.empty(); return Optional.empty();
} }
/**
* Gets if the acquirable element is local to this thread
*
* @return true if the element is linked to the current thread
*/
default boolean isLocal() {
return Thread.currentThread() == getHandler().getTickThread();
}
/** /**
* Locks the acquirable element, execute {@code consumer} synchronously and unlock the thread. * Locks the acquirable element, execute {@code consumer} synchronously and unlock the thread.
* <p> * <p>
@ -117,7 +112,7 @@ public interface Acquirable<T> {
* @see #async(Consumer) * @see #async(Consumer)
*/ */
default void sync(@NotNull Consumer<T> consumer) { default void sync(@NotNull Consumer<T> consumer) {
var acquired = lock(); Acquired<T> acquired = lock();
consumer.accept(acquired.get()); consumer.accept(acquired.get());
acquired.unlock(); acquired.unlock();
} }
@ -153,22 +148,21 @@ public interface Acquirable<T> {
@ApiStatus.Internal @ApiStatus.Internal
@NotNull Handler getHandler(); @NotNull Handler getHandler();
class Handler { final class Handler {
private volatile ThreadDispatcher.ChunkEntry chunkEntry;
private volatile ThreadProvider.ChunkEntry chunkEntry; public ThreadDispatcher.ChunkEntry getChunkEntry() {
public ThreadProvider.ChunkEntry getChunkEntry() {
return chunkEntry; return chunkEntry;
} }
@ApiStatus.Internal @ApiStatus.Internal
public void refreshChunkEntry(@NotNull ThreadProvider.ChunkEntry chunkEntry) { public void refreshChunkEntry(@NotNull ThreadDispatcher.ChunkEntry chunkEntry) {
this.chunkEntry = chunkEntry; this.chunkEntry = chunkEntry;
} }
public TickThread getTickThread() { public TickThread getTickThread() {
return chunkEntry != null ? chunkEntry.getThread() : null; final ThreadDispatcher.ChunkEntry entry = this.chunkEntry;
return entry != null ? entry.thread() : null;
} }
} }
} }

View File

@ -1,19 +1,14 @@
package net.minestom.server.acquirable; package net.minestom.server.acquirable;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.thread.TickThread; import net.minestom.server.thread.TickThread;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
class AcquirableImpl<T> implements Acquirable<T> { final class AcquirableImpl<T> implements Acquirable<T> {
static final AtomicLong WAIT_COUNTER_NANO = new AtomicLong();
protected static final ThreadLocal<Collection<ThreadProvider.ChunkEntry>> ENTRIES = ThreadLocal.withInitial(Collections::emptySet);
protected static final AtomicLong WAIT_COUNTER_NANO = new AtomicLong();
/** /**
* Global lock used for synchronization. * Global lock used for synchronization.
@ -38,41 +33,37 @@ class AcquirableImpl<T> implements Acquirable<T> {
return handler; return handler;
} }
protected static @Nullable ReentrantLock enter(@Nullable Thread currentThread, @Nullable TickThread elementThread) { static @Nullable ReentrantLock enter(@NotNull Thread currentThread, @Nullable TickThread elementThread) {
// Monitoring if (elementThread == null) return null;
long time = System.nanoTime(); if (currentThread == elementThread) return null;
final ReentrantLock currentLock = currentThread instanceof TickThread ? ((TickThread) currentThread).lock() : null;
ReentrantLock currentLock; final ReentrantLock targetLock = elementThread.lock();
{ if (targetLock.isHeldByCurrentThread()) return null;
final TickThread current = currentThread instanceof TickThread ?
(TickThread) currentThread : null;
currentLock = current != null && current.getLock().isHeldByCurrentThread() ?
current.getLock() : null;
}
if (currentLock != null)
currentLock.unlock();
GLOBAL_LOCK.lock();
if (currentLock != null)
currentLock.lock();
final var lock = elementThread != null ? elementThread.getLock() : null;
final boolean acquired = lock == null || lock.isHeldByCurrentThread();
if (!acquired) {
lock.lock();
}
// Monitoring // Monitoring
AcquirableImpl.WAIT_COUNTER_NANO.addAndGet(System.nanoTime() - time); final long time = System.nanoTime();
return !acquired ? lock : null; // Enter the target thread
// TODO reduce global lock scope
if (currentLock != null) {
while (!GLOBAL_LOCK.tryLock()) {
currentLock.unlock();
currentLock.lock();
}
} else {
GLOBAL_LOCK.lock();
}
targetLock.lock();
// Monitoring
WAIT_COUNTER_NANO.addAndGet(System.nanoTime() - time);
return targetLock;
} }
protected static void leave(@Nullable ReentrantLock lock) { static void leave(@Nullable ReentrantLock lock) {
if (lock != null) { if (lock != null) {
lock.unlock(); lock.unlock();
GLOBAL_LOCK.unlock();
} }
GLOBAL_LOCK.unlock();
} }
} }

View File

@ -6,47 +6,39 @@ import org.jetbrains.annotations.NotNull;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
public class Acquired<T> { /**
* Represents an object that has been safely acquired and can be freed again.
* <p>
* This class should not be shared, and it is recommended to call {@link #unlock()}
* once the acquisition goal has been fulfilled to limit blocking time.
*
* @param <T> the type of the acquired object
*/
public final class Acquired<T> {
private final T value; private final T value;
private final Thread owner;
private final boolean locked;
private final ReentrantLock lock; private final ReentrantLock lock;
private boolean unlocked; private boolean unlocked;
protected static <T> Acquired<T> local(@NotNull T value) { Acquired(T value, TickThread tickThread) {
return new Acquired<>(value, false, null, null);
}
protected static <T> Acquired<T> locked(@NotNull Acquirable<T> acquirable) {
final Thread currentThread = Thread.currentThread();
final TickThread tickThread = acquirable.getHandler().getTickThread();
return new Acquired<>(acquirable.unwrap(), true, currentThread, tickThread);
}
private Acquired(@NotNull T value,
boolean locked, Thread currentThread, TickThread tickThread) {
this.value = value; this.value = value;
this.locked = locked; this.owner = Thread.currentThread();
this.lock = locked ? AcquirableImpl.enter(currentThread, tickThread) : null; this.lock = AcquirableImpl.enter(owner, tickThread);
} }
public @NotNull T get() { public @NotNull T get() {
checkLock(); safeCheck();
return value; return value;
} }
public void unlock() { public void unlock() {
checkLock(); safeCheck();
this.unlocked = true; this.unlocked = true;
if (!locked)
return;
AcquirableImpl.leave(lock); AcquirableImpl.leave(lock);
} }
private void checkLock() { private void safeCheck() {
Check.stateCondition(Thread.currentThread() != owner, "Acquired object is owned by the thread {0}", owner);
Check.stateCondition(unlocked, "The acquired element has already been unlocked!"); Check.stateCondition(unlocked, "The acquired element has already been unlocked!");
} }
} }

View File

@ -6,14 +6,15 @@ import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.TitlePart;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.play.EntitySoundEffectPacket; import net.minestom.server.network.packet.server.play.*;
import net.minestom.server.network.packet.server.play.NamedSoundEffectPacket;
import net.minestom.server.network.packet.server.play.SoundEffectPacket;
import net.minestom.server.network.packet.server.play.StopSoundPacket;
import net.minestom.server.sound.SoundEvent; import net.minestom.server.sound.SoundEvent;
import net.minestom.server.utils.TickUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collection; import java.util.Collection;
@ -141,10 +142,9 @@ public class AdventurePacketConvertor {
public static @NotNull ServerPacket createSoundPacket(@NotNull Sound sound, Sound.@NotNull Emitter emitter) { public static @NotNull ServerPacket createSoundPacket(@NotNull Sound sound, Sound.@NotNull Emitter emitter) {
if (emitter == Sound.Emitter.self()) if (emitter == Sound.Emitter.self())
throw new IllegalArgumentException("you must replace instances of Emitter.self() before calling this method"); throw new IllegalArgumentException("you must replace instances of Emitter.self() before calling this method");
if (!(emitter instanceof Entity)) if (!(emitter instanceof Entity entity))
throw new IllegalArgumentException("you can only call this method with entities"); throw new IllegalArgumentException("you can only call this method with entities");
final Entity entity = (Entity) emitter;
final SoundEvent minestomSound = SoundEvent.fromNamespaceId(sound.name().asString()); final SoundEvent minestomSound = SoundEvent.fromNamespaceId(sound.name().asString());
if (minestomSound != null) { if (minestomSound != null) {
@ -206,4 +206,28 @@ public class AdventurePacketConvertor {
return packet; return packet;
} }
/**
* Creates one of the three title packets from a title part and a value.
*
* @param part the part
* @param value the value
* @param <T> the type of the part
* @return the title packet
*/
public static <T> @NotNull ServerPacket createTitlePartPacket(@NotNull TitlePart<T> part, @NotNull T value) {
if (part == TitlePart.TITLE) {
return new SetTitleTextPacket((Component) value);
} else if (part == TitlePart.SUBTITLE) {
return new SetTitleSubTitlePacket((Component) value);
} else if (part == TitlePart.TIMES) {
Title.Times times = (Title.Times) value;
return new SetTitleTimePacket(
TickUtils.fromDuration(times.fadeIn(), TickUtils.CLIENT_TICK_MS),
TickUtils.fromDuration(times.stay(), TickUtils.CLIENT_TICK_MS),
TickUtils.fromDuration(times.fadeOut(), TickUtils.CLIENT_TICK_MS));
} else {
throw new IllegalArgumentException("Unknown TitlePart " + part);
}
}
} }

View File

@ -1,203 +0,0 @@
package net.minestom.server.adventure;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationRegistry;
import net.kyori.adventure.translation.Translator;
import net.minestom.server.utils.ComponentUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
/**
* Manager class for handling Adventure serialization. By default AdventureSerializer will simply
* serialize components to Strings using {@link GsonComponentSerializer}. However, AdventureSerializer
* class can be used to change the way text is serialized. For example, a pre-JSON
* implementation of Minestom could change AdventureSerializer to the plain component serializer.
* <br><br>
* This manager also performs translation on all messages and the {@code serialize}
* method should be used when converting {@link Component}s into strings. This allows for
* messages with {@link TranslatableComponent} to be automatically translated into the locale
* of specific players, or other elements which implement {@link Localizable}. To add your
* own translations, use {@link GlobalTranslator#addSource(Translator)} with a
* {@link TranslationRegistry} or your own implementation of {@link Translator}.
*
* @deprecated Use {@link MinestomAdventure}
*/
@Deprecated(forRemoval = true)
public class AdventureSerializer {
/**
* If components should be automatically translated in outgoing packets.
* @deprecated Use {@link MinestomAdventure#AUTOMATIC_COMPONENT_TRANSLATION}
*/
@Deprecated(forRemoval = true)
public static final boolean AUTOMATIC_COMPONENT_TRANSLATION = MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION;
private static Function<Component, String> serializer = component -> GsonComponentSerializer.gson().serialize(component);
private AdventureSerializer() {}
/**
* Gets the root serializer that is used to convert components into strings.
*
* @return the serializer
* @deprecated The serializer is no longer in use, use the adventure-provided serializers
*/
@Deprecated(forRemoval = true)
public static @NotNull Function<Component, String> getSerializer() {
return AdventureSerializer.serializer;
}
/**
* Sets the root serializer that is used to convert components into strings.
*
* @param serializer the serializer
* @deprecated The serializer is no longer in use
*/
@Deprecated
public static void setSerializer(@NotNull Function<Component, String> serializer) {
AdventureSerializer.serializer = serializer;
}
/**
* Gets the default locale used to translate {@link TranslatableComponent} if, when
* {@link #translate(Component, Localizable)} is called with a localizable that
* does not have a locale.
*
* @return the default locale
* @deprecated Use {@link MinestomAdventure#getDefaultLocale()}
*/
@Deprecated(forRemoval = true)
public static @NotNull Locale getDefaultLocale() {
return MinestomAdventure.getDefaultLocale();
}
/**
* Sets the default locale used to translate {@link TranslatableComponent} if, when
* {@link #translate(Component, Localizable)} is called with a localizable that
* does not have a locale.
*
* @param defaultLocale the new default locale, or {@code null} to return to the default
* @deprecated Use {@link MinestomAdventure#setDefaultLocale(Locale)}}
*/
@Deprecated(forRemoval = true)
public static void setDefaultLocale(@Nullable Locale defaultLocale) {
MinestomAdventure.setDefaultLocale(defaultLocale);
}
/**
* Gets the global translator object used by AdventureSerializer manager. This is just shorthand for
* {@link GlobalTranslator#get()}.
*
* @return the global translator
* @deprecated Use {@link GlobalTranslator#get()}
*/
@Deprecated(forRemoval = true)
public static @NotNull GlobalTranslator getTranslator() {
return GlobalTranslator.get();
}
/**
* Prepares a component for serialization. This runs the component through the
* translator for the localizable's locale.
*
* @param component the component
* @param localizable the localizable
*
* @return the prepared component
* @deprecated Use {@link GlobalTranslator#translate(String, Locale)}
*/
@Deprecated(forRemoval = true)
public static @NotNull Component translate(@NotNull Component component, @NotNull Localizable localizable) {
return GlobalTranslator.renderer().render(component, Objects.requireNonNullElse(localizable.getLocale(), AdventureSerializer.getDefaultLocale()));
}
/**
* Prepares a component for serialization. This runs the component through the
* translator for the locale.
*
* @param component the component
* @param locale the locale
*
* @return the prepared component
* @deprecated Use {@link GlobalTranslator#translate(String, Locale)}
*/
@Deprecated(forRemoval = true)
public static @NotNull Component translate(@NotNull Component component, @NotNull Locale locale) {
return GlobalTranslator.renderer().render(component, locale);
}
/**
* Serializes a component into a string using {@link #getSerializer()}.
*
* @param component the component
*
* @return the serialized string
* @deprecated Use the Adventure serializers directly
*/
@Deprecated(forRemoval = true)
public static @NotNull String serialize(@NotNull Component component) {
return AdventureSerializer.serializer.apply(component);
}
/**
* Prepares and then serializes a component.
*
* @param component the component
* @param localizable the localisable
*
* @return the string
* @deprecated Use {@link GlobalTranslator#translate(String, Locale)} and the Adventure serializers
*/
@Deprecated(forRemoval = true)
public static String translateAndSerialize(@NotNull Component component, @NotNull Localizable localizable) {
return AdventureSerializer.translateAndSerialize(component, Objects.requireNonNullElse(localizable.getLocale(), AdventureSerializer.getDefaultLocale()));
}
/**
* Prepares and then serializes a component.
*
* @param component the component
* @param locale the locale
*
* @return the string
* @deprecated Use {@link GlobalTranslator#translate(String, Locale)} and the Adventure serializers
*/
@Deprecated(forRemoval = true)
public static String translateAndSerialize(@NotNull Component component, @NotNull Locale locale) {
return AdventureSerializer.serialize(AdventureSerializer.translate(component, locale));
}
/**
* Checks if a component can be translated server-side. This is done by running the
* component through the translator and seeing if the translated component is equal
* to the non translated component.
* @param component the component
* @return {@code true} if the component can be translated server-side,
* {@code false} otherwise
* @deprecated Use {@link ComponentUtils#isTranslatable(Component)}
*/
@Deprecated(forRemoval = true)
public static boolean isTranslatable(@NotNull Component component) {
return ComponentUtils.isTranslatable(component);
}
/**
* Checks if any of a series of components are translatable server-side.
* @param components the components
* @return {@code true} if any of the components can be translated server-side,
* {@code false} otherwise
* @deprecated Use {@link ComponentUtils#areAnyTranslatable(Collection)}
*/
@Deprecated(forRemoval = true)
public static boolean areAnyTranslatable(@NotNull Collection<Component> components) {
return ComponentUtils.areAnyTranslatable(components);
}
}

View File

@ -12,7 +12,6 @@ import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
/** /**
* Holder of custom audiences. * Holder of custom audiences.
@ -97,7 +96,7 @@ public class AudienceRegistry {
if (this.isEmpty()) { if (this.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} else { } else {
return this.registry.values().stream().flatMap(Collection::stream).collect(Collectors.toUnmodifiableList()); return this.registry.values().stream().flatMap(Collection::stream).toList();
} }
} }
@ -128,6 +127,6 @@ public class AudienceRegistry {
* @return the matching audience members * @return the matching audience members
*/ */
public @NotNull Iterable<? extends Audience> of(@NotNull Predicate<Audience> filter) { public @NotNull Iterable<? extends Audience> of(@NotNull Predicate<Audience> filter) {
return this.registry.values().stream().flatMap(Collection::stream).filter(filter).collect(Collectors.toUnmodifiableList()); return this.registry.values().stream().flatMap(Collection::stream).filter(filter).toList();
} }
} }

View File

@ -8,14 +8,16 @@ import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title; import net.kyori.adventure.title.TitlePart;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.AdventurePacketConvertor; import net.minestom.server.adventure.AdventurePacketConvertor;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.message.ChatPosition; import net.minestom.server.message.ChatPosition;
import net.minestom.server.message.Messenger; import net.minestom.server.message.Messenger;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.packet.server.play.ActionBarPacket;
import net.minestom.server.network.packet.server.play.ClearTitlesPacket;
import net.minestom.server.network.packet.server.play.PlayerListHeaderAndFooterPacket;
import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.PacketUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -47,6 +49,7 @@ public interface PacketGroupingAudience extends ForwardingAudience {
/** /**
* Broadcast a ServerPacket to all players of this audience * Broadcast a ServerPacket to all players of this audience
*
* @param packet the packet to broadcast * @param packet the packet to broadcast
*/ */
default void sendGroupedPacket(ServerPacket packet) { default void sendGroupedPacket(ServerPacket packet) {
@ -69,9 +72,8 @@ public interface PacketGroupingAudience extends ForwardingAudience {
} }
@Override @Override
default void showTitle(@NotNull Title title) { default <T> void sendTitlePart(@NotNull TitlePart<T> part, @NotNull T value) {
sendGroupedPacket(new SetTitleTextPacket(title.title())); sendGroupedPacket(AdventurePacketConvertor.createTitlePartPacket(part, value));
sendGroupedPacket(new SetTitleSubTitlePacket(title.subtitle()));
} }
@Override @Override
@ -116,8 +118,6 @@ public interface PacketGroupingAudience extends ForwardingAudience {
sendGroupedPacket(AdventurePacketConvertor.createSoundStopPacket(stop)); sendGroupedPacket(AdventurePacketConvertor.createSoundStopPacket(stop));
} }
@Override @Override
default @NotNull Iterable<? extends Audience> audiences() { default @NotNull Iterable<? extends Audience> audiences() {
return this.getPlayers(); return this.getPlayers();

View File

@ -147,7 +147,7 @@ public class BossBarManager {
if (holders == null) { if (holders == null) {
return Collections.emptyList(); return Collections.emptyList();
} else { } else {
return holders.stream().map(holder -> holder.bar).collect(Collectors.toUnmodifiableList()); return holders.stream().map(holder -> holder.bar).toList();
} }
} }

View File

@ -1,7 +1,5 @@
package net.minestom.server.adventure.provider; package net.minestom.server.adventure.provider;
import java.util.UUID;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -17,6 +15,7 @@ import org.jglrxavpok.hephaistos.nbt.NBTException;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer { final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer {
static final NBTLegacyHoverEventSerializer INSTANCE = new NBTLegacyHoverEventSerializer(); static final NBTLegacyHoverEventSerializer INSTANCE = new NBTLegacyHoverEventSerializer();
@ -33,8 +32,8 @@ final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer
try { try {
// attempt the parse // attempt the parse
final NBT nbt = MinestomAdventure.NBT_CODEC.decode(raw); final NBT nbt = MinestomAdventure.NBT_CODEC.decode(raw);
if (!(nbt instanceof NBTCompound)) throw new IOException("contents were not a compound"); if (!(nbt instanceof NBTCompound contents)) throw new IOException("contents were not a compound");
final NBTCompound contents = (NBTCompound) nbt, tag = contents.getCompound(ITEM_TAG); final NBTCompound tag = contents.getCompound(ITEM_TAG);
// create the event // create the event
return HoverEvent.ShowItem.of( return HoverEvent.ShowItem.of(
@ -53,9 +52,7 @@ final class NBTLegacyHoverEventSerializer implements LegacyHoverEventSerializer
try { try {
final NBT nbt = MinestomAdventure.NBT_CODEC.decode(raw); final NBT nbt = MinestomAdventure.NBT_CODEC.decode(raw);
if (!(nbt instanceof NBTCompound)) throw new IOException("contents were not a compound"); if (!(nbt instanceof NBTCompound contents)) throw new IOException("contents were not a compound");
final NBTCompound contents = (NBTCompound) nbt;
return HoverEvent.ShowEntity.of( return HoverEvent.ShowEntity.of(
Key.key(Objects.requireNonNullElse(contents.getString(ENTITY_TYPE), "")), Key.key(Objects.requireNonNullElse(contents.getString(ENTITY_TYPE), "")),

View File

@ -1,10 +1,8 @@
package net.minestom.server.attribute; package net.minestom.server.attribute;
import net.minestom.server.utils.UniqueIdUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/** /**
* Represent an attribute modifier. * Represent an attribute modifier.
@ -24,7 +22,7 @@ public class AttributeModifier {
* @param operation the operation to apply this modifier with * @param operation the operation to apply this modifier with
*/ */
public AttributeModifier(@NotNull String name, float amount, @NotNull AttributeOperation operation) { public AttributeModifier(@NotNull String name, float amount, @NotNull AttributeOperation operation) {
this(UniqueIdUtils.createRandomUUID(ThreadLocalRandom.current()), name, amount, operation); this(UUID.randomUUID(), name, amount, operation);
} }
/** /**

View File

@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
@ -18,13 +19,42 @@ public class BoundingBox {
private final Entity entity; private final Entity entity;
private final double x, y, z; private final double x, y, z;
private volatile Pos lastPosition; private final CachedFace bottomFace = new CachedFace(() -> List.of(
private List<Vec> bottomFace; new Vec(getMinX(), getMinY(), getMinZ()),
private List<Vec> topFace; new Vec(getMaxX(), getMinY(), getMinZ()),
private List<Vec> leftFace; new Vec(getMaxX(), getMinY(), getMaxZ()),
private List<Vec> rightFace; new Vec(getMinX(), getMinY(), getMaxZ())
private List<Vec> frontFace; ));
private List<Vec> backFace; private final CachedFace topFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())
));
private final CachedFace leftFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())
));
private final CachedFace rightFace = new CachedFace(() -> List.of(
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ())
));
private final CachedFace frontFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ())
));
private final CachedFace backFace = new CachedFace(() -> List.of(
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())
));
/** /**
* Creates a {@link BoundingBox} linked to an {@link Entity} and with a specific size. * Creates a {@link BoundingBox} linked to an {@link Entity} and with a specific size.
@ -74,20 +104,15 @@ public class BoundingBox {
public boolean intersectWithBlock(int blockX, int blockY, int blockZ) { public boolean intersectWithBlock(int blockX, int blockY, int blockZ) {
final double offsetX = 1; final double offsetX = 1;
final double maxX = (double) blockX + offsetX; final double maxX = (double) blockX + offsetX;
final boolean checkX = getMinX() < maxX && getMaxX() > (double) blockX; final boolean checkX = getMinX() < maxX && getMaxX() > (double) blockX;
if (!checkX) if (!checkX) return false;
return false;
final double maxY = (double) blockY + 0.99999; final double maxY = (double) blockY + 0.99999;
final boolean checkY = getMinY() < maxY && getMaxY() > (double) blockY; final boolean checkY = getMinY() < maxY && getMaxY() > (double) blockY;
if (!checkY) if (!checkY) return false;
return false;
final double offsetZ = 1; final double offsetZ = 1;
final double maxZ = (double) blockZ + offsetZ; final double maxZ = (double) blockZ + offsetZ;
// Z check // Z check
return getMinZ() < maxZ && getMaxZ() > (double) blockZ; return getMinZ() < maxZ && getMaxZ() > (double) blockZ;
} }
@ -102,16 +127,106 @@ public class BoundingBox {
return intersectWithBlock(blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ()); return intersectWithBlock(blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ());
} }
/**
* Used to know if the bounding box intersects (contains) a point.
*
* @param x x-coord of a point
* @param y y-coord of a point
* @param z z-coord of a point
* @return true if the bounding box intersects (contains) with the point, false otherwise
*/
public boolean intersect(double x, double y, double z) { public boolean intersect(double x, double y, double z) {
return (x >= getMinX() && x <= getMaxX()) && return (x >= getMinX() && x <= getMaxX()) &&
(y >= getMinY() && y <= getMaxY()) && (y >= getMinY() && y <= getMaxY()) &&
(z >= getMinZ() && z <= getMaxZ()); (z >= getMinZ() && z <= getMaxZ());
} }
/**
* Used to know if the bounding box intersects (contains) a point.
*
* @param point the point to check
* @return true if the bounding box intersects (contains) with the point, false otherwise
*/
public boolean intersect(@NotNull Point point) { public boolean intersect(@NotNull Point point) {
return intersect(point.x(), point.y(), point.z()); return intersect(point.x(), point.y(), point.z());
} }
/**
* Used to know if the bounding box intersects a line segment.
*
* @param x1 x-coord of first line segment point
* @param y1 y-coord of first line segment point
* @param z1 z-coord of first line segment point
* @param x2 x-coord of second line segment point
* @param y2 y-coord of second line segment point
* @param z2 z-coord of second line segment point
* @return true if the bounding box intersects with the line segment, false otherwise.
*/
public boolean intersect(double x1, double y1, double z1, double x2, double y2, double z2) {
// originally from http://www.3dkingdoms.com/weekly/weekly.php?a=3
double x3 = getMinX();
double x4 = getMaxX();
double y3 = getMinY();
double y4 = getMaxY();
double z3 = getMinZ();
double z4 = getMaxZ();
if (x1 > x3 && x1 < x4 && y1 > y3 && y1 < y4 && z1 > z3 && z1 < z4) {
return true;
}
if (x1 < x3 && x2 < x3 || x1 > x4 && x2 > x4 ||
y1 < y3 && y2 < y3 || y1 > y4 && y2 > y4 ||
z1 < z3 && z2 < z3 || z1 > z4 && z2 > z4) {
return false;
}
return isInsideBoxWithAxis(Axis.X, getSegmentIntersection(x1 - x3, x2 - x3, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.X, getSegmentIntersection(x1 - x4, x2 - x4, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Y, getSegmentIntersection(y1 - y3, y2 - y3, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Y, getSegmentIntersection(y1 - y4, y2 - y4, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Z, getSegmentIntersection(z1 - z3, z2 - z3, x1, y1, z1, x2, y2, z2)) ||
isInsideBoxWithAxis(Axis.Z, getSegmentIntersection(z1 - z4, z2 - z4, x1, y1, z1, x2, y2, z2));
}
/**
* Used to know if the bounding box intersects a line segment.
*
* @param start first line segment point
* @param end second line segment point
* @return true if the bounding box intersects with the line segment, false otherwise.
*/
public boolean intersect(@NotNull Point start, @NotNull Point end) {
return intersect(
Math.min(start.x(), end.x()),
Math.min(start.y(), end.y()),
Math.min(start.z(), end.z()),
Math.max(start.x(), end.x()),
Math.max(start.y(), end.y()),
Math.max(start.z(), end.z())
);
}
private @Nullable Vec getSegmentIntersection(double dst1, double dst2, double x1, double y1, double z1, double x2, double y2, double z2) {
if (dst1 == dst2 || dst1 * dst2 >= 0D) return null;
final double delta = dst1 / (dst1 - dst2);
return new Vec(
x1 + (x2 - x1) * delta,
y1 + (y2 - y1) * delta,
z1 + (z2 - z1) * delta
);
}
private boolean isInsideBoxWithAxis(Axis axis, @Nullable Vec intersection) {
if (intersection == null) return false;
double x1 = getMinX();
double x2 = getMaxX();
double y1 = getMinY();
double y2 = getMaxY();
double z1 = getMinZ();
double z2 = getMaxZ();
return axis == Axis.X && intersection.z() > z1 && intersection.z() < z2 && intersection.y() > y1 && intersection.y() < y2 ||
axis == Axis.Y && intersection.z() > z1 && intersection.z() < z2 && intersection.x() > x1 && intersection.x() < x2 ||
axis == Axis.Z && intersection.x() > x1 && intersection.x() < x2 && intersection.y() > y1 && intersection.y() < y2;
}
/** /**
* Creates a new {@link BoundingBox} linked to the same {@link Entity} with expanded size. * Creates a new {@link BoundingBox} linked to the same {@link Entity} with expanded size.
* *
@ -120,8 +235,7 @@ public class BoundingBox {
* @param z the Z offset * @param z the Z offset
* @return a new {@link BoundingBox} expanded * @return a new {@link BoundingBox} expanded
*/ */
@NotNull public @NotNull BoundingBox expand(double x, double y, double z) {
public BoundingBox expand(double x, double y, double z) {
return new BoundingBox(entity, this.x + x, this.y + y, this.z + z); return new BoundingBox(entity, this.x + x, this.y + y, this.z + z);
} }
@ -133,8 +247,7 @@ public class BoundingBox {
* @param z the Z offset * @param z the Z offset
* @return a new bounding box contracted * @return a new bounding box contracted
*/ */
@NotNull public @NotNull BoundingBox contract(double x, double y, double z) {
public BoundingBox contract(double x, double y, double z) {
return new BoundingBox(entity, this.x - x, this.y - y, this.z - z); return new BoundingBox(entity, this.x - x, this.y - y, this.z - z);
} }
@ -225,12 +338,7 @@ public class BoundingBox {
* @return the points at the bottom of the {@link BoundingBox} * @return the points at the bottom of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getBottomFace() { public @NotNull List<Vec> getBottomFace() {
this.bottomFace = get(bottomFace, () -> return bottomFace.get();
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())));
return bottomFace;
} }
/** /**
@ -239,12 +347,7 @@ public class BoundingBox {
* @return the points at the top of the {@link BoundingBox} * @return the points at the top of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getTopFace() { public @NotNull List<Vec> getTopFace() {
this.topFace = get(topFace, () -> return topFace.get();
List.of(new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())));
return topFace;
} }
/** /**
@ -253,12 +356,7 @@ public class BoundingBox {
* @return the points on the left face of the {@link BoundingBox} * @return the points on the left face of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getLeftFace() { public @NotNull List<Vec> getLeftFace() {
this.leftFace = get(leftFace, () -> return leftFace.get();
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMinY(), getMaxZ())));
return leftFace;
} }
/** /**
@ -267,12 +365,7 @@ public class BoundingBox {
* @return the points on the right face of the {@link BoundingBox} * @return the points on the right face of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getRightFace() { public @NotNull List<Vec> getRightFace() {
this.rightFace = get(rightFace, () -> return rightFace.get();
List.of(new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ())));
return rightFace;
} }
/** /**
@ -281,12 +374,7 @@ public class BoundingBox {
* @return the points at the front of the {@link BoundingBox} * @return the points at the front of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getFrontFace() { public @NotNull List<Vec> getFrontFace() {
this.frontFace = get(frontFace, () -> return frontFace.get();
List.of(new Vec(getMinX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMinY(), getMinZ()),
new Vec(getMaxX(), getMaxY(), getMinZ()),
new Vec(getMinX(), getMaxY(), getMinZ())));
return frontFace;
} }
/** /**
@ -295,12 +383,7 @@ public class BoundingBox {
* @return the points at the back of the {@link BoundingBox} * @return the points at the back of the {@link BoundingBox}
*/ */
public @NotNull List<Vec> getBackFace() { public @NotNull List<Vec> getBackFace() {
this.backFace = get(backFace, () -> List.of( return backFace.get();
new Vec(getMinX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMinY(), getMaxZ()),
new Vec(getMaxX(), getMaxY(), getMaxZ()),
new Vec(getMinX(), getMaxY(), getMaxZ())));
return backFace;
} }
@Override @Override
@ -315,13 +398,37 @@ public class BoundingBox {
return result; return result;
} }
private @NotNull List<Vec> get(@Nullable List<Vec> face, Supplier<? extends List<Vec>> vecSupplier) { private enum Axis {
final var lastPos = this.lastPosition; X, Y, Z
final var entityPos = entity.getPosition(); }
if (face != null && lastPos != null && lastPos.samePoint(entityPos)) {
return face; private final class CachedFace {
private final AtomicReference<@Nullable PositionedPoints> reference = new AtomicReference<>(null);
private final Supplier<@NotNull List<Vec>> faceProducer;
private CachedFace(Supplier<@NotNull List<Vec>> faceProducer) {
this.faceProducer = faceProducer;
}
@NotNull List<Vec> get() {
//noinspection ConstantConditions
return reference.updateAndGet(value -> {
Pos entityPosition = entity.getPosition();
if (value == null || !value.lastPosition.samePoint(entityPosition)) {
return new PositionedPoints(entityPosition, faceProducer.get());
}
return value;
}).points;
}
}
private static final class PositionedPoints {
private final @NotNull Pos lastPosition;
private final @NotNull List<Vec> points;
private PositionedPoints(@NotNull Pos lastPosition, @NotNull List<Vec> points) {
this.lastPosition = lastPosition;
this.points = points;
} }
this.lastPosition = entityPos;
return vecSupplier.get();
} }
} }

View File

@ -7,10 +7,10 @@ import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.WorldBorder;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockGetter;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class CollisionUtils { public class CollisionUtils {
@ -75,7 +75,7 @@ public class CollisionUtils {
* @return result of the step * @return result of the step
*/ */
private static StepResult stepAxis(Instance instance, Chunk originChunk, Vec startPosition, Vec axis, double stepAmount, List<Vec> corners) { private static StepResult stepAxis(Instance instance, Chunk originChunk, Vec startPosition, Vec axis, double stepAmount, List<Vec> corners) {
final List<Vec> mutableCorners = new ArrayList<>(corners); final Vec[] mutableCorners = corners.toArray(Vec[]::new);
final double sign = Math.signum(stepAmount); final double sign = Math.signum(stepAmount);
final int blockLength = (int) stepAmount; final int blockLength = (int) stepAmount;
final double remainingLength = stepAmount - blockLength; final double remainingLength = stepAmount - blockLength;
@ -94,7 +94,7 @@ public class CollisionUtils {
// find the corner which moved the least // find the corner which moved the least
double smallestDisplacement = Double.POSITIVE_INFINITY; double smallestDisplacement = Double.POSITIVE_INFINITY;
for (int i = 0; i < corners.size(); i++) { for (int i = 0; i < corners.size(); i++) {
final double displacement = corners.get(i).distance(mutableCorners.get(i)); final double displacement = corners.get(i).distance(mutableCorners[i]);
if (displacement < smallestDisplacement) { if (displacement < smallestDisplacement) {
smallestDisplacement = displacement; smallestDisplacement = displacement;
} }
@ -111,27 +111,27 @@ public class CollisionUtils {
* @param corners the corners of the bounding box to consider * @param corners the corners of the bounding box to consider
* @return true if found collision * @return true if found collision
*/ */
private static boolean stepOnce(Instance instance, Chunk originChunk, Vec axis, double amount, List<Vec> corners) { private static boolean stepOnce(Instance instance, Chunk originChunk, Vec axis, double amount, Vec[] corners) {
final double sign = Math.signum(amount); final double sign = Math.signum(amount);
for (int cornerIndex = 0; cornerIndex < corners.size(); cornerIndex++) { for (int cornerIndex = 0; cornerIndex < corners.length; cornerIndex++) {
final Vec originalCorner = corners.get(cornerIndex); final Vec originalCorner = corners[cornerIndex];
final Vec newCorner = originalCorner.add(axis.mul(amount)); final Vec newCorner = originalCorner.add(axis.mul(amount));
final Chunk chunk = ChunkUtils.retrieve(instance, originChunk, newCorner); final Chunk chunk = ChunkUtils.retrieve(instance, originChunk, newCorner);
if (!ChunkUtils.isLoaded(chunk)) { if (!ChunkUtils.isLoaded(chunk)) {
// Collision at chunk border // Collision at chunk border
return true; return true;
} }
final Block block = chunk.getBlock(newCorner); final Block block = chunk.getBlock(newCorner, BlockGetter.Condition.TYPE);
// TODO: block collision boxes // TODO: block collision boxes
// TODO: for the moment, always consider a full block // TODO: for the moment, always consider a full block
if (block.isSolid()) { if (block != null && block.isSolid()) {
corners.set(cornerIndex, new Vec( corners[cornerIndex] = new Vec(
Math.abs(axis.x()) > 10e-16 ? newCorner.blockX() - axis.x() * sign : originalCorner.x(), Math.abs(axis.x()) > 10e-16 ? newCorner.blockX() - axis.x() * sign : originalCorner.x(),
Math.abs(axis.y()) > 10e-16 ? newCorner.blockY() - axis.y() * sign : originalCorner.y(), Math.abs(axis.y()) > 10e-16 ? newCorner.blockY() - axis.y() * sign : originalCorner.y(),
Math.abs(axis.z()) > 10e-16 ? newCorner.blockZ() - axis.z() * sign : originalCorner.z())); Math.abs(axis.z()) > 10e-16 ? newCorner.blockZ() - axis.z() * sign : originalCorner.z());
return true; return true;
} }
corners.set(cornerIndex, newCorner); corners[cornerIndex] = newCorner;
} }
return false; return false;
} }
@ -148,54 +148,28 @@ public class CollisionUtils {
@NotNull Pos currentPosition, @NotNull Pos newPosition) { @NotNull Pos currentPosition, @NotNull Pos newPosition) {
final WorldBorder worldBorder = instance.getWorldBorder(); final WorldBorder worldBorder = instance.getWorldBorder();
final WorldBorder.CollisionAxis collisionAxis = worldBorder.getCollisionAxis(newPosition); final WorldBorder.CollisionAxis collisionAxis = worldBorder.getCollisionAxis(newPosition);
switch (collisionAxis) { return switch (collisionAxis) {
case NONE: case NONE ->
// Apply velocity + gravity // Apply velocity + gravity
return newPosition; newPosition;
case BOTH: case BOTH ->
// Apply Y velocity/gravity // Apply Y velocity/gravity
return new Pos(currentPosition.x(), newPosition.y(), currentPosition.z()); new Pos(currentPosition.x(), newPosition.y(), currentPosition.z());
case X: case X ->
// Apply Y/Z velocity/gravity // Apply Y/Z velocity/gravity
return new Pos(currentPosition.x(), newPosition.y(), newPosition.z()); new Pos(currentPosition.x(), newPosition.y(), newPosition.z());
case Z: case Z ->
// Apply X/Y velocity/gravity // Apply X/Y velocity/gravity
return new Pos(newPosition.x(), newPosition.y(), currentPosition.z()); new Pos(newPosition.x(), newPosition.y(), currentPosition.z());
} };
throw new IllegalStateException("Something weird happened...");
} }
public static class PhysicsResult { public record PhysicsResult(Pos newPosition,
private final Pos newPosition; Vec newVelocity,
private final Vec newVelocity; boolean isOnGround) {
private final boolean isOnGround;
public PhysicsResult(Pos newPosition, Vec newVelocity, boolean isOnGround) {
this.newPosition = newPosition;
this.newVelocity = newVelocity;
this.isOnGround = isOnGround;
}
public Pos newPosition() {
return newPosition;
}
public Vec newVelocity() {
return newVelocity;
}
public boolean isOnGround() {
return isOnGround;
}
} }
private static class StepResult { private record StepResult(Vec newPosition,
private final Vec newPosition; boolean foundCollision) {
private final boolean foundCollision;
public StepResult(Vec newPosition, boolean foundCollision) {
this.newPosition = newPosition;
this.foundCollision = foundCollision;
}
} }
} }

View File

@ -32,7 +32,7 @@ public class Color implements RGBLike {
* @param rgbLike the color * @param rgbLike the color
*/ */
public Color(RGBLike rgbLike) { public Color(RGBLike rgbLike) {
this(rgbLike.red(), rgbLike.blue(), rgbLike.green()); this(rgbLike.red(), rgbLike.green(), rgbLike.blue());
} }
/** /**

View File

@ -81,7 +81,7 @@ public final class CommandManager {
} }
/** /**
* Gets if a command with the name {@code commandName} already exists or name. * Gets if a command with the name {@code commandName} already exists or not.
* *
* @param commandName the command name to check * @param commandName the command name to check
* @return true if the command does exist * @return true if the command does exist
@ -92,29 +92,23 @@ public final class CommandManager {
} }
/** /**
* Executes a command for a {@link ConsoleSender}. * Executes a command for a {@link CommandSender}.
* *
* @param sender the sender of the command * @param sender the sender of the command
* @param command the raw command string (without the command prefix) * @param command the raw command string (without the command prefix)
* @return the execution result * @return the execution result
*/ */
public @NotNull CommandResult execute(@NotNull CommandSender sender, @NotNull String command) { public @NotNull CommandResult execute(@NotNull CommandSender sender, @NotNull String command) {
// Command event // Command event
if (sender instanceof Player) { if (sender instanceof Player player) {
Player player = (Player) sender;
PlayerCommandEvent playerCommandEvent = new PlayerCommandEvent(player, command); PlayerCommandEvent playerCommandEvent = new PlayerCommandEvent(player, command);
EventDispatcher.call(playerCommandEvent); EventDispatcher.call(playerCommandEvent);
if (playerCommandEvent.isCancelled()) if (playerCommandEvent.isCancelled())
return CommandResult.of(CommandResult.Type.CANCELLED, command); return CommandResult.of(CommandResult.Type.CANCELLED, command);
command = playerCommandEvent.getCommand(); command = playerCommandEvent.getCommand();
} }
// Process the command // Process the command
final var result = dispatcher.execute(sender, command); final CommandResult result = dispatcher.execute(sender, command);
if (result.getType() == CommandResult.Type.UNKNOWN) { if (result.getType() == CommandResult.Type.UNKNOWN) {
if (unknownCommandCallback != null) { if (unknownCommandCallback != null) {
this.unknownCommandCallback.apply(sender, command); this.unknownCommandCallback.apply(sender, command);
@ -124,8 +118,8 @@ public final class CommandManager {
} }
/** /**
* Executes the command using a {@link ServerSender} to do not * Executes the command using a {@link ServerSender}. This can be used
* print the command messages, and rely instead on the command return data. * to run a silent command (nothing is printed to console).
* *
* @see #execute(CommandSender, String) * @see #execute(CommandSender, String)
*/ */
@ -440,8 +434,7 @@ public final class CommandManager {
return literalNode; return literalNode;
} }
@NotNull private @NotNull DeclareCommandsPacket.Node createMainNode(@NotNull String name, boolean executable) {
private DeclareCommandsPacket.Node createMainNode(@NotNull String name, boolean executable) {
DeclareCommandsPacket.Node literalNode = new DeclareCommandsPacket.Node(); DeclareCommandsPacket.Node literalNode = new DeclareCommandsPacket.Node();
literalNode.flags = DeclareCommandsPacket.getFlag(DeclareCommandsPacket.NodeType.LITERAL, executable, false, false); literalNode.flags = DeclareCommandsPacket.getFlag(DeclareCommandsPacket.NodeType.LITERAL, executable, false, false);
literalNode.name = name; literalNode.name = name;

View File

@ -367,10 +367,8 @@ public class Command {
node = findNode.apply(node, Collections.singleton(literal)); node = findNode.apply(node, Collections.singleton(literal));
continue; continue;
} else if (argument instanceof ArgumentWord) { } else if (argument instanceof ArgumentWord argumentWord) {
ArgumentWord argumentWord = (ArgumentWord) argument;
if (argumentWord.hasRestrictions()) { if (argumentWord.hasRestrictions()) {
addArguments.accept(node, arguments); addArguments.accept(node, arguments);
arguments = new ArrayList<>(); arguments = new ArrayList<>();

View File

@ -65,8 +65,7 @@ public class CommandDispatcher {
this.cache.invalidateAll(); this.cache.invalidateAll();
} }
@NotNull public @NotNull Set<Command> getCommands() {
public Set<Command> getCommands() {
return Collections.unmodifiableSet(commands); return Collections.unmodifiableSet(commands);
} }
@ -76,8 +75,7 @@ public class CommandDispatcher {
* @param commandName the command name * @param commandName the command name
* @return the {@link Command} associated with the name, null if not any * @return the {@link Command} associated with the name, null if not any
*/ */
@Nullable public @Nullable Command findCommand(@NotNull String commandName) {
public Command findCommand(@NotNull String commandName) {
commandName = commandName.toLowerCase(); commandName = commandName.toLowerCase();
return commandMap.getOrDefault(commandName, null); return commandMap.getOrDefault(commandName, null);
} }
@ -89,8 +87,7 @@ public class CommandDispatcher {
* @param commandString the command with the argument(s) * @param commandString the command with the argument(s)
* @return the command result * @return the command result
*/ */
@NotNull public @NotNull CommandResult execute(@NotNull CommandSender source, @NotNull String commandString) {
public CommandResult execute(@NotNull CommandSender source, @NotNull String commandString) {
CommandResult commandResult = parse(commandString); CommandResult commandResult = parse(commandString);
ParsedCommand parsedCommand = commandResult.parsedCommand; ParsedCommand parsedCommand = commandResult.parsedCommand;
if (parsedCommand != null) { if (parsedCommand != null) {
@ -105,10 +102,8 @@ public class CommandDispatcher {
* @param commandString the command (containing the command name and the args if any) * @param commandString the command (containing the command name and the args if any)
* @return the parsing result * @return the parsing result
*/ */
@NotNull public @NotNull CommandResult parse(@NotNull String commandString) {
public CommandResult parse(@NotNull String commandString) {
commandString = commandString.trim(); commandString = commandString.trim();
// Verify if the result is cached // Verify if the result is cached
{ {
final CommandResult cachedResult = cache.getIfPresent(commandString); final CommandResult cachedResult = cache.getIfPresent(commandString);
@ -134,18 +129,15 @@ public class CommandDispatcher {
findParsedCommand(command, commandName, commandQueryResult.args, commandString, result); findParsedCommand(command, commandName, commandQueryResult.args, commandString, result);
// Cache result // Cache result
{ this.cache.put(commandString, result);
this.cache.put(commandString, result);
}
return result; return result;
} }
@Nullable private @Nullable ParsedCommand findParsedCommand(@NotNull Command command,
private ParsedCommand findParsedCommand(@NotNull Command command, @NotNull String commandName, @NotNull String[] args,
@NotNull String commandName, @NotNull String[] args, @NotNull String commandString,
@NotNull String commandString, @NotNull CommandResult result) {
@NotNull CommandResult result) {
final boolean hasArgument = args.length > 0; final boolean hasArgument = args.length > 0;
// Search for subcommand // Search for subcommand
@ -162,41 +154,37 @@ public class CommandDispatcher {
final String input = commandName + StringUtils.SPACE + String.join(StringUtils.SPACE, args); final String input = commandName + StringUtils.SPACE + String.join(StringUtils.SPACE, args);
ParsedCommand parsedCommand = new ParsedCommand(); ParsedCommand parsedCommand = new ParsedCommand();
parsedCommand.command = command; parsedCommand.command = command;
parsedCommand.commandString = commandString; parsedCommand.commandString = commandString;
// The default executor should be used if no argument is provided // The default executor should be used if no argument is provided
{ if (!hasArgument) {
if (!hasArgument) { Optional<CommandSyntax> optionalSyntax = command.getSyntaxes()
Optional<CommandSyntax> optionalSyntax = command.getSyntaxes() .stream()
.stream() .filter(syntax -> syntax.getArguments().length == 0)
.filter(syntax -> syntax.getArguments().length == 0) .findFirst();
.findFirst();
if (optionalSyntax.isPresent()) { if (optionalSyntax.isPresent()) {
// Empty syntax found // Empty syntax found
final CommandSyntax syntax = optionalSyntax.get(); final CommandSyntax syntax = optionalSyntax.get();
parsedCommand.syntax = syntax;
parsedCommand.executor = syntax.getExecutor();
parsedCommand.context = new CommandContext(input);
parsedCommand.syntax = syntax; result.type = CommandResult.Type.SUCCESS;
parsedCommand.executor = syntax.getExecutor(); result.parsedCommand = parsedCommand;
return parsedCommand;
} else {
// No empty syntax, use default executor if any
final CommandExecutor defaultExecutor = command.getDefaultExecutor();
if (defaultExecutor != null) {
parsedCommand.executor = defaultExecutor;
parsedCommand.context = new CommandContext(input); parsedCommand.context = new CommandContext(input);
result.type = CommandResult.Type.SUCCESS; result.type = CommandResult.Type.SUCCESS;
result.parsedCommand = parsedCommand; result.parsedCommand = parsedCommand;
return parsedCommand; return parsedCommand;
} else {
// No empty syntax, use default executor if any
final CommandExecutor defaultExecutor = command.getDefaultExecutor();
if (defaultExecutor != null) {
parsedCommand.executor = defaultExecutor;
parsedCommand.context = new CommandContext(input);
result.type = CommandResult.Type.SUCCESS;
result.parsedCommand = parsedCommand;
return parsedCommand;
}
} }
} }
} }
@ -207,7 +195,6 @@ public class CommandDispatcher {
final Collection<CommandSyntax> syntaxes = command.getSyntaxes(); final Collection<CommandSyntax> syntaxes = command.getSyntaxes();
// Contains all the fully validated syntaxes (we later find the one with the most amount of arguments) // Contains all the fully validated syntaxes (we later find the one with the most amount of arguments)
List<ValidSyntaxHolder> validSyntaxes = new ArrayList<>(syntaxes.size()); List<ValidSyntaxHolder> validSyntaxes = new ArrayList<>(syntaxes.size());
// Contains all the syntaxes that are not fully correct, used to later, retrieve the "most correct syntax" // 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 // Number of correct argument - The data about the failing argument
Int2ObjectRBTreeMap<CommandSuggestionHolder> syntaxesSuggestions = new Int2ObjectRBTreeMap<>(Collections.reverseOrder()); Int2ObjectRBTreeMap<CommandSuggestionHolder> syntaxesSuggestions = new Int2ObjectRBTreeMap<>(Collections.reverseOrder());
@ -233,29 +220,25 @@ public class CommandDispatcher {
result.parsedCommand = parsedCommand; result.parsedCommand = parsedCommand;
return parsedCommand; return parsedCommand;
} }
} }
// No all-correct syntax, find the closest one to use the argument callback // No all-correct syntax, find the closest one to use the argument callback
{ if (!syntaxesSuggestions.isEmpty()) {
// Get closest valid syntax final int max = syntaxesSuggestions.firstIntKey(); // number of correct arguments in the most correct syntax
if (!syntaxesSuggestions.isEmpty()) { final CommandSuggestionHolder suggestionHolder = syntaxesSuggestions.get(max);
final int max = syntaxesSuggestions.firstIntKey(); // number of correct arguments in the most correct syntax final CommandSyntax syntax = suggestionHolder.syntax;
final CommandSuggestionHolder suggestionHolder = syntaxesSuggestions.get(max); final ArgumentSyntaxException argumentSyntaxException = suggestionHolder.argumentSyntaxException;
final CommandSyntax syntax = suggestionHolder.syntax; final int argIndex = suggestionHolder.argIndex;
final ArgumentSyntaxException argumentSyntaxException = suggestionHolder.argumentSyntaxException;
final int argIndex = suggestionHolder.argIndex;
// Found the closest syntax with at least 1 correct argument // Found the closest syntax with at least 1 correct argument
final Argument<?> argument = syntax.getArguments()[argIndex]; final Argument<?> argument = syntax.getArguments()[argIndex];
if (argument.hasErrorCallback()) { if (argument.hasErrorCallback() && argumentSyntaxException != null) {
parsedCommand.callback = argument.getCallback(); parsedCommand.callback = argument.getCallback();
parsedCommand.argumentSyntaxException = argumentSyntaxException; parsedCommand.argumentSyntaxException = argumentSyntaxException;
result.type = CommandResult.Type.INVALID_SYNTAX; result.type = CommandResult.Type.INVALID_SYNTAX;
result.parsedCommand = parsedCommand; result.parsedCommand = parsedCommand;
return parsedCommand; return parsedCommand;
}
} }
} }

View File

@ -10,23 +10,19 @@ public class CommandResult {
protected ParsedCommand parsedCommand; protected ParsedCommand parsedCommand;
protected CommandData commandData; protected CommandData commandData;
@NotNull public @NotNull Type getType() {
public Type getType() {
return type; return type;
} }
@NotNull public @NotNull String getInput() {
public String getInput() {
return input; return input;
} }
@Nullable public @Nullable ParsedCommand getParsedCommand() {
public ParsedCommand getParsedCommand() {
return parsedCommand; return parsedCommand;
} }
@Nullable public @Nullable CommandData getCommandData() {
public CommandData getCommandData() {
return commandData; return commandData;
} }
@ -35,30 +31,25 @@ public class CommandResult {
* Command and syntax successfully found. * Command and syntax successfully found.
*/ */
SUCCESS, SUCCESS,
/** /**
* Command found, but the syntax is invalid. * Command found, but the syntax is invalid.
* Executor sets to {@link Command#getDefaultExecutor()}. * Executor sets to {@link Command#getDefaultExecutor()}.
*/ */
INVALID_SYNTAX, INVALID_SYNTAX,
/** /**
* Command cancelled by an event listener. * Command cancelled by an event listener.
*/ */
CANCELLED, CANCELLED,
/** /**
* Command is not registered, it is also the default result type. * Command is not registered, it is also the default result type.
*/ */
UNKNOWN UNKNOWN
} }
@NotNull public static @NotNull CommandResult of(@NotNull Type type, @NotNull String input) {
public static CommandResult of(@NotNull Type type, @NotNull String input) {
CommandResult result = new CommandResult(); CommandResult result = new CommandResult();
result.type = type; result.type = type;
result.input = input; result.input = input;
return result; return result;
} }
} }

View File

@ -7,6 +7,8 @@ import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/** /**
* Represents a {@link Command} ready to be executed (already parsed). * Represents a {@link Command} ready to be executed (already parsed).
*/ */
@ -35,16 +37,14 @@ public class ParsedCommand {
* @param source the command source * @param source the command source
* @return the command data, null if none * @return the command data, null if none
*/ */
@Nullable public @Nullable CommandData execute(@NotNull CommandSender source) {
public CommandData execute(@NotNull CommandSender source) {
// Global listener // Global listener
command.globalListener(source, context, commandString); command.globalListener(source, Objects.requireNonNullElseGet(context, () -> new CommandContext(commandString)), commandString);
// Command condition check // Command condition check
final CommandCondition condition = command.getCondition(); final CommandCondition condition = command.getCondition();
if (condition != null) { if (condition != null) {
final boolean result = condition.canUse(source, commandString); final boolean result = condition.canUse(source, commandString);
if (!result) if (!result) return null;
return null;
} }
// Condition is respected // Condition is respected
if (executor != null) { if (executor != null) {
@ -57,16 +57,16 @@ public class ParsedCommand {
context.retrieveDefaultValues(syntax.getDefaultValuesMap()); context.retrieveDefaultValues(syntax.getDefaultValuesMap());
try { try {
executor.apply(source, context); executor.apply(source, context);
} catch (Exception exception) { } catch (Throwable throwable) {
MinecraftServer.getExceptionManager().handleException(exception); MinecraftServer.getExceptionManager().handleException(throwable);
} }
} }
} else { } else {
// The executor is probably the default one // The executor is probably the default one
try { try {
executor.apply(source, context); executor.apply(source, context);
} catch (Exception exception) { } catch (Throwable throwable) {
MinecraftServer.getExceptionManager().handleException(exception); MinecraftServer.getExceptionManager().handleException(throwable);
} }
} }
} else if (callback != null && argumentSyntaxException != null) { } else if (callback != null && argumentSyntaxException != null) {
@ -83,8 +83,7 @@ public class ParsedCommand {
return context.getReturnData(); return context.getReturnData();
} }
@NotNull public static @NotNull ParsedCommand withDefaultExecutor(@NotNull Command command, @NotNull String input) {
public static ParsedCommand withDefaultExecutor(@NotNull Command command, @NotNull String input) {
ParsedCommand parsedCommand = new ParsedCommand(); ParsedCommand parsedCommand = new ParsedCommand();
parsedCommand.command = command; parsedCommand.command = command;
parsedCommand.commandString = input; parsedCommand.commandString = input;
@ -92,5 +91,4 @@ public class ParsedCommand {
parsedCommand.context = new CommandContext(input); parsedCommand.context = new CommandContext(input);
return parsedCommand; return parsedCommand;
} }
} }

View File

@ -13,6 +13,7 @@ public class ArgumentBlockState extends Argument<Block> {
public static final int NO_BLOCK = 1; public static final int NO_BLOCK = 1;
public static final int INVALID_BLOCK = 2; public static final int INVALID_BLOCK = 2;
public static final int INVALID_PROPERTY = 3; public static final int INVALID_PROPERTY = 3;
public static final int INVALID_PROPERTY_VALUE = 4;
public ArgumentBlockState(@NotNull String id) { public ArgumentBlockState(@NotNull String id) {
super(id, true, false); super(id, true, false);
@ -58,7 +59,11 @@ public class ArgumentBlockState extends Argument<Block> {
// Compute properties // Compute properties
final String query = input.substring(nbtIndex); final String query = input.substring(nbtIndex);
final var propertyMap = BlockUtils.parseProperties(query); final var propertyMap = BlockUtils.parseProperties(query);
return block.withProperties(propertyMap); try {
return block.withProperties(propertyMap);
} catch (IllegalArgumentException e) {
throw new ArgumentSyntaxException("Invalid property values", input, INVALID_PROPERTY_VALUE);
}
} }
} }

View File

@ -245,7 +245,7 @@ public class ArgumentEntity extends Argument<EntityFinder> {
break; break;
case "level": case "level":
try { try {
final IntRange level = ArgumentIntRange.staticParse(value); final IntRange level = Argument.parse(new ArgumentIntRange(value));
entityFinder.setLevel(level); entityFinder.setLevel(level);
} catch (ArgumentSyntaxException e) { } catch (ArgumentSyntaxException e) {
throw new ArgumentSyntaxException("Invalid level number", input, INVALID_ARGUMENT_VALUE); throw new ArgumentSyntaxException("Invalid level number", input, INVALID_ARGUMENT_VALUE);
@ -253,7 +253,7 @@ public class ArgumentEntity extends Argument<EntityFinder> {
break; break;
case "distance": case "distance":
try { try {
final IntRange distance = ArgumentIntRange.staticParse(value); final IntRange distance = Argument.parse(new ArgumentIntRange(value));
entityFinder.setDistance(distance); entityFinder.setDistance(distance);
} catch (ArgumentSyntaxException e) { } catch (ArgumentSyntaxException e) {
throw new ArgumentSyntaxException("Invalid level number", input, INVALID_ARGUMENT_VALUE); throw new ArgumentSyntaxException("Invalid level number", input, INVALID_ARGUMENT_VALUE);

View File

@ -1,66 +1,16 @@
package net.minestom.server.command.builder.arguments.minecraft; package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.NodeMaker;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import net.minestom.server.utils.math.FloatRange; import net.minestom.server.utils.math.FloatRange;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Pattern;
/** /**
* Represents an argument which will give you an {@link FloatRange}. * Represents an argument which will give you an {@link FloatRange}.
* <p> * <p>
* Example: ..3, 3.., 5..10, 15 * Example: ..3, 3.., 5..10, 15
*/ */
public class ArgumentFloatRange extends ArgumentRange<FloatRange> { public class ArgumentFloatRange extends ArgumentRange<FloatRange, Float> {
public ArgumentFloatRange(String id) { public ArgumentFloatRange(String id) {
super(id); super(id, "minecraft:float_range", Float.MIN_VALUE, Float.MAX_VALUE, Float::parseFloat, FloatRange::new);
}
@NotNull
@Override
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]);
} else {
// Format NUMBER..
min = Float.parseFloat(split[0]);
max = Float.MAX_VALUE;
}
}
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);
}
}
@Override
public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, false);
argumentNode.parser = "minecraft:float_range";
nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode});
} }
@Override @Override

View File

@ -1,76 +1,16 @@
package net.minestom.server.command.builder.arguments.minecraft; package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.NodeMaker;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import net.minestom.server.utils.math.IntRange; import net.minestom.server.utils.math.IntRange;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Pattern;
/** /**
* Represents an argument which will give you an {@link IntRange}. * Represents an argument which will give you an {@link IntRange}.
* <p> * <p>
* Example: ..3, 3.., 5..10, 15 * Example: ..3, 3.., 5..10, 15
*/ */
public class ArgumentIntRange extends ArgumentRange<IntRange> { public class ArgumentIntRange extends ArgumentRange<IntRange, Integer> {
public ArgumentIntRange(String id) { public ArgumentIntRange(String id) {
super(id); super(id, "minecraft:int_range", Integer.MIN_VALUE, Integer.MAX_VALUE, Integer::parseInt, IntRange::new);
}
@NotNull
@Override
public IntRange parse(@NotNull String input) throws ArgumentSyntaxException {
return staticParse(input);
}
/**
* @deprecated use {@link Argument#parse(Argument)}
*/
@Deprecated
@NotNull
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]);
} else {
// Format NUMBER..
min = Integer.parseInt(split[0]);
max = Integer.MAX_VALUE;
}
}
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);
}
}
@Override
public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, false);
argumentNode.parser = "minecraft:int_range";
nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode});
} }
@Override @Override

View File

@ -1,17 +1,81 @@
package net.minestom.server.command.builder.arguments.minecraft; package net.minestom.server.command.builder.arguments.minecraft;
import net.minestom.server.command.builder.NodeMaker;
import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import net.minestom.server.utils.math.Range;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
/** /**
* Abstract class used by {@link ArgumentIntRange} and {@link ArgumentFloatRange}. * Abstract class used by {@link ArgumentIntRange} and {@link ArgumentFloatRange}.
* *
* @param <T> the type of the range * @param <T> the type of the range
*/ */
public abstract class ArgumentRange<T> extends Argument<T> { public abstract class ArgumentRange<T extends Range<N>, N extends Number> extends Argument<T> {
public static final int FORMAT_ERROR = -1; public static final int FORMAT_ERROR = -1;
private final N min;
private final N max;
private final Function<String, N> parser;
private final String parserName;
private final BiFunction<N, N, T> rangeConstructor;
public ArgumentRange(String id) { public ArgumentRange(@NotNull String id, String parserName, N min, N max, Function<String, N> parser, BiFunction<N, N, T> rangeConstructor) {
super(id); super(id);
this.min = min;
this.max = max;
this.parser = parser;
this.parserName = parserName;
this.rangeConstructor = rangeConstructor;
} }
@NotNull
@Override
public T parse(@NotNull String input) throws ArgumentSyntaxException {
try {
final String[] split = input.split(Pattern.quote(".."), -1);
if (split.length == 2) {
final N min;
final N max;
if (split[0].length() == 0 && split[1].length() > 0) {
// Format ..NUMBER
min = this.min;
max = parser.apply(split[1]);
} else if (split[0].length() > 0 && split[1].length() == 0) {
// Format NUMBER..
min = parser.apply(split[0]);
max = this.max;
} else if (split[0].length() > 0) {
// Format NUMBER..NUMBER
min = parser.apply(split[0]);
max = parser.apply(split[1]);
} else {
// Format ..
throw new ArgumentSyntaxException("Invalid range format", input, FORMAT_ERROR);
}
return rangeConstructor.apply(min, max);
} else if (split.length == 1) {
final N number = parser.apply(input);
return rangeConstructor.apply(number, number);
}
} catch (NumberFormatException e2) {
throw new ArgumentSyntaxException("Invalid number", input, FORMAT_ERROR);
}
throw new ArgumentSyntaxException("Invalid range format", input, FORMAT_ERROR);
}
@Override
public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, false);
argumentNode.parser = parserName;
nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode});
}
} }

View File

@ -23,7 +23,7 @@ public class ArgumentEntityType extends ArgumentRegistry<EntityType> {
@Override @Override
public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) { public void processNodes(@NotNull NodeMaker nodeMaker, boolean executable) {
DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, true); DeclareCommandsPacket.Node argumentNode = simpleArgumentNode(this, executable, false, true);
argumentNode.parser = "minecraft:entity_summon"; argumentNode.parser = "minecraft:resource_location";
argumentNode.suggestionsType = SuggestionType.SUMMONABLE_ENTITIES.getIdentifier(); argumentNode.suggestionsType = SuggestionType.SUMMONABLE_ENTITIES.getIdentifier();
nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode}); nodeMaker.addNodes(new DeclareCommandsPacket.Node[]{argumentNode});

View File

@ -18,7 +18,8 @@ import java.util.regex.Pattern;
public class ArgumentNumber<T extends Number> extends Argument<T> { public class ArgumentNumber<T extends Number> extends Argument<T> {
public static final int NOT_NUMBER_ERROR = 1; public static final int NOT_NUMBER_ERROR = 1;
public static final int RANGE_ERROR = 2; public static final int TOO_LOW_ERROR = 2;
public static final int TOO_HIGH_ERROR = 3;
protected boolean hasMin, hasMax; protected boolean hasMin, hasMax;
protected T min, max; protected T min, max;
@ -53,10 +54,10 @@ public class ArgumentNumber<T extends Number> extends Argument<T> {
// Check range // Check range
if (hasMin && comparator.compare(value, min) < 0) { if (hasMin && comparator.compare(value, min) < 0) {
throw new ArgumentSyntaxException("Input is lower than the minimum required value", input, RANGE_ERROR); throw new ArgumentSyntaxException("Input is lower than the minimum allowed value", input, TOO_LOW_ERROR);
} }
if (hasMax && comparator.compare(value, max) > 0) { if (hasMax && comparator.compare(value, max) > 0) {
throw new ArgumentSyntaxException("Input is higher than the minimum required value", input, RANGE_ERROR); throw new ArgumentSyntaxException("Input is higher than the maximum allowed value", input, TOO_HIGH_ERROR);
} }
return value; return value;

View File

@ -3,7 +3,6 @@ package net.minestom.server.coordinate;
import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockFace;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -15,8 +14,7 @@ import java.util.function.DoubleUnaryOperator;
* Can either be a {@link Pos} or {@link Vec}. * Can either be a {@link Pos} or {@link Vec}.
* Interface will become {@code sealed} in the future. * Interface will become {@code sealed} in the future.
*/ */
@ApiStatus.NonExtendable public sealed interface Point permits Vec, Pos {
public interface Point {
/** /**
* Gets the X coordinate. * Gets the X coordinate.
@ -72,6 +70,16 @@ public interface Point {
return (int) Math.floor(z()); return (int) Math.floor(z());
} }
@Contract(pure = true)
default int chunkX() {
return ChunkUtils.getChunkCoordinate(x());
}
@Contract(pure = true)
default int chunkZ() {
return ChunkUtils.getChunkCoordinate(z());
}
/** /**
* Creates a point with a modified X coordinate based on its value. * Creates a point with a modified X coordinate based on its value.
* *
@ -164,22 +172,35 @@ public interface Point {
@Contract(pure = true) @Contract(pure = true)
default @NotNull Point relative(@NotNull BlockFace face) { default @NotNull Point relative(@NotNull BlockFace face) {
switch (face) { return switch (face) {
case BOTTOM: case BOTTOM -> sub(0, 1, 0);
return sub(0, 1, 0); case TOP -> add(0, 1, 0);
case TOP: case NORTH -> sub(0, 0, 1);
return add(0, 1, 0); case SOUTH -> add(0, 0, 1);
case NORTH: case WEST -> sub(1, 0, 0);
return sub(0, 0, 1); case EAST -> add(1, 0, 0);
case SOUTH: };
return add(0, 0, 1); }
case WEST:
return sub(1, 0, 0); @Contract(pure = true)
case EAST: default double distanceSquared(double x, double y, double z) {
return add(1, 0, 0); return MathUtils.square(x() - x) + MathUtils.square(y() - y) + MathUtils.square(z() - z);
default: // should never be called }
return this;
} /**
* Gets the squared distance between this point and another.
*
* @param point the other point
* @return the squared distance
*/
@Contract(pure = true)
default double distanceSquared(@NotNull Point point) {
return distanceSquared(point.x(), point.y(), point.z());
}
@Contract(pure = true)
default double distance(double x, double y, double z) {
return Math.sqrt(distanceSquared(x, y, z));
} }
/** /**
@ -194,22 +215,11 @@ public interface Point {
*/ */
@Contract(pure = true) @Contract(pure = true)
default double distance(@NotNull Point point) { default double distance(@NotNull Point point) {
return Math.sqrt(MathUtils.square(x() - point.x()) + return distance(point.x(), point.y(), point.z());
MathUtils.square(y() - point.y()) +
MathUtils.square(z() - point.z()));
} }
/** default boolean samePoint(double x, double y, double z) {
* Gets the squared distance between this point and another. return Double.compare(x, x()) == 0 && Double.compare(y, y()) == 0 && Double.compare(z, z()) == 0;
*
* @param point the other point
* @return the squared distance
*/
@Contract(pure = true)
default double distanceSquared(@NotNull Point point) {
return MathUtils.square(x() - point.x()) +
MathUtils.square(y() - point.y()) +
MathUtils.square(z() - point.z());
} }
/** /**
@ -219,9 +229,7 @@ public interface Point {
* @return true if the two positions are similar * @return true if the two positions are similar
*/ */
default boolean samePoint(@NotNull Point point) { default boolean samePoint(@NotNull Point point) {
return Double.compare(point.x(), x()) == 0 && return samePoint(point.x(), point.y(), point.z());
Double.compare(point.y(), y()) == 0 &&
Double.compare(point.z(), z()) == 0;
} }
/** /**
@ -241,7 +249,20 @@ public interface Point {
* @return true if 'this' is in the same chunk as {@code point} * @return true if 'this' is in the same chunk as {@code point}
*/ */
default boolean sameChunk(@NotNull Point point) { default boolean sameChunk(@NotNull Point point) {
return ChunkUtils.getChunkCoordinate(x()) == ChunkUtils.getChunkCoordinate(point.x()) && return chunkX() == point.chunkX() && chunkZ() == point.chunkZ();
ChunkUtils.getChunkCoordinate(z()) == ChunkUtils.getChunkCoordinate(point.z()); }
default boolean sameBlock(int blockX, int blockY, int blockZ) {
return blockX() == blockX && blockY() == blockY && blockZ() == blockZ;
}
/**
* Gets if two points are in the same chunk.
*
* @param point the point to compare two
* @return true if 'this' is in the same chunk as {@code point}
*/
default boolean sameBlock(@NotNull Point point) {
return sameBlock(point.blockX(), point.blockY(), point.blockZ());
} }
} }

View File

@ -5,7 +5,6 @@ import net.minestom.server.utils.MathUtils;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
/** /**
@ -13,18 +12,11 @@ import java.util.function.DoubleUnaryOperator;
* <p> * <p>
* To become record and primitive. * To become record and primitive.
*/ */
public final class Pos implements Point { public record Pos(double x, double y, double z, float yaw, float pitch) implements Point {
public static final Pos ZERO = new Pos(0, 0, 0); public static final Pos ZERO = new Pos(0, 0, 0);
private final double x, y, z; public Pos {
private final float yaw, pitch; yaw = fixYaw(yaw);
public Pos(double x, double y, double z, float yaw, float pitch) {
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
} }
public Pos(double x, double y, double z) { public Pos(double x, double y, double z) {
@ -47,8 +39,7 @@ public final class Pos implements Point {
* @return the converted position * @return the converted position
*/ */
public static @NotNull Pos fromPoint(@NotNull Point point) { public static @NotNull Pos fromPoint(@NotNull Point point) {
if (point instanceof Pos) if (point instanceof Pos pos) return pos;
return (Pos) point;
return new Pos(point.x(), point.y(), point.z()); return new Pos(point.x(), point.y(), point.z());
} }
@ -108,7 +99,7 @@ public final class Pos implements Point {
@Contract(pure = true) @Contract(pure = true)
public @NotNull Pos withYaw(@NotNull DoubleUnaryOperator operator) { public @NotNull Pos withYaw(@NotNull DoubleUnaryOperator operator) {
return new Pos(x, y, z, (float) operator.applyAsDouble(yaw), pitch); return withYaw((float) operator.applyAsDouble(yaw));
} }
@Contract(pure = true) @Contract(pure = true)
@ -118,7 +109,7 @@ public final class Pos implements Point {
@Contract(pure = true) @Contract(pure = true)
public @NotNull Pos withPitch(@NotNull DoubleUnaryOperator operator) { public @NotNull Pos withPitch(@NotNull DoubleUnaryOperator operator) {
return new Pos(x, y, z, yaw, (float) operator.applyAsDouble(pitch)); return withPitch((float) operator.applyAsDouble(pitch));
} }
/** /**
@ -148,24 +139,6 @@ public final class Pos implements Point {
xz * Math.cos(Math.toRadians(rotX))); xz * Math.cos(Math.toRadians(rotX)));
} }
@Override
@Contract(pure = true)
public double x() {
return x;
}
@Override
@Contract(pure = true)
public double y() {
return y;
}
@Override
@Contract(pure = true)
public double z() {
return z;
}
/** /**
* Returns a new position based on this position fields. * Returns a new position based on this position fields.
* *
@ -180,7 +153,7 @@ public final class Pos implements Point {
@Override @Override
@Contract(pure = true) @Contract(pure = true)
public @NotNull Pos withX(@NotNull DoubleUnaryOperator operator) { public @NotNull Pos withX(@NotNull DoubleUnaryOperator operator) {
return new Pos(operator.applyAsDouble(x), y, z); return new Pos(operator.applyAsDouble(x), y, z, yaw, pitch);
} }
@Override @Override
@ -192,7 +165,7 @@ public final class Pos implements Point {
@Override @Override
@Contract(pure = true) @Contract(pure = true)
public @NotNull Pos withY(@NotNull DoubleUnaryOperator operator) { public @NotNull Pos withY(@NotNull DoubleUnaryOperator operator) {
return new Pos(x, operator.applyAsDouble(y), z); return new Pos(x, operator.applyAsDouble(y), z, yaw, pitch);
} }
@Override @Override
@ -204,7 +177,7 @@ public final class Pos implements Point {
@Override @Override
@Contract(pure = true) @Contract(pure = true)
public @NotNull Pos withZ(@NotNull DoubleUnaryOperator operator) { public @NotNull Pos withZ(@NotNull DoubleUnaryOperator operator) {
return new Pos(x, y, operator.applyAsDouble(z)); return new Pos(x, y, operator.applyAsDouble(z), yaw, pitch);
} }
@Override @Override
@ -278,51 +251,30 @@ public final class Pos implements Point {
return (Pos) Point.super.relative(face); return (Pos) Point.super.relative(face);
} }
@Contract(pure = true)
public float yaw() {
return yaw;
}
@Contract(pure = true)
public float pitch() {
return pitch;
}
@Contract(pure = true) @Contract(pure = true)
public @NotNull Vec asVec() { public @NotNull Vec asVec() {
return new Vec(x, y, z); return new Vec(x, y, z);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pos pos = (Pos) o;
return Double.compare(pos.x, x) == 0 &&
Double.compare(pos.y, y) == 0 &&
Double.compare(pos.z, z) == 0 &&
Float.compare(pos.yaw, yaw) == 0 &&
Float.compare(pos.pitch, pitch) == 0;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z, yaw, pitch);
}
@Override
public String toString() {
return "Pos{" +
"x=" + x +
", y=" + y +
", z=" + z +
", yaw=" + yaw +
", pitch=" + pitch +
'}';
}
@FunctionalInterface @FunctionalInterface
public interface Operator { public interface Operator {
@NotNull Pos apply(double x, double y, double z, float yaw, float pitch); @NotNull Pos apply(double x, double y, double z, float yaw, float pitch);
} }
/**
* Fixes a yaw value that is not between -180.0F and 180.0F
* So for example -1355.0F becomes 85.0F and 225.0F becomes -135.0F
*
* @param yaw The possible "wrong" yaw
* @return a fixed yaw
*/
private static float fixYaw(float yaw) {
yaw = yaw % 360;
if (yaw < -180.0F) {
yaw += 360.0F;
} else if (yaw > 180.0F) {
yaw -= 360.0F;
}
return yaw;
}
} }

View File

@ -5,7 +5,6 @@ import net.minestom.server.utils.MathUtils;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
/** /**
@ -13,27 +12,12 @@ import java.util.function.DoubleUnaryOperator;
* <p> * <p>
* To become record and primitive. * To become record and primitive.
*/ */
public final class Vec implements Point { public record Vec(double x, double y, double z) implements Point {
public static final Vec ZERO = new Vec(0); public static final Vec ZERO = new Vec(0);
public static final Vec ONE = new Vec(1); public static final Vec ONE = new Vec(1);
public static final double EPSILON = 0.000001; public static final double EPSILON = 0.000001;
private final double x, y, z;
/**
* Creates a new vec with the 3 coordinates set.
*
* @param x the X coordinate
* @param y the Y coordinate
* @param z the Z coordinate
*/
public Vec(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
/** /**
* Creates a new vec with the [x;z] coordinates set. Y is set to 0. * Creates a new vec with the [x;z] coordinates set. Y is set to 0.
* *
@ -61,8 +45,7 @@ public final class Vec implements Point {
* @return the converted vector * @return the converted vector
*/ */
public static @NotNull Vec fromPoint(@NotNull Point point) { public static @NotNull Vec fromPoint(@NotNull Point point) {
if (point instanceof Vec) if (point instanceof Vec vec) return vec;
return (Vec) point;
return new Vec(point.x(), point.y(), point.z()); return new Vec(point.x(), point.y(), point.z());
} }
@ -487,43 +470,6 @@ public final class Vec implements Point {
return lerp(target, interpolation.apply(alpha)); return lerp(target, interpolation.apply(alpha));
} }
@Override
public double x() {
return x;
}
@Override
public double y() {
return y;
}
@Override
public double z() {
return z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Vec vec = (Vec) o;
return Double.compare(vec.x, x) == 0 && Double.compare(vec.y, y) == 0 && Double.compare(vec.z, z) == 0;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z);
}
@Override
public String toString() {
return "Vec{" +
"x=" + x +
", y=" + y +
", z=" + z +
'}';
}
@FunctionalInterface @FunctionalInterface
public interface Operator { public interface Operator {
/** /**

View File

@ -13,6 +13,7 @@ import java.util.Set;
* <p> * <p>
* See {@link DataImpl} for the default implementation. * See {@link DataImpl} for the default implementation.
*/ */
@Deprecated
public abstract class Data implements PublicCloneable<Data> { public abstract class Data implements PublicCloneable<Data> {
public static final Data EMPTY = new Data() { public static final Data EMPTY = new Data() {

View File

@ -19,7 +19,7 @@ public interface DataContainer {
* meaning that this will be null if no data has been defined. * meaning that this will be null if no data has been defined.
* *
* @return the {@link Data} of this container, can be null * @return the {@link Data} of this container, can be null
* @deprecated use the tag API https://wiki.minestom.com/feature/tags * @deprecated use the tag API https://wiki.minestom.net/feature/tags
*/ */
@Deprecated @Deprecated
@Nullable Data getData(); @Nullable Data getData();
@ -31,8 +31,8 @@ public interface DataContainer {
* on your use-case. * on your use-case.
* *
* @param data the new {@link Data} of this container, null to remove it * @param data the new {@link Data} of this container, null to remove it
* @deprecated use the tag API https://wiki.minestom.com/feature/tags * @deprecated use the tag API https://wiki.minestom.net/feature/tags
*/ */
@Deprecated @Deprecated
void setData(@Nullable Data data); void setData(@Nullable Data data);
} }

View File

@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap;
/** /**
* {@link Data} implementation which uses a {@link ConcurrentHashMap}. * {@link Data} implementation which uses a {@link ConcurrentHashMap}.
*/ */
@Deprecated
public class DataImpl extends Data { public class DataImpl extends Data {
protected final ConcurrentHashMap<String, Object> data = new ConcurrentHashMap<>(); protected final ConcurrentHashMap<String, Object> data = new ConcurrentHashMap<>();

View File

@ -23,6 +23,7 @@ import java.util.UUID;
* A lot of types are already registered by default so you do not have to add all of them manually, * A lot of types are already registered by default so you do not have to add all of them manually,
* you can verify if {@link #getDataType(Class)} returns null for the desired type, if it is then you will need to register it. * you can verify if {@link #getDataType(Class)} returns null for the desired type, if it is then you will need to register it.
*/ */
@Deprecated
public final class DataManager { public final class DataManager {
private final Map<Class, DataType> dataTypeMap = new HashMap<>(); private final Map<Class, DataType> dataTypeMap = new HashMap<>();

View File

@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
* *
* @param <T> the type of the object * @param <T> the type of the object
*/ */
@Deprecated
public abstract class DataType<T> { public abstract class DataType<T> {
/** /**

View File

@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull;
* <p> * <p>
* See {@link SerializableDataImpl} for the default implementation. * See {@link SerializableDataImpl} for the default implementation.
*/ */
@Deprecated
public abstract class SerializableData extends Data { public abstract class SerializableData extends Data {
/** /**

View File

@ -19,6 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
/** /**
* {@link SerializableData} implementation based on {@link DataImpl}. * {@link SerializableData} implementation based on {@link DataImpl}.
*/ */
@Deprecated
public class SerializableDataImpl extends SerializableData { public class SerializableDataImpl extends SerializableData {
protected static final DataManager DATA_MANAGER = MinecraftServer.getDataManager(); protected static final DataManager DATA_MANAGER = MinecraftServer.getDataManager();

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
package net.minestom.server.entity; package net.minestom.server.entity;
import com.extollit.gaming.ai.path.HydrazinePathFinder; import com.extollit.gaming.ai.path.HydrazinePathFinder;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.ai.EntityAI; import net.minestom.server.entity.ai.EntityAI;
import net.minestom.server.entity.ai.EntityAIGroup; import net.minestom.server.entity.ai.EntityAIGroup;
@ -49,7 +48,7 @@ public class EntityCreature extends LivingEntity implements NavigableEntity, Ent
aiTick(time); aiTick(time);
// Path finding // Path finding
this.navigator.tick(getAttributeValue(Attribute.MOVEMENT_SPEED)); this.navigator.tick();
// Fire, item pickup, ... // Fire, item pickup, ...
super.update(time); super.update(time);

View File

@ -16,8 +16,7 @@ public enum EntitySpawnType {
packet.uuid = entity.getUuid(); packet.uuid = entity.getUuid();
packet.type = entity.getEntityType().id(); packet.type = entity.getEntityType().id();
packet.position = entity.getPosition(); packet.position = entity.getPosition();
if (entity.getEntityMeta() instanceof ObjectDataProvider) { if (entity.getEntityMeta() instanceof ObjectDataProvider objectDataProvider) {
ObjectDataProvider objectDataProvider = (ObjectDataProvider) entity.getEntityMeta();
packet.data = objectDataProvider.getObjectData(); packet.data = objectDataProvider.getObjectData();
if (objectDataProvider.requiresVelocityPacketAtSpawn()) { if (objectDataProvider.requiresVelocityPacketAtSpawn()) {
final var velocity = entity.getVelocityForPacket(); final var velocity = entity.getVelocityForPacket();
@ -57,8 +56,7 @@ public enum EntitySpawnType {
SpawnExperienceOrbPacket packet = new SpawnExperienceOrbPacket(); SpawnExperienceOrbPacket packet = new SpawnExperienceOrbPacket();
packet.entityId = entity.getEntityId(); packet.entityId = entity.getEntityId();
packet.position = entity.getPosition(); packet.position = entity.getPosition();
if (entity.getEntityMeta() instanceof ExperienceOrbMeta) { if (entity.getEntityMeta() instanceof ExperienceOrbMeta experienceOrbMeta) {
ExperienceOrbMeta experienceOrbMeta = (ExperienceOrbMeta) entity.getEntityMeta();
packet.expCount = (short) experienceOrbMeta.getCount(); packet.expCount = (short) experienceOrbMeta.getCount();
} }
return packet; return packet;
@ -70,8 +68,7 @@ public enum EntitySpawnType {
SpawnPaintingPacket packet = new SpawnPaintingPacket(); SpawnPaintingPacket packet = new SpawnPaintingPacket();
packet.entityId = entity.getEntityId(); packet.entityId = entity.getEntityId();
packet.entityUuid = entity.getUuid(); packet.entityUuid = entity.getUuid();
if (entity.getEntityMeta() instanceof PaintingMeta) { if (entity.getEntityMeta() instanceof PaintingMeta paintingMeta) {
PaintingMeta paintingMeta = (PaintingMeta) entity.getEntityMeta();
packet.motive = paintingMeta.getMotive().ordinal(); packet.motive = paintingMeta.getMotive().ordinal();
packet.position = new Vec( packet.position = new Vec(
Math.max(0, (paintingMeta.getMotive().getWidth() >> 1) - 1), Math.max(0, (paintingMeta.getMotive().getWidth() >> 1) - 1),
@ -79,18 +76,10 @@ public enum EntitySpawnType {
0 0
); );
switch (paintingMeta.getDirection()) { switch (paintingMeta.getDirection()) {
case SOUTH: case SOUTH -> packet.direction = 0;
packet.direction = 0; case WEST -> packet.direction = 1;
break; case NORTH -> packet.direction = 2;
case WEST: case EAST -> packet.direction = 3;
packet.direction = 1;
break;
case NORTH:
packet.direction = 2;
break;
case EAST:
packet.direction = 3;
break;
} }
} else { } else {
packet.position = Vec.ZERO; packet.position = Vec.ZERO;

View File

@ -3,15 +3,13 @@ package net.minestom.server.entity;
import net.minestom.server.registry.ProtocolObject; import net.minestom.server.registry.ProtocolObject;
import net.minestom.server.registry.Registry; import net.minestom.server.registry.Registry;
import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collection; import java.util.Collection;
@ApiStatus.NonExtendable public sealed interface EntityType extends ProtocolObject, EntityTypes permits EntityTypeImpl {
public interface EntityType extends ProtocolObject, EntityTypeConstants {
/** /**
* Returns the entity registry. * Returns the entity registry.
* *

View File

@ -22,10 +22,7 @@ import net.minestom.server.entity.metadata.monster.raider.*;
import net.minestom.server.entity.metadata.monster.skeleton.SkeletonMeta; import net.minestom.server.entity.metadata.monster.skeleton.SkeletonMeta;
import net.minestom.server.entity.metadata.monster.skeleton.StrayMeta; import net.minestom.server.entity.metadata.monster.skeleton.StrayMeta;
import net.minestom.server.entity.metadata.monster.skeleton.WitherSkeletonMeta; import net.minestom.server.entity.metadata.monster.skeleton.WitherSkeletonMeta;
import net.minestom.server.entity.metadata.monster.zombie.DrownedMeta; import net.minestom.server.entity.metadata.monster.zombie.*;
import net.minestom.server.entity.metadata.monster.zombie.ZombieMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombieVillagerMeta;
import net.minestom.server.entity.metadata.monster.zombie.ZombifiedPiglinMeta;
import net.minestom.server.entity.metadata.other.*; import net.minestom.server.entity.metadata.other.*;
import net.minestom.server.entity.metadata.villager.VillagerMeta; import net.minestom.server.entity.metadata.villager.VillagerMeta;
import net.minestom.server.entity.metadata.villager.WanderingTraderMeta; import net.minestom.server.entity.metadata.villager.WanderingTraderMeta;
@ -45,7 +42,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction; import java.util.function.BiFunction;
final class EntityTypeImpl implements EntityType { record EntityTypeImpl(Registry.EntityEntry registry) implements EntityType {
private static final Registry.Container<EntityType> CONTAINER = new Registry.Container<>(Registry.Resource.ENTITIES, private static final Registry.Container<EntityType> CONTAINER = new Registry.Container<>(Registry.Resource.ENTITIES,
(container, namespace, object) -> container.register(new EntityTypeImpl(Registry.entity(namespace, object, null)))); (container, namespace, object) -> container.register(new EntityTypeImpl(Registry.entity(namespace, object, null))));
private static final Map<String, BiFunction<Entity, Metadata, EntityMeta>> ENTITY_META_SUPPLIER = createMetaMap(); private static final Map<String, BiFunction<Entity, Metadata, EntityMeta>> ENTITY_META_SUPPLIER = createMetaMap();
@ -119,6 +116,7 @@ final class EntityTypeImpl implements EntityType {
supplier.put("minecraft:guardian", GuardianMeta::new); supplier.put("minecraft:guardian", GuardianMeta::new);
supplier.put("minecraft:hoglin", HoglinMeta::new); supplier.put("minecraft:hoglin", HoglinMeta::new);
supplier.put("minecraft:horse", HorseMeta::new); supplier.put("minecraft:horse", HorseMeta::new);
supplier.put("minecraft:husk", HuskMeta::new);
supplier.put("minecraft:illusioner", IllusionerMeta::new); supplier.put("minecraft:illusioner", IllusionerMeta::new);
supplier.put("minecraft:iron_golem", IronGolemMeta::new); supplier.put("minecraft:iron_golem", IronGolemMeta::new);
supplier.put("minecraft:item", ItemEntityMeta::new); supplier.put("minecraft:item", ItemEntityMeta::new);
@ -244,17 +242,6 @@ final class EntityTypeImpl implements EntityType {
return result; return result;
} }
private final Registry.EntityEntry registry;
EntityTypeImpl(Registry.EntityEntry registry) {
this.registry = registry;
}
@Override
public @NotNull Registry.EntityEntry registry() {
return registry;
}
@Override @Override
public String toString() { public String toString() {
return name(); return name();

View File

@ -3,37 +3,44 @@ package net.minestom.server.entity;
import net.minestom.server.item.attribute.AttributeSlot; import net.minestom.server.item.attribute.AttributeSlot;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*;
public enum EquipmentSlot { public enum EquipmentSlot {
MAIN_HAND, MAIN_HAND(false, -1),
OFF_HAND, OFF_HAND(false, -1),
BOOTS, BOOTS(true, BOOTS_SLOT),
LEGGINGS, LEGGINGS(true, LEGGINGS_SLOT),
CHESTPLATE, CHESTPLATE(true, CHESTPLATE_SLOT),
HELMET; HELMET(true, HELMET_SLOT);
private final boolean armor;
private final int armorSlot;
EquipmentSlot(boolean armor, int armorSlot) {
this.armor = armor;
this.armorSlot = armorSlot;
}
public boolean isHand() { public boolean isHand() {
return this == MAIN_HAND || this == OFF_HAND; return !armor;
} }
public boolean isArmor() { public boolean isArmor() {
return !isHand(); return armor;
}
public int armorSlot() {
return armorSlot;
} }
public static EquipmentSlot fromAttributeSlot(@NotNull AttributeSlot attributeSlot) { public static EquipmentSlot fromAttributeSlot(@NotNull AttributeSlot attributeSlot) {
switch (attributeSlot) { return switch (attributeSlot) {
case MAINHAND: case MAINHAND -> MAIN_HAND;
return MAIN_HAND; case OFFHAND -> OFF_HAND;
case OFFHAND: case FEET -> BOOTS;
return OFF_HAND; case LEGS -> LEGGINGS;
case FEET: case CHEST -> CHESTPLATE;
return BOOTS; case HEAD -> HELMET;
case LEGS: };
return LEGGINGS;
case CHEST:
return CHESTPLATE;
case HEAD:
return HELMET;
}
throw new IllegalStateException("Something weird happened");
} }
} }

View File

@ -3,7 +3,7 @@ package net.minestom.server.entity;
import net.minestom.server.entity.metadata.item.ItemEntityMeta; import net.minestom.server.entity.metadata.item.ItemEntityMeta;
import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.entity.EntityItemMergeEvent; import net.minestom.server.event.entity.EntityItemMergeEvent;
import net.minestom.server.instance.Chunk; import net.minestom.server.instance.EntityTracker;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.StackingRule; import net.minestom.server.item.StackingRule;
import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.Cooldown;
@ -13,7 +13,6 @@ import org.jetbrains.annotations.Nullable;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.TemporalUnit; import java.time.temporal.TemporalUnit;
import java.util.Set;
/** /**
* Represents an item on the ground. * Represents an item on the ground.
@ -71,47 +70,26 @@ public class ItemEntity extends Entity {
(mergeDelay == null || !Cooldown.hasCooldown(time, lastMergeCheck, mergeDelay))) { (mergeDelay == null || !Cooldown.hasCooldown(time, lastMergeCheck, mergeDelay))) {
this.lastMergeCheck = time; this.lastMergeCheck = time;
final Chunk chunk = instance.getChunkAt(getPosition()); this.instance.getEntityTracker().nearbyEntities(position, mergeRange,
final Set<Entity> entities = instance.getChunkEntities(chunk); EntityTracker.Target.ITEMS, itemEntity -> {
for (Entity entity : entities) { if (itemEntity == this) return;
if (entity instanceof ItemEntity) { if (!itemEntity.isPickable() || !itemEntity.isMergeable()) return;
if (getDistance(itemEntity) > mergeRange) return;
// Do not merge with itself final ItemStack itemStackEntity = itemEntity.getItemStack();
if (entity == this) final StackingRule stackingRule = itemStack.getStackingRule();
continue; final boolean canStack = stackingRule.canBeStacked(itemStack, itemStackEntity);
final ItemEntity itemEntity = (ItemEntity) entity; if (!canStack) return;
if (!itemEntity.isPickable() || !itemEntity.isMergeable()) final int totalAmount = stackingRule.getAmount(itemStack) + stackingRule.getAmount(itemStackEntity);
continue; if (!stackingRule.canApply(itemStack, totalAmount)) return;
final ItemStack result = stackingRule.apply(itemStack, totalAmount);
// Too far, do not merge EntityItemMergeEvent entityItemMergeEvent = new EntityItemMergeEvent(this, itemEntity, result);
if (getDistance(itemEntity) > mergeRange) EventDispatcher.callCancellable(entityItemMergeEvent, () -> {
continue; setItemStack(entityItemMergeEvent.getResult());
itemEntity.remove();
final ItemStack itemStackEntity = itemEntity.getItemStack(); });
final StackingRule stackingRule = itemStack.getStackingRule();
final boolean canStack = stackingRule.canBeStacked(itemStack, itemStackEntity);
if (!canStack)
continue;
final int totalAmount = stackingRule.getAmount(itemStack) + stackingRule.getAmount(itemStackEntity);
final boolean canApply = stackingRule.canApply(itemStack, totalAmount);
if (!canApply)
continue;
final ItemStack result = stackingRule.apply(itemStack, totalAmount);
EntityItemMergeEvent entityItemMergeEvent = new EntityItemMergeEvent(this, itemEntity, result);
EventDispatcher.callCancellable(entityItemMergeEvent, () -> {
setItemStack(entityItemMergeEvent.getResult());
itemEntity.remove();
}); });
}
}
} }
} }

View File

@ -15,8 +15,7 @@ import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.entity.EntityFireEvent; import net.minestom.server.event.entity.EntityFireEvent;
import net.minestom.server.event.item.EntityEquipEvent; import net.minestom.server.event.item.EntityEquipEvent;
import net.minestom.server.event.item.PickupItemEvent; import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.instance.Chunk; import net.minestom.server.instance.EntityTracker;
import net.minestom.server.instance.block.Block;
import net.minestom.server.inventory.EquipmentHandler; import net.minestom.server.inventory.EquipmentHandler;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.network.ConnectionState; import net.minestom.server.network.ConnectionState;
@ -201,32 +200,21 @@ public class LivingEntity extends Entity implements EquipmentHandler {
// Items picking // Items picking
if (canPickupItem() && itemPickupCooldown.isReady(time)) { if (canPickupItem() && itemPickupCooldown.isReady(time)) {
itemPickupCooldown.refreshLastUpdate(time); itemPickupCooldown.refreshLastUpdate(time);
this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.getWidth(),
final Chunk chunk = getChunk(); // TODO check surrounding chunks EntityTracker.Target.ITEMS, itemEntity -> {
final Set<Entity> entities = instance.getChunkEntities(chunk); if (this instanceof Player player && !itemEntity.isViewer(player)) return;
for (Entity entity : entities) { if (!itemEntity.isPickable()) return;
if (entity instanceof ItemEntity) { final BoundingBox itemBoundingBox = itemEntity.getBoundingBox();
// Do not pick up if not visible if (expandedBoundingBox.intersect(itemBoundingBox)) {
if (this instanceof Player && !entity.isViewer((Player) this)) if (itemEntity.shouldRemove() || itemEntity.isRemoveScheduled()) return;
continue; PickupItemEvent pickupItemEvent = new PickupItemEvent(this, itemEntity);
EventDispatcher.callCancellable(pickupItemEvent, () -> {
final ItemEntity itemEntity = (ItemEntity) entity; final ItemStack item = itemEntity.getItemStack();
if (!itemEntity.isPickable()) sendPacketToViewersAndSelf(new CollectItemPacket(itemEntity.getEntityId(), getEntityId(), item.getAmount()));
continue; itemEntity.remove();
});
final BoundingBox itemBoundingBox = itemEntity.getBoundingBox(); }
if (expandedBoundingBox.intersect(itemBoundingBox)) { });
if (itemEntity.shouldRemove() || itemEntity.isRemoveScheduled())
continue;
PickupItemEvent pickupItemEvent = new PickupItemEvent(this, itemEntity);
EventDispatcher.callCancellable(pickupItemEvent, () -> {
final ItemStack item = itemEntity.getItemStack();
sendPacketToViewersAndSelf(new CollectItemPacket(itemEntity.getEntityId(), getEntityId(), item.getAmount()));
entity.remove();
});
}
}
}
} }
} }
@ -355,8 +343,7 @@ public class LivingEntity extends Entity implements EquipmentHandler {
sendPacketToViewersAndSelf(new EntityAnimationPacket(getEntityId(), EntityAnimationPacket.Animation.TAKE_DAMAGE)); sendPacketToViewersAndSelf(new EntityAnimationPacket(getEntityId(), EntityAnimationPacket.Animation.TAKE_DAMAGE));
// Additional hearts support // Additional hearts support
if (this instanceof Player) { if (this instanceof Player player) {
final Player player = (Player) this;
final float additionalHearts = player.getAdditionalHearts(); final float additionalHearts = player.getAdditionalHearts();
if (additionalHearts > 0) { if (additionalHearts > 0) {
if (remainingDamage > additionalHearts) { if (remainingDamage > additionalHearts) {
@ -477,8 +464,7 @@ public class LivingEntity extends Entity implements EquipmentHandler {
protected void onAttributeChanged(@NotNull AttributeInstance attributeInstance) { protected void onAttributeChanged(@NotNull AttributeInstance attributeInstance) {
if (attributeInstance.getAttribute().isShared()) { if (attributeInstance.getAttribute().isShared()) {
boolean self = false; boolean self = false;
if (this instanceof Player) { if (this instanceof Player player) {
Player player = (Player) this;
PlayerConnection playerConnection = player.playerConnection; PlayerConnection playerConnection = player.playerConnection;
// connection null during Player initialization (due to #super call) // connection null during Player initialization (due to #super call)
self = playerConnection != null && playerConnection.getConnectionState() == ConnectionState.PLAY; self = playerConnection != null && playerConnection.getConnectionState() == ConnectionState.PLAY;
@ -531,17 +517,11 @@ public class LivingEntity extends Entity implements EquipmentHandler {
} }
@Override @Override
protected boolean addViewer0(@NotNull Player player) { public void updateNewViewer(@NotNull Player player) {
if (!super.addViewer0(player)) { super.updateNewViewer(player);
return false; player.sendPacket(getEquipmentsPacket());
} player.sendPacket(getPropertiesPacket());
final PlayerConnection playerConnection = player.getPlayerConnection(); if (getTeam() != null) player.sendPacket(getTeam().createTeamsCreationPacket());
playerConnection.sendPacket(getEquipmentsPacket());
playerConnection.sendPacket(getPropertiesPacket());
if (getTeam() != null) {
playerConnection.sendPacket(getTeam().createTeamsCreationPacket());
}
return true;
} }
@Override @Override
@ -682,20 +662,10 @@ public class LivingEntity extends Entity implements EquipmentHandler {
*/ */
public void setTeam(Team team) { public void setTeam(Team team) {
if (this.team == team) return; if (this.team == team) return;
String member = this instanceof Player player ? player.getUsername() : uuid.toString();
String member;
if (this instanceof Player) {
Player player = (Player) this;
member = player.getUsername();
} else {
member = this.uuid.toString();
}
if (this.team != null) { if (this.team != null) {
this.team.removeMember(member); this.team.removeMember(member);
} }
this.team = team; this.team = team;
if (team != null) { if (team != null) {
team.addMember(member); team.addMember(member);
@ -711,46 +681,6 @@ public class LivingEntity extends Entity implements EquipmentHandler {
return team; return team;
} }
/**
* Gets the line of sight of the entity.
*
* @param maxDistance The max distance to scan
* @return A list of {@link Point poiints} in this entities line of sight
*/
public List<Point> getLineOfSight(int maxDistance) {
List<Point> blocks = new ArrayList<>();
Iterator<Point> it = new BlockIterator(this, maxDistance);
while (it.hasNext()) {
final Point position = it.next();
if (!getInstance().getBlock(position).isAir()) blocks.add(position);
}
return blocks;
}
/**
* Checks whether the current entity has line of sight to the given one.
* If so, it doesn't mean that the given entity is IN line of sight of the current,
* but the current one can rotate so that it will be true.
*
* @param entity the entity to be checked.
* @return if the current entity has line of sight to the given one.
*/
public boolean hasLineOfSight(Entity entity) {
final var start = getPosition().asVec().add(0D, getEyeHeight(), 0D);
final var end = entity.getPosition().asVec().add(0D, getEyeHeight(), 0D);
final var direction = end.sub(start);
final int maxDistance = (int) Math.ceil(direction.length());
Iterator<Point> it = new BlockIterator(start, direction.normalize(), 0D, maxDistance);
while (it.hasNext()) {
Block block = getInstance().getBlock(it.next());
if (!block.isAir() && !block.isLiquid()) {
return false;
}
}
return true;
}
/** /**
* Gets the target (not-air) block position of the entity. * Gets the target (not-air) block position of the entity.
* *

View File

@ -291,49 +291,28 @@ public class Metadata {
} }
private static <T> Value<T> getCorrespondingNewEmptyValue(int type) { private static <T> Value<T> getCorrespondingNewEmptyValue(int type) {
switch (type) { return switch (type) {
case TYPE_BYTE: case TYPE_BYTE -> (Value<T>) Byte((byte) 0);
return (Value<T>) Byte((byte) 0); case TYPE_VARINT -> (Value<T>) VarInt(0);
case TYPE_VARINT: case TYPE_FLOAT -> (Value<T>) Float(0);
return (Value<T>) VarInt(0); case TYPE_STRING -> (Value<T>) String("");
case TYPE_FLOAT: case TYPE_CHAT -> (Value<T>) Chat(Component.empty());
return (Value<T>) Float(0); case TYPE_OPTCHAT -> (Value<T>) OptChat(null);
case TYPE_STRING: case TYPE_SLOT -> (Value<T>) Slot(ItemStack.AIR);
return (Value<T>) String(""); case TYPE_BOOLEAN -> (Value<T>) Boolean(false);
case TYPE_CHAT: case TYPE_ROTATION -> (Value<T>) Rotation(Vec.ZERO);
return (Value<T>) Chat(Component.empty()); case TYPE_POSITION -> (Value<T>) Position(Vec.ZERO);
case TYPE_OPTCHAT: case TYPE_OPTPOSITION -> (Value<T>) OptPosition(null);
return (Value<T>) OptChat(null); case TYPE_DIRECTION -> (Value<T>) Direction(Direction.DOWN);
case TYPE_SLOT: case TYPE_OPTUUID -> (Value<T>) OptUUID(null);
return (Value<T>) Slot(ItemStack.AIR); case TYPE_OPTBLOCKID -> (Value<T>) OptBlockID(null);
case TYPE_BOOLEAN: case TYPE_NBT -> (Value<T>) NBT(new NBTEnd());
return (Value<T>) Boolean(false); case TYPE_PARTICLE -> throw new UnsupportedOperationException();
case TYPE_ROTATION: case TYPE_VILLAGERDATA -> (Value<T>) VillagerData(0, 0, 0);
return (Value<T>) Rotation(Vec.ZERO); case TYPE_OPTVARINT -> (Value<T>) OptVarInt(null);
case TYPE_POSITION: case TYPE_POSE -> (Value<T>) Pose(Entity.Pose.STANDING);
return (Value<T>) Position(Vec.ZERO); default -> throw new UnsupportedOperationException();
case TYPE_OPTPOSITION: };
return (Value<T>) OptPosition(null);
case TYPE_DIRECTION:
return (Value<T>) Direction(Direction.DOWN);
case TYPE_OPTUUID:
return (Value<T>) OptUUID(null);
case TYPE_OPTBLOCKID:
return (Value<T>) OptBlockID(null);
case TYPE_NBT:
return (Value<T>) NBT(new NBTEnd());
case TYPE_PARTICLE:
throw new UnsupportedOperationException();
case TYPE_VILLAGERDATA:
return (Value<T>) VillagerData(0, 0, 0);
case TYPE_OPTVARINT:
return (Value<T>) OptVarInt(null);
case TYPE_POSE:
return (Value<T>) Pose(Entity.Pose.STANDING);
default:
throw new UnsupportedOperationException();
}
} }
private static <T> Value<T> read(int type, BinaryReader reader) { private static <T> Value<T> read(int type, BinaryReader reader) {

View File

@ -12,8 +12,8 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.event.HoverEvent.ShowEntity; import net.kyori.adventure.text.event.HoverEvent.ShowEntity;
import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title; import net.kyori.adventure.title.TitlePart;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.advancements.AdvancementTab;
import net.minestom.server.adventure.AdventurePacketConvertor; import net.minestom.server.adventure.AdventurePacketConvertor;
@ -32,12 +32,14 @@ import net.minestom.server.entity.fakeplayer.FakePlayer;
import net.minestom.server.entity.metadata.PlayerMeta; import net.minestom.server.entity.metadata.PlayerMeta;
import net.minestom.server.entity.vehicle.PlayerVehicleInformation; import net.minestom.server.entity.vehicle.PlayerVehicleInformation;
import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.GlobalHandles;
import net.minestom.server.event.inventory.InventoryOpenEvent; import net.minestom.server.event.inventory.InventoryOpenEvent;
import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.ItemUpdateStateEvent; import net.minestom.server.event.item.ItemUpdateStateEvent;
import net.minestom.server.event.item.PickupExperienceEvent; import net.minestom.server.event.item.PickupExperienceEvent;
import net.minestom.server.event.player.*; import net.minestom.server.event.player.*;
import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.EntityTracker;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.inventory.PlayerInventory;
@ -50,6 +52,7 @@ import net.minestom.server.message.Messenger;
import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState; import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.PlayerProvider;
import net.minestom.server.network.packet.FramedPacket;
import net.minestom.server.network.packet.client.ClientPlayPacket; import net.minestom.server.network.packet.client.ClientPlayPacket;
import net.minestom.server.network.packet.client.play.ClientChatMessagePacket; import net.minestom.server.network.packet.client.play.ClientChatMessagePacket;
import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacket;
@ -63,13 +66,11 @@ import net.minestom.server.resourcepack.ResourcePack;
import net.minestom.server.scoreboard.BelowNameTag; import net.minestom.server.scoreboard.BelowNameTag;
import net.minestom.server.scoreboard.Team; import net.minestom.server.scoreboard.Team;
import net.minestom.server.statistic.PlayerStatistic; import net.minestom.server.statistic.PlayerStatistic;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.TickUtils;
import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.function.IntegerBiConsumer;
import net.minestom.server.utils.identity.NamedAndIdentified; import net.minestom.server.utils.identity.NamedAndIdentified;
import net.minestom.server.utils.instance.InstanceUtils; import net.minestom.server.utils.instance.InstanceUtils;
import net.minestom.server.utils.inventory.PlayerInventoryUtils; import net.minestom.server.utils.inventory.PlayerInventoryUtils;
@ -85,9 +86,9 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
/** /**
@ -98,14 +99,14 @@ import java.util.function.UnaryOperator;
*/ */
public class Player extends LivingEntity implements CommandSender, Localizable, HoverEventSource<ShowEntity>, Identified, NamedAndIdentified { public class Player extends LivingEntity implements CommandSender, Localizable, HoverEventSource<ShowEntity>, Identified, NamedAndIdentified {
private static final Component REMOVE_MESSAGE = Component.text("You have been removed from the server without reason.", NamedTextColor.RED);
private long lastKeepAlive; private long lastKeepAlive;
private boolean answerKeepAlive; private boolean answerKeepAlive;
private String username; private String username;
private Component usernameComponent; private Component usernameComponent;
protected final PlayerConnection playerConnection; protected final PlayerConnection playerConnection;
// All the entities that this player can see
protected final Set<Entity> viewableEntities = ConcurrentHashMap.newKeySet();
private int latency; private int latency;
private Component displayName; private Component displayName;
@ -113,8 +114,29 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
private DimensionType dimensionType; private DimensionType dimensionType;
private GameMode gameMode; private GameMode gameMode;
// Chunks that the player can view final IntegerBiConsumer chunkAdder = (chunkX, chunkZ) -> {
protected final Set<Chunk> viewableChunks = ConcurrentHashMap.newKeySet(); // Load new chunks
this.instance.loadOptionalChunk(chunkX, chunkZ).thenAccept(chunk -> {
try {
if (chunk != null) {
chunk.sendChunk(this);
GlobalHandles.PLAYER_CHUNK_LOAD.call(new PlayerChunkLoadEvent(this, chunkX, chunkZ));
}
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
}
});
};
final IntegerBiConsumer chunkRemover = (chunkX, chunkZ) -> {
// Unload old chunks
final Instance instance = this.instance;
if (instance == null) return;
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk != null) {
sendPacket(new UnloadChunkPacket(chunkX, chunkZ));
GlobalHandles.PLAYER_CHUNK_UNLOAD.call(new PlayerChunkUnloadEvent(this, chunkX, chunkZ));
}
};
private final AtomicInteger teleportId = new AtomicInteger(); private final AtomicInteger teleportId = new AtomicInteger();
private int receivedTeleportId; private int receivedTeleportId;
@ -166,9 +188,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Vehicle // Vehicle
private final PlayerVehicleInformation vehicleInformation = new PlayerVehicleInformation(); private final PlayerVehicleInformation vehicleInformation = new PlayerVehicleInformation();
// Tick related
private final PlayerTickEvent playerTickEvent = new PlayerTickEvent(this);
// Adventure // Adventure
private Identity identity; private Identity identity;
private final Pointers pointers; private final Pointers pointers;
@ -276,7 +295,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Recipes end // Recipes end
// Tags // Tags
this.playerConnection.sendPacket(TagsPacket.getRequiredTagsPacket()); this.playerConnection.sendPacket(TagsPacket.DEFAULT_TAGS);
// Some client updates // Some client updates
this.playerConnection.sendPacket(getPropertiesPacket()); // Send default properties this.playerConnection.sendPacket(getPropertiesPacket()); // Send default properties
@ -309,23 +328,19 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Experience orb pickup // Experience orb pickup
if (experiencePickupCooldown.isReady(time)) { if (experiencePickupCooldown.isReady(time)) {
experiencePickupCooldown.refreshLastUpdate(time); experiencePickupCooldown.refreshLastUpdate(time);
final Chunk chunk = getChunk(); // TODO check surrounding chunks this.instance.getEntityTracker().nearbyEntities(position, expandedBoundingBox.getWidth(),
final Set<Entity> entities = instance.getChunkEntities(chunk); EntityTracker.Target.EXPERIENCE_ORBS, experienceOrb -> {
for (Entity entity : entities) { final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox();
if (entity instanceof ExperienceOrb) { if (expandedBoundingBox.intersect(itemBoundingBox)) {
final ExperienceOrb experienceOrb = (ExperienceOrb) entity; if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled())
final BoundingBox itemBoundingBox = experienceOrb.getBoundingBox(); return;
if (expandedBoundingBox.intersect(itemBoundingBox)) { PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(this, experienceOrb);
if (experienceOrb.shouldRemove() || experienceOrb.isRemoveScheduled()) EventDispatcher.callCancellable(pickupExperienceEvent, () -> {
continue; short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player
PickupExperienceEvent pickupExperienceEvent = new PickupExperienceEvent(experienceOrb); experienceOrb.remove();
EventDispatcher.callCancellable(pickupExperienceEvent, () -> { });
short experienceCount = pickupExperienceEvent.getExperienceCount(); // TODO give to player }
entity.remove(); });
});
}
}
}
} }
// Eating animation // Eating animation
@ -353,7 +368,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} }
// Tick event // Tick event
EventDispatcher.call(playerTickEvent); GlobalHandles.PLAYER_TICK.call(new PlayerTickEvent(this));
} }
@Override @Override
@ -420,6 +435,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
getPlayerConnection().sendPacket(respawnPacket); getPlayerConnection().sendPacket(respawnPacket);
PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this); PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this);
EventDispatcher.call(respawnEvent); EventDispatcher.call(respawnEvent);
triggerStatus((byte) (24 + permissionLevel)); // Set permission level
refreshIsDead(false); refreshIsDead(false);
// Runnable called when teleportation is successful (after loading and sending necessary chunk) // Runnable called when teleportation is successful (after loading and sending necessary chunk)
@ -448,9 +464,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
EventDispatcher.call(new PlayerDisconnectEvent(this)); EventDispatcher.call(new PlayerDisconnectEvent(this));
super.remove(); super.remove();
this.packets.clear(); this.packets.clear();
if (getOpenInventory() != null) { final Inventory currentInventory = getOpenInventory();
getOpenInventory().removeViewer(this); if (currentInventory != null) currentInventory.removeViewer(this);
}
MinecraftServer.getBossBarManager().removeAllBossBars(this); MinecraftServer.getBossBarManager().removeAllBossBars(this);
// Advancement tabs cache // Advancement tabs cache
{ {
@ -461,24 +476,29 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} }
} }
} }
final Pos position = this.position;
final int chunkX = position.chunkX();
final int chunkZ = position.chunkZ();
// Clear all viewable entities // Clear all viewable entities
this.viewableEntities.forEach(entity -> entity.removeViewer(this)); this.instance.getEntityTracker().visibleEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES,
trackingUpdate::remove);
// Clear all viewable chunks // Clear all viewable chunks
this.viewableChunks.forEach(chunk -> chunk.removeViewer(this)); ChunkUtils.forChunksInRange(chunkX, chunkZ, MinecraftServer.getChunkViewDistance(), chunkRemover);
// Remove from the tab-list // Remove from the tab-list
PacketUtils.broadcastPacket(getRemovePlayerToList()); PacketUtils.broadcastPacket(getRemovePlayerToList());
// Prevent the player from being stuck in loading screen, or just unable to interact with the server
// This should be considered as a bug, since the player will ultimately time out anyway.
if (playerConnection.isOnline()) kick(REMOVE_MESSAGE);
} }
@Override @Override
protected boolean removeViewer0(@NotNull Player player) { public void updateOldViewer(@NotNull Player player) {
if (player == this || !super.removeViewer0(player)) { super.updateOldViewer(player);
return false;
}
// Team // Team
if (this.getTeam() != null && this.getTeam().getMembers().size() == 1) {// If team only contains "this" player if (this.getTeam() != null && this.getTeam().getMembers().size() == 1) {// If team only contains "this" player
player.getPlayerConnection().sendPacket(this.getTeam().createTeamDestructionPacket()); player.sendPacket(this.getTeam().createTeamDestructionPacket());
} }
return true;
} }
@Override @Override
@ -487,6 +507,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
super.sendPacketToViewersAndSelf(packet); super.sendPacketToViewersAndSelf(packet);
} }
@Override
public void sendPacketToViewersAndSelf(@NotNull FramedPacket framedPacket) {
this.playerConnection.sendPacket(framedPacket);
super.sendPacketToViewersAndSelf(framedPacket);
}
/** /**
* Changes the player instance and load surrounding chunks if needed. * Changes the player instance and load surrounding chunks if needed.
* <p> * <p>
@ -501,18 +527,37 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
final Instance currentInstance = this.instance; final Instance currentInstance = this.instance;
Check.argCondition(currentInstance == instance, "Instance should be different than the current one"); Check.argCondition(currentInstance == 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 (e.g. SharedInstance) if (InstanceUtils.areLinked(currentInstance, instance) && spawnPosition.sameChunk(this.position)) {
if (!InstanceUtils.areLinked(currentInstance, instance) || !spawnPosition.sameChunk(this.position)) {
final boolean firstSpawn = currentInstance == null;
return instance.loadOptionalChunk(spawnPosition)
.thenRun(() -> spawnPlayer(instance, spawnPosition, firstSpawn,
!Objects.equals(dimensionType, instance.getDimensionType()), true));
} else {
// The player already has the good version of all the chunks. // 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 // We just need to refresh his entity viewing list and add him to the instance
return AsyncUtils.VOID_FUTURE spawnPlayer(instance, spawnPosition, null, false, false, false);
.thenRun(() -> spawnPlayer(instance, spawnPosition, false, false, false)); return AsyncUtils.VOID_FUTURE;
} }
// Must update the player chunks
final boolean dimensionChange = !Objects.equals(dimensionType, instance.getDimensionType());
final Thread runThread = Thread.currentThread();
final Consumer<Instance> runnable = (i) -> spawnPlayer(i, spawnPosition,
currentInstance,
currentInstance == null, dimensionChange, true);
// Wait for all surrounding chunks to load
List<CompletableFuture<Chunk>> futures = new ArrayList<>();
ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(),
(chunkX, chunkZ) -> futures.add(instance.loadOptionalChunk(chunkX, chunkZ)));
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.thenCompose(unused -> {
if (runThread == Thread.currentThread()) {
runnable.accept(instance);
return AsyncUtils.VOID_FUTURE;
} else {
// Complete the future during the next instance tick
CompletableFuture<Void> future = new CompletableFuture<>();
instance.scheduleNextTick(i -> {
runnable.accept(i);
future.complete(null);
});
return future;
}
});
} }
/** /**
@ -536,42 +581,42 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* <p> * <p>
* UNSAFE: only called with {@link #setInstance(Instance, Pos)}. * UNSAFE: only called with {@link #setInstance(Instance, Pos)}.
* *
* @param spawnPosition the position to teleport the player * @param spawnPosition the position to teleport the player
* @param firstSpawn true if this is the player first spawn * @param previousInstance the previous player instance, null if first spawn
* @param updateChunks true if chunks should be refreshed, false if the new instance shares the same * @param firstSpawn true if this is the player first spawn
* chunks * @param updateChunks true if chunks should be refreshed, false if the new instance shares the same
* chunks
*/ */
private void spawnPlayer(@NotNull Instance instance, @NotNull Pos spawnPosition, private void spawnPlayer(@NotNull Instance instance, @NotNull Pos spawnPosition,
@Nullable Instance previousInstance,
boolean firstSpawn, boolean dimensionChange, boolean updateChunks) { boolean firstSpawn, boolean dimensionChange, boolean updateChunks) {
final Set<Chunk> previousChunks = Set.copyOf(viewableChunks);
if (!firstSpawn) { if (!firstSpawn) {
// Player instance changed, clear current viewable collections // Player instance changed, clear current viewable collections
previousChunks.forEach(chunk -> chunk.removeViewer(this)); if (updateChunks)
this.viewableEntities.forEach(entity -> entity.removeViewer(this)); ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(), chunkRemover);
} }
if (previousInstance != null) {
previousInstance.getEntityTracker().visibleEntities(position,
EntityTracker.Target.ENTITIES, trackingUpdate::remove);
}
if (dimensionChange) sendDimension(instance.getDimensionType());
super.setInstance(instance, spawnPosition); super.setInstance(instance, spawnPosition);
if (dimensionChange) { if (updateChunks) {
sendDimension(instance.getDimensionType()); sendPacket(new UpdateViewPositionPacket(spawnPosition.chunkX(), spawnPosition.chunkZ()));
ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(), chunkAdder);
} }
if (updateChunks) { synchronizePosition(true); // So the player doesn't get stuck
// Warning: loop to remove once `refreshVisibleChunks` manage it
previousChunks.forEach(chunk ->
playerConnection.sendPacket(new UnloadChunkPacket(chunk.getChunkX(), chunk.getChunkZ())));
refreshVisibleChunks();
}
if (dimensionChange || firstSpawn) { if (dimensionChange || firstSpawn) {
synchronizePosition(true); // So the player doesn't get stuck
this.inventory.update(); this.inventory.update();
} }
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn); EventDispatcher.call(new PlayerSpawnEvent(this, instance, firstSpawn));
EventDispatcher.call(spawnEvent);
this.playerConnection.flush();
} }
/** /**
@ -596,14 +641,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
sendPluginMessage(channel, message.getBytes(StandardCharsets.UTF_8)); sendPluginMessage(channel, message.getBytes(StandardCharsets.UTF_8));
} }
/**
* @deprecated Use {@link #sendMessage(Component)}
*/
@Deprecated
public void sendJsonMessage(@NotNull String json) {
this.sendMessage(GsonComponentSerializer.gson().deserialize(json));
}
@Override @Override
public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) { public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) {
Messenger.sendMessage(this, message, ChatPosition.fromMessageType(type), source.uuid()); Messenger.sendMessage(this, message, ChatPosition.fromMessageType(type), source.uuid());
@ -673,16 +710,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} }
@Override @Override
public void showTitle(@NotNull Title title) { public <T> void sendTitlePart(@NotNull TitlePart<T> part, @NotNull T value) {
playerConnection.sendPacket(new SetTitleTextPacket(title.title())); playerConnection.sendPacket(AdventurePacketConvertor.createTitlePartPacket(part, value));
playerConnection.sendPacket(new SetTitleSubTitlePacket(title.subtitle()));
final var times = title.times();
if (times != null) {
playerConnection.sendPacket(new SetTitleTimePacket(
TickUtils.fromDuration(times.fadeIn(), TickUtils.CLIENT_TICK_MS),
TickUtils.fromDuration(times.stay(), TickUtils.CLIENT_TICK_MS),
TickUtils.fromDuration(times.fadeOut(), TickUtils.CLIENT_TICK_MS)));
}
} }
@Override @Override
@ -690,20 +719,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
playerConnection.sendPacket(new ActionBarPacket(message)); playerConnection.sendPacket(new ActionBarPacket(message));
} }
/**
* Specifies the display time of a title.
*
* @param fadeIn ticks to spend fading in
* @param stay ticks to keep the title displayed
* @param fadeOut ticks to spend out, not when to start fading out
* @deprecated Use {@link #showTitle(Title)}. Note that this will overwrite the
* existing title. This is expected behavior and will be the case in 1.17.
*/
@Deprecated
public void sendTitleTime(int fadeIn, int stay, int fadeOut) {
playerConnection.sendPacket(new SetTitleTimePacket(fadeIn, stay, fadeOut));
}
@Override @Override
public void resetTitle() { public void resetTitle() {
playerConnection.sendPacket(new ClearTitlesPacket(true)); playerConnection.sendPacket(new ClearTitlesPacket(true));
@ -1099,13 +1114,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
sendPacketToViewersAndSelf(getEquipmentsPacket()); sendPacketToViewersAndSelf(getEquipmentsPacket());
getInventory().update(); getInventory().update();
{
// Send new chunks
final Chunk chunk = instance.getChunkAt(position);
Check.notNull(chunk, "Tried to interact with an unloaded chunk.");
refreshVisibleChunks(chunk);
}
} }
/** /**
@ -1160,87 +1168,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
this.playerConnection.sendPacket(new SetExperiencePacket(exp, level, 0)); this.playerConnection.sendPacket(new SetExperiencePacket(exp, level, 0));
} }
/**
* Called when the player changes chunk (move from one to another).
* Can also be used to refresh the list of chunks that the client should see based on {@link #getChunkRange()}.
* <p>
* It does remove and add the player from the chunks viewers list when removed or added.
* It also calls the events {@link PlayerChunkUnloadEvent} and {@link PlayerChunkLoadEvent}.
*
* @param newChunk the current/new player chunk (can be the current one)
*/
public void refreshVisibleChunks(@NotNull Chunk newChunk) {
// Previous chunks indexes
final long[] lastVisibleChunks = viewableChunks.stream().mapToLong(ChunkUtils::getChunkIndex).toArray();
// New chunks indexes
final long[] updatedVisibleChunks = ChunkUtils.getChunksInRange(newChunk.toPosition(), getChunkRange());
// Update client render distance
updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ());
// Unload old chunks
ArrayUtils.forDifferencesBetweenArray(lastVisibleChunks, updatedVisibleChunks, chunkIndex -> {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
//playerConnection.sendPacket(new UnloadChunkPacket(chunkX, chunkZ));
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk != null) {
chunk.removeViewer(this);
}
});
// Load new chunks
ArrayUtils.forDifferencesBetweenArray(updatedVisibleChunks, lastVisibleChunks, chunkIndex -> {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
this.instance.loadOptionalChunk(chunkX, chunkZ).thenAccept(chunk -> {
if (chunk == null) {
// Cannot load chunk (auto load is not enabled)
return;
}
chunk.addViewer(this);
});
});
}
public void refreshVisibleChunks() {
final Chunk chunk = getChunk();
if (chunk != null) {
refreshVisibleChunks(chunk);
}
}
/**
* Refreshes the list of entities that the player should be able to see based
* on {@link MinecraftServer#getEntityViewDistance()} and {@link Entity#isAutoViewable()}.
*
* @param newChunk the new chunk of the player (can be the current one)
*/
public void refreshVisibleEntities(@NotNull Chunk newChunk) {
final int entityViewDistance = MinecraftServer.getEntityViewDistance();
final float maximalDistance = entityViewDistance * Chunk.CHUNK_SECTION_SIZE;
// Manage already viewable entities
this.viewableEntities.stream()
.filter(entity -> entity.getDistance(this) > maximalDistance)
.forEach(entity -> {
// Entity shouldn't be viewable anymore
if (isAutoViewable()) {
entity.removeViewer(this);
}
if (entity instanceof Player && entity.isAutoViewable()) {
removeViewer((Player) entity);
}
});
// Manage entities in unchecked chunks
EntityUtils.forEachRange(instance, newChunk.toPosition(), entityViewDistance, entity -> {
if (entity.isAutoViewable() && !entity.viewers.contains(this)) {
entity.addViewer(this);
}
if (entity instanceof Player && isAutoViewable() && !viewers.contains(entity)) {
addViewer((Player) entity);
}
});
}
/** /**
* Gets the player connection. * Gets the player connection.
* <p> * <p>
@ -1248,11 +1175,25 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* *
* @return the player connection * @return the player connection
*/ */
@NotNull public @NotNull PlayerConnection getPlayerConnection() {
public PlayerConnection getPlayerConnection() {
return playerConnection; return playerConnection;
} }
/**
* Shortcut for {@link PlayerConnection#sendPacket(ServerPacket)}.
*
* @param packet the packet to send
*/
@ApiStatus.Experimental
public void sendPacket(@NotNull ServerPacket packet) {
this.playerConnection.sendPacket(packet);
}
@ApiStatus.Experimental
public void sendPacket(@NotNull FramedPacket framedPacket) {
this.playerConnection.sendPacket(framedPacket);
}
/** /**
* Gets if the player is online or not. * Gets if the player is online or not.
* *
@ -1267,8 +1208,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* *
* @return the player settings * @return the player settings
*/ */
@NotNull public @NotNull PlayerSettings getSettings() {
public PlayerSettings getSettings() {
return settings; return settings;
} }
@ -1281,8 +1221,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
return dimensionType; return dimensionType;
} }
@NotNull public @NotNull PlayerInventory getInventory() {
public PlayerInventory getInventory() {
return inventory; return inventory;
} }
@ -1429,8 +1368,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* *
* @return the currently open inventory, null if there is not (player inventory is not detected) * @return the currently open inventory, null if there is not (player inventory is not detected)
*/ */
@Nullable public @Nullable Inventory getOpenInventory() {
public Inventory getOpenInventory() {
return openInventory; return openInventory;
} }
@ -1441,7 +1379,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* @return true if the inventory has been opened/sent to the player, false otherwise (cancelled by event) * @return true if the inventory has been opened/sent to the player, false otherwise (cancelled by event)
*/ */
public boolean openInventory(@NotNull Inventory inventory) { public boolean openInventory(@NotNull Inventory inventory) {
InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this); InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this);
EventDispatcher.callCancellable(inventoryOpenEvent, () -> { EventDispatcher.callCancellable(inventoryOpenEvent, () -> {
@ -1451,7 +1388,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} }
Inventory newInventory = inventoryOpenEvent.getInventory(); Inventory newInventory = inventoryOpenEvent.getInventory();
if (newInventory == null) { if (newInventory == null) {
// just close the inventory // just close the inventory
return; return;
@ -1463,9 +1399,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
playerConnection.sendPacket(openWindowPacket); playerConnection.sendPacket(openWindowPacket);
newInventory.addViewer(this); newInventory.addViewer(this);
this.openInventory = newInventory; this.openInventory = newInventory;
}); });
return !inventoryOpenEvent.isCancelled(); return !inventoryOpenEvent.isCancelled();
} }
@ -1528,26 +1462,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
this.didCloseInventory = didCloseInventory; this.didCloseInventory = didCloseInventory;
} }
/** public int getNextTeleportId() {
* Gets the player viewable chunks. return teleportId.incrementAndGet();
* <p>
* WARNING: adding or removing a chunk there will not load/unload it,
* use {@link Chunk#addViewer(Player)} or {@link Chunk#removeViewer(Player)}.
*
* @return a {@link Set} containing all the chunks that the player sees
*/
public Set<Chunk> getViewableChunks() {
return viewableChunks;
}
/**
* Sends a {@link UpdateViewPositionPacket} to the player.
*
* @param chunkX the chunk X
* @param chunkZ the chunk Z
*/
public void updateViewPosition(int chunkX, int chunkZ) {
playerConnection.sendPacket(new UpdateViewPositionPacket(chunkX, chunkZ));
} }
public int getLastSentTeleportId() { public int getLastSentTeleportId() {
@ -1569,7 +1485,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
@ApiStatus.Internal @ApiStatus.Internal
protected void synchronizePosition(boolean includeSelf) { protected void synchronizePosition(boolean includeSelf) {
if (includeSelf) { if (includeSelf) {
playerConnection.sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, teleportId.incrementAndGet(), false)); playerConnection.sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, getNextTeleportId(), false));
} }
super.synchronizePosition(includeSelf); super.synchronizePosition(includeSelf);
} }
@ -1643,6 +1559,15 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
refreshAbilities(); refreshAbilities();
} }
@Override
public void setSneaking(boolean sneaking) {
if (isFlying()) { //If we are flying, don't set the players pose to sneaking as this can clip them through blocks
this.entityMeta.setSneaking(sneaking);
} else {
super.setSneaking(sneaking);
}
}
/** /**
* Gets if the player is currently flying. * Gets if the player is currently flying.
* *
@ -1658,7 +1583,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* @param flying should the player fly * @param flying should the player fly
*/ */
public void setFlying(boolean flying) { public void setFlying(boolean flying) {
this.flying = flying; refreshFlying(flying);
refreshAbilities(); refreshAbilities();
} }
@ -1671,6 +1596,17 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* @see #setFlying(boolean) instead * @see #setFlying(boolean) instead
*/ */
public void refreshFlying(boolean flying) { public void refreshFlying(boolean flying) {
//When the player starts or stops flying, their pose needs to change
if (this.flying != flying) {
Pose pose = getPose();
if (this.isSneaking() && pose == Pose.STANDING) {
setPose(Pose.SNEAKING);
} else if (pose == Pose.SNEAKING) {
setPose(Pose.STANDING);
}
}
this.flying = flying; this.flying = flying;
} }
@ -1892,15 +1828,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
this.vehicleInformation.refresh(sideways, forward, jump, unmount); this.vehicleInformation.refresh(sideways, forward, jump, unmount);
} }
/**
* @return the chunk range of the viewers,
* which is {@link MinecraftServer#getChunkViewDistance()} or {@link PlayerSettings#getViewDistance()}
* based on which one is the lowest
*/
public int getChunkRange() {
return Math.min(getSettings().viewDistance, MinecraftServer.getChunkViewDistance());
}
/** /**
* Gets the last sent keep alive id. * Gets the last sent keep alive id.
* *
@ -1920,8 +1847,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* *
* @return a {@link PlayerInfoPacket} to add the player * @return a {@link PlayerInfoPacket} to add the player
*/ */
@NotNull protected @NotNull PlayerInfoPacket getAddPlayerToList() {
protected PlayerInfoPacket getAddPlayerToList() {
PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.ADD_PLAYER); PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.ADD_PLAYER);
PlayerInfoPacket.AddPlayer addPlayer = PlayerInfoPacket.AddPlayer addPlayer =
@ -1930,11 +1856,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Skin support // Skin support
if (skin != null) { if (skin != null) {
final String textures = skin.getTextures();
final String signature = skin.getSignature();
PlayerInfoPacket.AddPlayer.Property prop = PlayerInfoPacket.AddPlayer.Property prop =
new PlayerInfoPacket.AddPlayer.Property("textures", textures, signature); new PlayerInfoPacket.AddPlayer.Property("textures", skin.textures(), skin.signature());
addPlayer.properties.add(prop); addPlayer.properties.add(prop);
} }
@ -1947,14 +1870,9 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* *
* @return a {@link PlayerInfoPacket} to remove the player * @return a {@link PlayerInfoPacket} to remove the player
*/ */
@NotNull protected @NotNull PlayerInfoPacket getRemovePlayerToList() {
protected PlayerInfoPacket getRemovePlayerToList() {
PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER); PlayerInfoPacket playerInfoPacket = new PlayerInfoPacket(PlayerInfoPacket.Action.REMOVE_PLAYER);
playerInfoPacket.playerInfos.add(new PlayerInfoPacket.RemovePlayer(getUuid()));
PlayerInfoPacket.RemovePlayer removePlayer =
new PlayerInfoPacket.RemovePlayer(getUuid());
playerInfoPacket.playerInfos.add(removePlayer);
return playerInfoPacket; return playerInfoPacket;
} }
@ -1982,9 +1900,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
connection.sendPacket(new EntityHeadLookPacket(getEntityId(), position.yaw())); connection.sendPacket(new EntityHeadLookPacket(getEntityId(), position.yaw()));
} }
@NotNull
@Override @Override
public ItemStack getItemInMainHand() { public @NotNull ItemStack getItemInMainHand() {
return inventory.getItemInMainHand(); return inventory.getItemInMainHand();
} }
@ -1993,9 +1910,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
inventory.setItemInMainHand(itemStack); inventory.setItemInMainHand(itemStack);
} }
@NotNull
@Override @Override
public ItemStack getItemInOffHand() { public @NotNull ItemStack getItemInOffHand() {
return inventory.getItemInOffHand(); return inventory.getItemInOffHand();
} }
@ -2004,9 +1920,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
inventory.setItemInOffHand(itemStack); inventory.setItemInOffHand(itemStack);
} }
@NotNull
@Override @Override
public ItemStack getHelmet() { public @NotNull ItemStack getHelmet() {
return inventory.getHelmet(); return inventory.getHelmet();
} }
@ -2015,9 +1930,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
inventory.setHelmet(itemStack); inventory.setHelmet(itemStack);
} }
@NotNull
@Override @Override
public ItemStack getChestplate() { public @NotNull ItemStack getChestplate() {
return inventory.getChestplate(); return inventory.getChestplate();
} }
@ -2026,9 +1940,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
inventory.setChestplate(itemStack); inventory.setChestplate(itemStack);
} }
@NotNull
@Override @Override
public ItemStack getLeggings() { public @NotNull ItemStack getLeggings() {
return inventory.getLeggings(); return inventory.getLeggings();
} }
@ -2037,9 +1950,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
inventory.setLeggings(itemStack); inventory.setLeggings(itemStack);
} }
@NotNull
@Override @Override
public ItemStack getBoots() { public @NotNull ItemStack getBoots() {
return inventory.getBoots(); return inventory.getBoots();
} }
@ -2050,7 +1962,9 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
@Override @Override
public Locale getLocale() { public Locale getLocale() {
return settings.locale == null ? null : Locale.forLanguageTag(settings.locale); final String locale = settings.locale;
if (locale == null) return null;
return Locale.forLanguageTag(locale.replace("_", "-"));
} }
/** /**
@ -2114,16 +2028,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
RIGHT RIGHT
} }
/**
* @deprecated See {@link ChatMessageType}
*/
@Deprecated
public enum ChatMode {
ENABLED,
COMMANDS_ONLY,
HIDDEN
}
public class PlayerSettings { public class PlayerSettings {
private String locale; private String locale;
@ -2155,17 +2059,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
return viewDistance; return viewDistance;
} }
/**
* Gets the player chat mode.
*
* @return the player chat mode
* @deprecated Use {@link #getChatMessageType()}
*/
@Deprecated
public ChatMode getChatMode() {
return ChatMode.values()[chatMessageType.ordinal()];
}
/** /**
* Gets the messages this player wants to receive. * Gets the messages this player wants to receive.
* *
@ -2211,9 +2104,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
*/ */
public void refresh(String locale, byte viewDistance, ChatMessageType chatMessageType, boolean chatColors, public void refresh(String locale, byte viewDistance, ChatMessageType chatMessageType, boolean chatColors,
byte displayedSkinParts, MainHand mainHand) { byte displayedSkinParts, MainHand mainHand) {
final boolean viewDistanceChanged = this.viewDistance != viewDistance;
this.locale = locale; this.locale = locale;
this.viewDistance = viewDistance; this.viewDistance = viewDistance;
this.chatMessageType = chatMessageType; this.chatMessageType = chatMessageType;
@ -2223,11 +2113,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// TODO: Use the metadata object here // TODO: Use the metadata object here
metadata.setIndex((byte) 17, Metadata.Byte(displayedSkinParts)); metadata.setIndex((byte) 17, Metadata.Byte(displayedSkinParts));
// Client changed his view distance in the settings
if (viewDistanceChanged) {
refreshVisibleChunks();
}
} }
} }

View File

@ -4,26 +4,17 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import net.minestom.server.utils.mojang.MojangUtils; import net.minestom.server.utils.mojang.MojangUtils;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/** /**
* Contains all the data required to store a skin. * Contains all the data required to store a skin.
* <p> * <p>
* Can be applied to a player with {@link Player#setSkin(PlayerSkin)} * Can be applied to a player with {@link Player#setSkin(PlayerSkin)}
* or in the linked event {@link net.minestom.server.event.player.PlayerSkinInitEvent}. * or in the linked event {@link net.minestom.server.event.player.PlayerSkinInitEvent}.
*/ */
public class PlayerSkin { public record PlayerSkin(String textures, String signature) {
private final String textures;
private final String signature;
public PlayerSkin(String textures, String signature) {
this.textures = textures;
this.signature = signature;
}
/** /**
* Gets a skin from a Mojang UUID. * Gets a skin from a Mojang UUID.
@ -31,16 +22,14 @@ public class PlayerSkin {
* @param uuid Mojang UUID * @param uuid Mojang UUID
* @return a player skin based on the UUID, null if not found * @return a player skin based on the UUID, null if not found
*/ */
@Nullable @Blocking
public static PlayerSkin fromUuid(@NotNull String uuid) { public static @Nullable PlayerSkin fromUuid(@NotNull String uuid) {
final JsonObject jsonObject = MojangUtils.fromUuid(uuid); final JsonObject jsonObject = MojangUtils.fromUuid(uuid);
final JsonArray propertiesArray = jsonObject.get("properties").getAsJsonArray(); final JsonArray propertiesArray = jsonObject.get("properties").getAsJsonArray();
for (JsonElement jsonElement : propertiesArray) { for (JsonElement jsonElement : propertiesArray) {
final JsonObject propertyObject = jsonElement.getAsJsonObject(); final JsonObject propertyObject = jsonElement.getAsJsonObject();
final String name = propertyObject.get("name").getAsString(); final String name = propertyObject.get("name").getAsString();
if (!name.equals("textures")) if (!name.equals("textures")) continue;
continue;
final String textureValue = propertyObject.get("value").getAsString(); final String textureValue = propertyObject.get("value").getAsString();
final String signatureValue = propertyObject.get("signature").getAsString(); final String signatureValue = propertyObject.get("signature").getAsString();
return new PlayerSkin(textureValue, signatureValue); return new PlayerSkin(textureValue, signatureValue);
@ -54,8 +43,8 @@ public class PlayerSkin {
* @param username the Minecraft username * @param username the Minecraft username
* @return a skin based on a Minecraft username, null if not found * @return a skin based on a Minecraft username, null if not found
*/ */
@Nullable @Blocking
public static PlayerSkin fromUsername(@NotNull String username) { public static @Nullable PlayerSkin fromUsername(@NotNull String username) {
final JsonObject jsonObject = MojangUtils.fromUsername(username); final JsonObject jsonObject = MojangUtils.fromUsername(username);
final String uuid = jsonObject.get("id").getAsString(); final String uuid = jsonObject.get("id").getAsString();
// Retrieve the skin data from the mojang uuid // Retrieve the skin data from the mojang uuid
@ -63,49 +52,18 @@ public class PlayerSkin {
} }
/** /**
* Gets the skin textures value. * @deprecated use {@link #textures()}
*
* @return the textures value
*/ */
@Deprecated
public String getTextures() { public String getTextures() {
return textures; return textures;
} }
/** /**
* Gets the skin signature. * @deprecated use {@link #signature()}
*
* @return the skin signature
*/ */
@Deprecated
public String getSignature() { public String getSignature() {
return signature; return signature;
} }
@Override
public String toString() {
return "PlayerSkin{" +
"textures='" + textures + '\'' +
", signature='" + signature + '\'' +
'}';
}
/**
* {@inheritDoc}
*/
@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);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hash(textures, signature);
}
} }

View File

@ -198,6 +198,7 @@ public class CombinedAttackGoal extends GoalSelector {
if (pathPosition != null) { if (pathPosition != null) {
navigator.setPathTo(null); navigator.setPathTo(null);
} }
this.entityCreature.lookAt(target);
return; return;
} }
// Otherwise going to the target. // Otherwise going to the target.

View File

@ -1,6 +1,7 @@
package net.minestom.server.entity.ai.goal; package net.minestom.server.entity.ai.goal;
import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.ai.GoalSelector; import net.minestom.server.entity.ai.GoalSelector;
@ -15,6 +16,8 @@ public class FollowTargetGoal extends GoalSelector {
private boolean forceEnd = false; private boolean forceEnd = false;
private Point lastTargetPos; private Point lastTargetPos;
private Entity target;
/** /**
* Creates a follow target goal object. * Creates a follow target goal object.
* *
@ -28,8 +31,14 @@ public class FollowTargetGoal extends GoalSelector {
@Override @Override
public boolean shouldStart() { public boolean shouldStart() {
return entityCreature.getTarget() != null && Entity target = entityCreature.getTarget();
entityCreature.getTarget().getPosition().distance(entityCreature.getPosition()) >= 2; if (target == null) target = findTarget();
if (target == null) return false;
final boolean result = target.getPosition().distance(entityCreature.getPosition()) >= 2;
if (result) {
this.target = target;
}
return result;
} }
@Override @Override
@ -37,23 +46,22 @@ public class FollowTargetGoal extends GoalSelector {
lastUpdateTime = 0; lastUpdateTime = 0;
forceEnd = false; forceEnd = false;
lastTargetPos = null; lastTargetPos = null;
final Entity target = entityCreature.getTarget(); if (target == null) {
if (target != null) { // No defined target
Navigator navigator = entityCreature.getNavigator(); this.forceEnd = true;
return;
lastTargetPos = target.getPosition(); }
if (lastTargetPos.distance(entityCreature.getPosition()) < 2) { this.entityCreature.setTarget(target);
forceEnd = true; Navigator navigator = entityCreature.getNavigator();
navigator.setPathTo(null); this.lastTargetPos = target.getPosition();
return; if (lastTargetPos.distance(entityCreature.getPosition()) < 2) {
} // Target is too far
this.forceEnd = true;
if (navigator.getPathPosition() == null || navigator.setPathTo(null);
(!navigator.getPathPosition().samePoint(lastTargetPos))) { return;
navigator.setPathTo(lastTargetPos); }
} else { if (navigator.getPathPosition() == null || !navigator.getPathPosition().samePoint(lastTargetPos)) {
forceEnd = true; navigator.setPathTo(lastTargetPos);
}
} else { } else {
forceEnd = true; forceEnd = true;
} }
@ -66,7 +74,7 @@ public class FollowTargetGoal extends GoalSelector {
pathDuration.toMillis() + lastUpdateTime > time) { pathDuration.toMillis() + lastUpdateTime > time) {
return; return;
} }
final var targetPos = entityCreature.getTarget() != null ? entityCreature.getTarget().getPosition() : null; final Pos targetPos = entityCreature.getTarget() != null ? entityCreature.getTarget().getPosition() : null;
if (targetPos != null && !targetPos.samePoint(lastTargetPos)) { if (targetPos != null && !targetPos.samePoint(lastTargetPos)) {
this.lastUpdateTime = time; this.lastUpdateTime = time;
this.lastTargetPos = targetPos; this.lastTargetPos = targetPos;
@ -79,11 +87,12 @@ public class FollowTargetGoal extends GoalSelector {
final Entity target = entityCreature.getTarget(); final Entity target = entityCreature.getTarget();
return forceEnd || return forceEnd ||
target == null || target == null ||
target.isRemoved() ||
target.getPosition().distance(entityCreature.getPosition()) < 2; target.getPosition().distance(entityCreature.getPosition()) < 2;
} }
@Override @Override
public void end() { public void end() {
entityCreature.getNavigator().setPathTo(null); this.entityCreature.getNavigator().setPathTo(null);
} }
} }

View File

@ -81,6 +81,7 @@ public class MeleeAttackGoal extends GoalSelector {
// Attack the target entity // Attack the target entity
if (entityCreature.getDistance(target) <= range) { if (entityCreature.getDistance(target) <= range) {
entityCreature.lookAt(target);
if (!Cooldown.hasCooldown(time, lastHit, delay)) { if (!Cooldown.hasCooldown(time, lastHit, delay)) {
entityCreature.attack(target, true); entityCreature.attack(target, true);
this.lastHit = time; this.lastHit = time;

View File

@ -6,8 +6,8 @@ import net.minestom.server.entity.ai.GoalSelector;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random;
public class RandomStrollGoal extends GoalSelector { public class RandomStrollGoal extends GoalSelector {
@ -15,6 +15,7 @@ public class RandomStrollGoal extends GoalSelector {
private final int radius; private final int radius;
private final List<Vec> closePositions; private final List<Vec> closePositions;
private final Random random = new Random();
private long lastStroll; private long lastStroll;
@ -31,8 +32,11 @@ public class RandomStrollGoal extends GoalSelector {
@Override @Override
public void start() { public void start() {
Collections.shuffle(closePositions); int remainingAttempt = closePositions.size();
for (var position : closePositions) { while (remainingAttempt-- > 0) {
final int index = random.nextInt(closePositions.size());
final Vec position = closePositions.get(index);
final var target = entityCreature.getPosition().add(position); final var target = entityCreature.getPosition().add(position);
final boolean result = entityCreature.getNavigator().setPathTo(target); final boolean result = entityCreature.getNavigator().setPathTo(target);
if (result) { if (result) {

View File

@ -124,6 +124,7 @@ public class RangedAttackGoal extends GoalSelector {
if (pathPosition != null) { if (pathPosition != null) {
navigator.setPathTo(null); navigator.setPathTo(null);
} }
this.entityCreature.lookAt(target);
return; return;
} }
final var targetPosition = target.getPosition(); final var targetPosition = target.getPosition();

View File

@ -21,6 +21,7 @@ public class ClosestEntityTarget extends TargetSelector {
private final float range; private final float range;
private final Class<? extends LivingEntity>[] entitiesTarget; private final Class<? extends LivingEntity>[] entitiesTarget;
@SafeVarargs
public ClosestEntityTarget(@NotNull EntityCreature entityCreature, float range, public ClosestEntityTarget(@NotNull EntityCreature entityCreature, float range,
@NotNull Class<? extends LivingEntity>... entitiesTarget) { @NotNull Class<? extends LivingEntity>... entitiesTarget) {
super(entityCreature); super(entityCreature);

View File

@ -22,20 +22,15 @@ public class LastEntityDamagerTarget extends TargetSelector {
@Override @Override
public Entity findTarget() { public Entity findTarget() {
final DamageType damageType = entityCreature.getLastDamageSource(); final DamageType damageType = entityCreature.getLastDamageSource();
if (!(damageType instanceof EntityDamage entityDamage)) {
if (!(damageType instanceof EntityDamage)) {
// No damager recorded, return null // No damager recorded, return null
return null; return null;
} }
final EntityDamage entityDamage = (EntityDamage) damageType;
final Entity entity = entityDamage.getSource(); final Entity entity = entityDamage.getSource();
if (entity.isRemoved()) { if (entity.isRemoved()) {
// Entity not valid // Entity not valid
return null; return null;
} }
// Check range // Check range
return entityCreature.getDistance(entity) < range ? entity : null; return entityCreature.getDistance(entity) < range ? entity : null;
} }

View File

@ -2,7 +2,6 @@ package net.minestom.server.entity.fakeplayer;
import com.extollit.gaming.ai.path.HydrazinePathFinder; import com.extollit.gaming.ai.path.HydrazinePathFinder;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.entity.pathfinding.NavigableEntity; import net.minestom.server.entity.pathfinding.NavigableEntity;
@ -118,9 +117,8 @@ public class FakePlayer extends Player implements NavigableEntity {
@Override @Override
public void update(long time) { public void update(long time) {
super.update(time); super.update(time);
// Path finding // Path finding
this.navigator.tick(getAttributeValue(Attribute.MOVEMENT_SPEED)); this.navigator.tick();
} }
@Override @Override
@ -131,12 +129,10 @@ public class FakePlayer extends Player implements NavigableEntity {
} }
@Override @Override
protected boolean addViewer0(@NotNull Player player) { public void updateNewViewer(@NotNull Player player) {
final boolean result = super.addViewer0(player); player.getPlayerConnection().sendPacket(getAddPlayerToList());
if (result) { handleTabList(player.getPlayerConnection());
handleTabList(player.getPlayerConnection()); super.updateNewViewer(player);
}
return result;
} }
/** /**

View File

@ -25,9 +25,11 @@ public class FakePlayerOption {
* WARNING: this can't be changed halfway. * WARNING: this can't be changed halfway.
* *
* @param registered should the fake player be registered internally * @param registered should the fake player be registered internally
* @return this instance, allowing for chained method calls
*/ */
public void setRegistered(boolean registered) { public FakePlayerOption setRegistered(boolean registered) {
this.registered = registered; this.registered = registered;
return this;
} }
/** /**
@ -45,8 +47,10 @@ public class FakePlayerOption {
* WARNING: this can't be changed halfway. * WARNING: this can't be changed halfway.
* *
* @param inTabList should the player be in the tab-list * @param inTabList should the player be in the tab-list
* @return this instance, allowing for chained method calls
*/ */
public void setInTabList(boolean inTabList) { public FakePlayerOption setInTabList(boolean inTabList) {
this.inTabList = inTabList; this.inTabList = inTabList;
return this;
} }
} }

View File

@ -24,15 +24,9 @@ public class PufferfishMeta extends AbstractFishMeta {
private void updateBoundingBox(State state) { private void updateBoundingBox(State state) {
switch (state) { switch (state) {
case UNPUFFED: case UNPUFFED -> setBoundingBox(.35D, .35D);
setBoundingBox(.35D, .35D); case SEMI_PUFFED -> setBoundingBox(.5D, .5D);
break; default -> setBoundingBox(.7D, .7D);
case SEMI_PUFFED:
setBoundingBox(.5D, .5D);
break;
default:
setBoundingBox(.7D, .7D);
break;
} }
} }

View File

@ -14,6 +14,7 @@ import net.minestom.server.instance.Instance;
import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.WorldBorder;
import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.position.PositionUtils; import net.minestom.server.utils.position.PositionUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -22,11 +23,9 @@ import org.jetbrains.annotations.Nullable;
/** /**
* Necessary object for all {@link NavigableEntity}. * Necessary object for all {@link NavigableEntity}.
*/ */
public class Navigator { public final class Navigator {
private final PFPathingEntity pathingEntity; private final PFPathingEntity pathingEntity;
private HydrazinePathFinder pathFinder; private HydrazinePathFinder pathFinder;
private IPath path;
private Point pathPosition; private Point pathPosition;
private final Entity entity; private final Entity entity;
@ -88,30 +87,24 @@ public class Navigator {
// Tried to set path to the same target position // Tried to set path to the same target position
return false; return false;
} }
final Instance instance = entity.getInstance(); final Instance instance = entity.getInstance();
if (pathFinder == null) { if (pathFinder == null) {
// Unexpected error // Unexpected error
return false; return false;
} }
this.pathFinder.reset();
pathFinder.reset();
if (point == null) { if (point == null) {
return false; return false;
} }
// Can't path with a null instance. // Can't path with a null instance.
if (instance == null) { if (instance == null) {
return false; return false;
} }
// Can't path outside the world border // Can't path outside the world border
final WorldBorder worldBorder = instance.getWorldBorder(); final WorldBorder worldBorder = instance.getWorldBorder();
if (!worldBorder.isInside(point)) { if (!worldBorder.isInside(point)) {
return false; return false;
} }
// Can't path in an unloaded chunk // Can't path in an unloaded chunk
final Chunk chunk = instance.getChunkAt(point); final Chunk chunk = instance.getChunkAt(point);
if (!ChunkUtils.isLoaded(chunk)) { if (!ChunkUtils.isLoaded(chunk)) {
@ -126,11 +119,9 @@ public class Navigator {
point.y(), point.y(),
point.z(), point.z(),
pathOptions); pathOptions);
this.path = path;
final boolean success = path != null; final boolean success = path != null;
this.pathPosition = success ? point : null; this.pathPosition = success ? point : null;
return success; return success;
} }
@ -141,57 +132,16 @@ public class Navigator {
return setPathTo(position, true); return setPathTo(position, true);
} }
public synchronized void tick(float speed) { @ApiStatus.Internal
// No pathfinding tick for dead entities public synchronized void tick() {
if (pathPosition == null) return; // No path
if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead()) if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead())
return; return; // No pathfinding tick for dead entities
if (pathFinder.updatePathFor(pathingEntity) == null) {
if (pathPosition != null) { reset();
IPath path = pathFinder.updatePathFor(pathingEntity);
this.path = path;
if (path != null) {
final Point targetPosition = pathingEntity.getTargetPosition();
if (targetPosition != null) {
moveTowards(targetPosition, speed);
}
} else {
if (pathPosition != null) {
this.pathPosition = null;
pathFinder.reset();
}
}
} }
} }
/**
* Gets the pathing entity.
* <p>
* Used by the pathfinder.
*
* @return the pathing entity
*/
@NotNull
public PFPathingEntity getPathingEntity() {
return pathingEntity;
}
/**
* Gets the assigned pathfinder.
* <p>
* Can be null if the navigable element hasn't been assigned to an {@link Instance} yet.
*
* @return the current pathfinder, null if none
*/
@Nullable
public HydrazinePathFinder getPathFinder() {
return pathFinder;
}
public void setPathFinder(@Nullable HydrazinePathFinder pathFinder) {
this.pathFinder = pathFinder;
}
/** /**
* Gets the target pathfinder position. * Gets the target pathfinder position.
* *
@ -201,28 +151,22 @@ public class Navigator {
return pathPosition; return pathPosition;
} }
/** public @NotNull Entity getEntity() {
* Changes the position this element is trying to reach.
*
* @param pathPosition the new current path position
* @deprecated Please use {@link #setPathTo(Point)}
*/
@Deprecated
public void setPathPosition(@Nullable Point pathPosition) {
this.pathPosition = pathPosition;
}
@Nullable
public IPath getPath() {
return path;
}
public void setPath(@Nullable IPath path) {
this.path = path;
}
@NotNull
public Entity getEntity() {
return entity; return entity;
} }
@ApiStatus.Internal
public @NotNull PFPathingEntity getPathingEntity() {
return pathingEntity;
}
@ApiStatus.Internal
public void setPathFinder(@Nullable HydrazinePathFinder pathFinder) {
this.pathFinder = pathFinder;
}
private void reset() {
this.pathPosition = null;
this.pathFinder.reset();
}
} }

View File

@ -9,15 +9,15 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.LivingEntity;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class PFPathingEntity implements IPathingEntity { @ApiStatus.Internal
public final class PFPathingEntity implements IPathingEntity {
private final Navigator navigator; private final Navigator navigator;
private final Entity entity; private final Entity entity;
private float searchRange; private float searchRange;
private Point targetPosition;
// Capacities // Capacities
private boolean fireResistant; private boolean fireResistant;
@ -37,10 +37,6 @@ public class PFPathingEntity implements IPathingEntity {
this.searchRange = getAttributeValue(Attribute.FOLLOW_RANGE); this.searchRange = getAttributeValue(Attribute.FOLLOW_RANGE);
} }
public Point getTargetPosition() {
return targetPosition;
}
@Override @Override
public int age() { public int age() {
return (int) entity.getAliveTicks(); return (int) entity.getAliveTicks();
@ -194,7 +190,8 @@ public class PFPathingEntity implements IPathingEntity {
@Override @Override
public void moveTo(Vec3d position, Passibility passibility, Gravitation gravitation) { public void moveTo(Vec3d position, Passibility passibility, Gravitation gravitation) {
this.targetPosition = new Vec(position.x, position.y, position.z); final Point targetPosition = new Vec(position.x, position.y, position.z);
this.navigator.moveTowards(targetPosition, getAttributeValue(Attribute.MOVEMENT_SPEED));
final double entityY = entity.getPosition().y(); final double entityY = entity.getPosition().y();
if (entityY < targetPosition.y()) { if (entityY < targetPosition.y()) {
this.navigator.jump(1); this.navigator.jump(1);

View File

@ -4,12 +4,16 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class EventDispatcher { public final class EventDispatcher {
public static void call(@NotNull Event event) { public static void call(@NotNull Event event) {
MinecraftServer.getGlobalEventHandler().call(event); MinecraftServer.getGlobalEventHandler().call(event);
} }
public static <E extends Event> ListenerHandle<E> getHandle(@NotNull Class<E> handleType) {
return MinecraftServer.getGlobalEventHandler().getHandle(handleType);
}
public static void callCancellable(@NotNull CancellableEvent event, @NotNull Runnable successCallback) { public static void callCancellable(@NotNull CancellableEvent event, @NotNull Runnable successCallback) {
MinecraftServer.getGlobalEventHandler().callCancellable(event, successCallback); MinecraftServer.getGlobalEventHandler().callCancellable(event, successCallback);
} }

View File

@ -19,7 +19,7 @@ import java.util.function.Predicate;
*/ */
public interface EventListener<T extends Event> { public interface EventListener<T extends Event> {
@NotNull Class<T> getEventType(); @NotNull Class<T> eventType();
@NotNull Result run(@NotNull T event); @NotNull Result run(@NotNull T event);
@ -122,7 +122,7 @@ public interface EventListener<T extends Event> {
final var handler = this.handler; final var handler = this.handler;
return new EventListener<>() { return new EventListener<>() {
@Override @Override
public @NotNull Class<T> getEventType() { public @NotNull Class<T> eventType() {
return eventType; return eventType;
} }

View File

@ -23,8 +23,7 @@ import java.util.function.Predicate;
* *
* @param <T> The event type accepted by this node * @param <T> The event type accepted by this node
*/ */
@ApiStatus.NonExtendable public sealed interface EventNode<T extends Event> permits EventNodeImpl {
public interface EventNode<T extends Event> {
/** /**
* Creates an event node which accepts any event type with no filtering. * Creates an event node which accepts any event type with no filtering.
@ -182,15 +181,24 @@ public interface EventNode<T extends Event> {
} }
/** /**
* Executes the given event on this node. The event must pass all conditions before * Calls an event starting from this node.
* it will be forwarded to the listeners.
* <p>
* Calling an event on a node will execute all child nodes, however, an event may be
* called anywhere on the event graph and it will propagate down from there only.
* *
* @param event the event to execute * @param event the event to call
*/ */
void call(@NotNull T event); default void call(@NotNull T event) {
//noinspection unchecked
getHandle((Class<T>) event.getClass()).call(event);
}
/**
* Gets the handle of an event type.
*
* @param handleType the handle type
* @param <E> the event type
* @return the handle linked to {@code handleType}
*/
@ApiStatus.Experimental
<E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType);
/** /**
* Execute a cancellable event with a callback to execute if the event is successful. * Execute a cancellable event with a callback to execute if the event is successful.
@ -287,7 +295,9 @@ public interface EventNode<T extends Event> {
* *
* @param name The node name to filter for * @param name The node name to filter for
*/ */
void removeChildren(@NotNull String name); default void removeChildren(@NotNull String name) {
removeChildren(name, getEventType());
}
/** /**
* Directly adds a child node to this node. * Directly adds a child node to this node.
@ -318,9 +328,24 @@ public interface EventNode<T extends Event> {
@Contract(value = "_ -> this") @Contract(value = "_ -> this")
@NotNull EventNode<T> removeListener(@NotNull EventListener<? extends T> listener); @NotNull EventNode<T> removeListener(@NotNull EventListener<? extends T> listener);
/**
* Maps a specific object to a node.
* <p>
* Be aware that such structure have huge performance penalty as they will
* always require a map lookup. Use only at last resort.
*
* @param node the node to map
* @param value the mapped value
*/
@ApiStatus.Experimental @ApiStatus.Experimental
void map(@NotNull EventNode<? extends T> node, @NotNull Object value); void map(@NotNull EventNode<? extends T> node, @NotNull Object value);
/**
* Undo {@link #map(EventNode, Object)}
*
* @param value the value to unmap
* @return true if the value has been unmapped, false if nothing happened
*/
@ApiStatus.Experimental @ApiStatus.Experimental
boolean unmap(@NotNull Object value); boolean unmap(@NotNull Object value);

View File

@ -1,6 +1,7 @@
package net.minestom.server.event; package net.minestom.server.event;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.event.trait.RecursiveEvent;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -10,19 +11,17 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.BiConsumer;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.IntUnaryOperator;
import java.util.stream.Collectors;
class EventNodeImpl<T extends Event> implements EventNode<T> { non-sealed class EventNodeImpl<T extends Event> implements EventNode<T> {
private static final Object GLOBAL_CHILD_LOCK = new Object(); private static final Object GLOBAL_CHILD_LOCK = new Object();
private final Object lock = new Object();
private final Map<Class<? extends T>, Handle<T>> handleMap = new ConcurrentHashMap<>();
private final Map<Class<? extends T>, ListenerEntry<T>> listenerMap = new ConcurrentHashMap<>(); private final Map<Class<? extends T>, ListenerEntry<T>> listenerMap = new ConcurrentHashMap<>();
private final Set<EventNode<T>> children = new CopyOnWriteArraySet<>(); private final Set<EventNodeImpl<T>> children = new CopyOnWriteArraySet<>();
private final Map<Object, ListenerEntry<T>> mappedNodeCache = new WeakHashMap<>(); private final Map<Object, EventNodeImpl<T>> mappedNodeCache = new WeakHashMap<>();
private final String name; private final String name;
private final EventFilter<T, ?> filter; private final EventFilter<T, ?> filter;
@ -31,9 +30,9 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
private volatile int priority; private volatile int priority;
private volatile EventNodeImpl<? super T> parent; private volatile EventNodeImpl<? super T> parent;
protected EventNodeImpl(@NotNull String name, EventNodeImpl(@NotNull String name,
@NotNull EventFilter<T, ?> filter, @NotNull EventFilter<T, ?> filter,
@Nullable BiPredicate<T, Object> predicate) { @Nullable BiPredicate<T, Object> predicate) {
this.name = name; this.name = name;
this.filter = filter; this.filter = filter;
this.predicate = predicate; this.predicate = predicate;
@ -41,35 +40,16 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
} }
@Override @Override
public void call(@NotNull T event) { public <E extends T> @NotNull ListenerHandle<E> getHandle(@NotNull Class<E> handleType) {
final var eventClass = event.getClass(); //noinspection unchecked
if (!eventType.isAssignableFrom(eventClass)) return; // Invalid event type return (ListenerHandle<E>) handleMap.computeIfAbsent(handleType,
// Conditions aClass -> new Handle<>(this, (Class<T>) aClass));
if (predicate != null) {
try {
final var value = filter.getHandler(event);
if (!predicate.test(event, value)) return;
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
return;
}
}
// Process listeners list
final var entry = listenerMap.get(eventClass);
if (entry == null) return; // No listener nor children
entry.call(event);
// Process children
if (entry.childCount > 0) {
this.children.stream()
.sorted(Comparator.comparing(EventNode::getPriority))
.forEach(child -> child.call(event));
}
} }
@Override @Override
public <E extends T> @NotNull List<EventNode<E>> findChildren(@NotNull String name, Class<E> eventType) { public <E extends T> @NotNull List<EventNode<E>> findChildren(@NotNull String name, Class<E> eventType) {
if (children.isEmpty()) return Collections.emptyList();
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
if (children.isEmpty()) return Collections.emptyList();
List<EventNode<E>> result = new ArrayList<>(); List<EventNode<E>> result = new ArrayList<>();
for (EventNode<T> child : children) { for (EventNode<T> child : children) {
if (equals(child, name, eventType)) { if (equals(child, name, eventType)) {
@ -115,23 +95,15 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
} }
} }
@Override
public void removeChildren(@NotNull String name) {
removeChildren(name, eventType);
}
@Override @Override
public @NotNull EventNode<T> addChild(@NotNull EventNode<? extends T> child) { public @NotNull EventNode<T> addChild(@NotNull EventNode<? extends T> child) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
final var childImpl = (EventNodeImpl<? extends T>) child; final var childImpl = (EventNodeImpl<? extends T>) child;
Check.stateCondition(childImpl.parent != null, "Node already has a parent"); Check.stateCondition(childImpl.parent != null, "Node already has a parent");
Check.stateCondition(Objects.equals(parent, child), "Cannot have a child as parent"); Check.stateCondition(Objects.equals(parent, child), "Cannot have a child as parent");
final boolean result = this.children.add((EventNodeImpl<T>) childImpl); if (!children.add((EventNodeImpl<T>) childImpl)) return this; // Couldn't add the child (already present?)
if (result) { childImpl.parent = this;
childImpl.parent = this; childImpl.invalidateEventsFor(this);
// Increase listener count
propagateNode(childImpl, IntUnaryOperator.identity());
}
} }
return this; return this;
} }
@ -139,13 +111,11 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
@Override @Override
public @NotNull EventNode<T> removeChild(@NotNull EventNode<? extends T> child) { public @NotNull EventNode<T> removeChild(@NotNull EventNode<? extends T> child) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
final boolean result = this.children.remove(child); final var childImpl = (EventNodeImpl<? extends T>) child;
if (result) { final boolean result = this.children.remove(childImpl);
final var childImpl = (EventNodeImpl<? extends T>) child; if (!result) return this; // Child not found
childImpl.parent = null; childImpl.parent = null;
// Decrease listener count childImpl.invalidateEventsFor(this);
propagateNode(childImpl, count -> -count);
}
} }
return this; return this;
} }
@ -153,10 +123,10 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
@Override @Override
public @NotNull EventNode<T> addListener(@NotNull EventListener<? extends T> listener) { public @NotNull EventNode<T> addListener(@NotNull EventListener<? extends T> listener) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
final var eventType = listener.getEventType(); final var eventType = listener.eventType();
var entry = getEntry(eventType); ListenerEntry<T> entry = getEntry(eventType);
entry.listeners.add((EventListener<T>) listener); entry.listeners.add((EventListener<T>) listener);
propagateToParent(eventType, 1); invalidateEvent(eventType);
} }
return this; return this;
} }
@ -164,50 +134,36 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
@Override @Override
public @NotNull EventNode<T> removeListener(@NotNull EventListener<? extends T> listener) { public @NotNull EventNode<T> removeListener(@NotNull EventListener<? extends T> listener) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
final var eventType = listener.getEventType(); final var eventType = listener.eventType();
var entry = listenerMap.get(eventType); ListenerEntry<T> entry = listenerMap.get(eventType);
if (entry == null) return this; if (entry == null) return this; // There is no listener with such type
var listeners = entry.listeners; if (entry.listeners.remove(listener)) invalidateEvent(eventType);
final boolean removed = listeners.remove(listener);
if (removed) propagateToParent(eventType, -1);
} }
return this; return this;
} }
@Override @Override
public void map(@NotNull EventNode<? extends T> node, @NotNull Object value) { public void map(@NotNull EventNode<? extends T> node, @NotNull Object value) {
final var nodeImpl = (EventNodeImpl<? extends T>) node;
final var valueType = value.getClass();
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
nodeImpl.listenerMap.forEach((type, listenerEntry) -> { final var nodeImpl = (EventNodeImpl<? extends T>) node;
final var entry = getEntry(type); Check.stateCondition(nodeImpl.parent != null, "Node already has a parent");
final boolean correct = entry.filters.stream().anyMatch(eventFilter -> { Check.stateCondition(Objects.equals(parent, nodeImpl), "Cannot map to self");
final var handlerType = eventFilter.handlerType(); EventNodeImpl<T> previous = this.mappedNodeCache.put(value, (EventNodeImpl<T>) nodeImpl);
return handlerType != null && handlerType.isAssignableFrom(valueType); if (previous != null) previous.parent = null;
}); nodeImpl.parent = this;
Check.stateCondition(!correct, "The node filter {0} is not compatible with type {1}", nodeImpl.eventType, valueType); nodeImpl.invalidateEventsFor(this);
synchronized (mappedNodeCache) {
entry.mappedNode.put(value, (EventNode<T>) nodeImpl);
mappedNodeCache.put(value, entry);
// TODO propagate
}
});
} }
} }
@Override @Override
public boolean unmap(@NotNull Object value) { public boolean unmap(@NotNull Object value) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
synchronized (mappedNodeCache) { final var mappedNode = this.mappedNodeCache.remove(value);
var entry = mappedNodeCache.remove(value); if (mappedNode == null) return false; // Mapped node not found
if (entry == null) return false; final var childImpl = (EventNodeImpl<? extends T>) mappedNode;
final EventNode<T> previousNode = entry.mappedNode.remove(value); childImpl.parent = null;
if (previousNode != null) { childImpl.invalidateEventsFor(this);
// TODO propagate return true;
return true;
}
return false;
}
} }
} }
@ -215,9 +171,9 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
public void register(@NotNull EventBinding<? extends T> binding) { public void register(@NotNull EventBinding<? extends T> binding) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
for (var eventType : binding.eventTypes()) { for (var eventType : binding.eventTypes()) {
var entry = getEntry((Class<? extends T>) eventType); ListenerEntry<T> entry = getEntry((Class<? extends T>) eventType);
final boolean added = entry.bindingConsumers.add((Consumer<T>) binding.consumer(eventType)); final boolean added = entry.bindingConsumers.add((Consumer<T>) binding.consumer(eventType));
if (added) propagateToParent((Class<? extends T>) eventType, 1); if (added) invalidateEvent((Class<? extends T>) eventType);
} }
} }
} }
@ -226,10 +182,10 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
public void unregister(@NotNull EventBinding<? extends T> binding) { public void unregister(@NotNull EventBinding<? extends T> binding) {
synchronized (GLOBAL_CHILD_LOCK) { synchronized (GLOBAL_CHILD_LOCK) {
for (var eventType : binding.eventTypes()) { for (var eventType : binding.eventTypes()) {
var entry = listenerMap.get(eventType); ListenerEntry<T> entry = listenerMap.get(eventType);
if (entry == null) return; if (entry == null) return;
final boolean removed = entry.bindingConsumers.remove(binding.consumer(eventType)); final boolean removed = entry.bindingConsumers.remove(binding.consumer(eventType));
if (removed) propagateToParent((Class<? extends T>) eventType, -1); if (removed) invalidateEvent((Class<? extends T>) eventType);
} }
} }
} }
@ -260,103 +216,224 @@ class EventNodeImpl<T extends Event> implements EventNode<T> {
return parent; return parent;
} }
private void propagateChildCountChange(Class<? extends T> eventClass, int count) { private void invalidateEventsFor(EventNodeImpl<? super T> node) {
var entry = getEntry(eventClass); for (Class<? extends T> eventType : listenerMap.keySet()) {
final int result = ListenerEntry.CHILD_UPDATER.addAndGet(entry, count); node.invalidateEvent(eventType);
if (result == 0 && entry.listeners.isEmpty()) {
this.listenerMap.remove(eventClass);
} else if (result < 0) {
throw new IllegalStateException("Something wrong happened, listener count: " + result);
} }
if (parent != null) { // TODO bindings?
parent.propagateChildCountChange(eventClass, count); for (EventNodeImpl<T> child : children) {
child.invalidateEventsFor(node);
} }
} }
private void propagateToParent(Class<? extends T> eventClass, int count) { private void invalidateEvent(Class<? extends T> eventClass) {
final var parent = this.parent; forTargetEvents(eventClass, type -> {
if (parent != null) { Handle<? super T> handle = handleMap.get(type);
synchronized (parent.lock) { if (handle != null) handle.updated = false;
parent.propagateChildCountChange(eventClass, count); });
} final EventNodeImpl<? super T> parent = this.parent;
} if (parent != null) parent.invalidateEvent(eventClass);
}
private void propagateNode(EventNodeImpl<? extends T> child, IntUnaryOperator operator) {
synchronized (lock) {
final var listeners = child.listenerMap;
listeners.forEach((eventClass, eventListeners) -> {
final var entry = listeners.get(eventClass);
if (entry == null) return;
final int childCount = entry.listeners.size() + entry.childCount;
propagateChildCountChange(eventClass, operator.applyAsInt(childCount));
});
}
} }
private ListenerEntry<T> getEntry(Class<? extends T> type) { private ListenerEntry<T> getEntry(Class<? extends T> type) {
return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>(this, (Class<T>) aClass)); return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>());
} }
private static boolean equals(EventNode<?> node, String name, Class<?> eventType) { private static boolean equals(EventNode<?> node, String name, Class<?> eventType) {
final boolean nameCheck = node.getName().equals(name); return node.getName().equals(name) && eventType.isAssignableFrom((node.getEventType()));
final boolean typeCheck = eventType.isAssignableFrom(((EventNodeImpl<?>) node).eventType); }
return nameCheck && typeCheck;
private static void forTargetEvents(Class<?> type, Consumer<Class<?>> consumer) {
consumer.accept(type);
// Recursion
if (RecursiveEvent.class.isAssignableFrom(type)) {
final Class<?> superclass = type.getSuperclass();
if (superclass != null && RecursiveEvent.class.isAssignableFrom(superclass)) {
forTargetEvents(superclass, consumer);
}
}
} }
private static class ListenerEntry<T extends Event> { private static class ListenerEntry<T extends Event> {
private static final List<EventFilter<? extends Event, ?>> FILTERS = List.of(
EventFilter.ENTITY,
EventFilter.ITEM, EventFilter.INSTANCE,
EventFilter.INVENTORY, EventFilter.BLOCK);
@SuppressWarnings("rawtypes")
private static final AtomicIntegerFieldUpdater<ListenerEntry> CHILD_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(ListenerEntry.class, "childCount");
final EventNodeImpl<T> node;
final List<EventFilter<?, ?>> filters;
final List<EventListener<T>> listeners = new CopyOnWriteArrayList<>(); final List<EventListener<T>> listeners = new CopyOnWriteArrayList<>();
final Set<Consumer<T>> bindingConsumers = new CopyOnWriteArraySet<>(); final Set<Consumer<T>> bindingConsumers = new CopyOnWriteArraySet<>();
final Map<Object, EventNode<T>> mappedNode = new WeakHashMap<>(); }
volatile int childCount;
ListenerEntry(EventNodeImpl<T> node, Class<T> eventType) { static final class Handle<E extends Event> implements ListenerHandle<E> {
private final EventNodeImpl<E> node;
private final Class<E> eventType;
private Consumer<E> listener = null;
private volatile boolean updated;
Handle(EventNodeImpl<E> node, Class<E> eventType) {
this.node = node; this.node = node;
this.filters = FILTERS.stream().filter(eventFilter -> eventFilter.eventType().isAssignableFrom(eventType)).collect(Collectors.toList()); this.eventType = eventType;
} }
void call(T event) { @Override
// Event interfaces public void call(@NotNull E event) {
if (!bindingConsumers.isEmpty()) { final Consumer<E> listener = updatedListener();
for (var consumer : bindingConsumers) { if (listener == null) return;
consumer.accept(event); try {
} listener.accept(event);
} catch (Throwable e) {
MinecraftServer.getExceptionManager().handleException(e);
} }
// Mapped listeners }
if (!mappedNode.isEmpty()) {
synchronized (node.mappedNodeCache) { @Override
// Check mapped listeners for each individual event handler public boolean hasListener() {
for (var filter : filters) { return updatedListener() != null;
final var handler = filter.castHandler(event); }
final var map = mappedNode.get(handler);
if (map != null) map.call(event); @Nullable Consumer<E> updatedListener() {
} if (updated) return listener;
} synchronized (GLOBAL_CHILD_LOCK) {
if (updated) return listener;
final Consumer<E> listener = createConsumer();
this.listener = listener;
this.updated = true;
return listener;
} }
// Basic listeners }
if (!listeners.isEmpty()) {
for (EventListener<T> listener : listeners) { private @Nullable Consumer<E> createConsumer() {
EventListener.Result result; // Standalone listeners
try { List<Consumer<E>> listeners = new ArrayList<>();
result = listener.run(event); forTargetEvents(eventType, type -> {
} catch (Exception e) { final ListenerEntry<E> entry = node.listenerMap.get(type);
result = EventListener.Result.EXCEPTION; if (entry != null) {
MinecraftServer.getExceptionManager().handleException(e); final Consumer<E> result = listenersConsumer(entry);
} if (result != null) listeners.add(result);
if (result == EventListener.Result.EXPIRED) { }
listeners.remove(listener); });
final Consumer<E>[] listenersArray = listeners.toArray(Consumer[]::new);
// Mapped
final Consumer<E> mappedListener = mappedConsumer();
// Children
final Consumer<E>[] childrenListeners = node.children.stream()
.filter(child -> child.eventType.isAssignableFrom(eventType)) // Invalid event type
.sorted(Comparator.comparing(EventNode::getPriority))
.map(child -> ((Handle<E>) child.getHandle(eventType)).updatedListener())
.filter(Objects::nonNull)
.toArray(Consumer[]::new);
// Empty check
final BiPredicate<E, Object> predicate = node.predicate;
final EventFilter<E, ?> filter = node.filter;
final boolean hasPredicate = predicate != null;
final boolean hasListeners = listenersArray.length > 0;
final boolean hasMap = mappedListener != null;
final boolean hasChildren = childrenListeners.length > 0;
if (listenersArray.length == 0 && mappedListener == null && childrenListeners.length == 0) {
// No listener
return null;
}
return e -> {
// Filtering
if (hasPredicate) {
final Object value = filter.getHandler(e);
if (!predicate.test(e, value)) return;
}
// Normal listeners
if (hasListeners) {
for (Consumer<E> listener : listenersArray) {
listener.accept(e);
} }
} }
// Mapped nodes
if (hasMap) mappedListener.accept(e);
// Children
if (hasChildren) {
for (Consumer<E> childHandle : childrenListeners) {
childHandle.accept(e);
}
}
};
}
/**
* Create a consumer calling all listeners from {@link EventNode#addListener(EventListener)} and
* {@link EventNode#register(EventBinding)}.
* <p>
* Most computation should ideally be done outside the consumers as a one-time cost.
*/
private @Nullable Consumer<E> listenersConsumer(@NotNull ListenerEntry<E> entry) {
final EventListener<E>[] listenersCopy = entry.listeners.toArray(EventListener[]::new);
final Consumer<E>[] bindingsCopy = entry.bindingConsumers.toArray(Consumer[]::new);
final boolean listenersEmpty = listenersCopy.length == 0;
final boolean bindingsEmpty = bindingsCopy.length == 0;
if (listenersEmpty && bindingsEmpty) return null;
if (bindingsEmpty && listenersCopy.length == 1) {
// Only one normal listener
final EventListener<E> listener = listenersCopy[0];
return e -> callListener(listener, e);
}
// Worse case scenario, try to run everything
return e -> {
if (!listenersEmpty) {
for (EventListener<E> listener : listenersCopy) {
callListener(listener, e);
}
}
if (!bindingsEmpty) {
for (Consumer<E> eConsumer : bindingsCopy) {
eConsumer.accept(e);
}
}
};
}
/**
* Create a consumer handling {@link EventNode#map(EventNode, Object)}.
* The goal is to limit the amount of map lookup.
*/
private @Nullable Consumer<E> mappedConsumer() {
final var mappedNodeCache = node.mappedNodeCache;
if (mappedNodeCache.isEmpty()) return null;
Set<EventFilter<E, ?>> filters = new HashSet<>(mappedNodeCache.size());
Map<Object, Handle<E>> handlers = new HashMap<>(mappedNodeCache.size());
// Retrieve all filters used to retrieve potential handlers
for (var mappedEntry : mappedNodeCache.entrySet()) {
final EventNodeImpl<E> mappedNode = mappedEntry.getValue();
final Handle<E> handle = (Handle<E>) mappedNode.getHandle(eventType);
if (!handle.hasListener()) continue; // Implicit update
filters.add(mappedNode.filter);
handlers.put(mappedEntry.getKey(), handle);
}
// If at least one mapped node listen to this handle type,
// loop through them and forward to mapped node if there is a match
if (filters.isEmpty()) return null;
final EventFilter<E, ?>[] filterList = filters.toArray(EventFilter[]::new);
final BiConsumer<EventFilter<E, ?>, E> mapper = (filter, event) -> {
final Object handler = filter.castHandler(event);
final Handle<E> handle = handlers.get(handler);
if (handle != null) handle.call(event);
};
if (filterList.length == 1) {
final var firstFilter = filterList[0];
// Common case where there is only one filter
return event -> mapper.accept(firstFilter, event);
} else if (filterList.length == 2) {
final var firstFilter = filterList[0];
final var secondFilter = filterList[1];
return event -> {
mapper.accept(firstFilter, event);
mapper.accept(secondFilter, event);
};
} else {
return event -> {
for (var filter : filterList) {
mapper.accept(filter, event);
}
};
}
}
void callListener(@NotNull EventListener<E> listener, E event) {
EventListener.Result result = listener.run(event);
if (result == EventListener.Result.EXPIRED) {
node.removeListener(listener);
this.updated = false;
} }
} }
} }

View File

@ -0,0 +1,28 @@
package net.minestom.server.event;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.entity.EntityTickEvent;
import net.minestom.server.event.instance.InstanceChunkLoadEvent;
import net.minestom.server.event.instance.InstanceTickEvent;
import net.minestom.server.event.inventory.InventoryItemChangeEvent;
import net.minestom.server.event.inventory.PlayerInventoryItemChangeEvent;
import net.minestom.server.event.player.*;
import org.jetbrains.annotations.ApiStatus;
/**
* Contains handles to the main node {@link MinecraftServer#getGlobalEventHandler()}
* (compatible with {@link EventDispatcher}).
*/
@ApiStatus.Internal
public final class GlobalHandles {
public static final ListenerHandle<PlayerTickEvent> PLAYER_TICK = EventDispatcher.getHandle(PlayerTickEvent.class);
public static final ListenerHandle<PlayerPacketEvent> PLAYER_PACKET = EventDispatcher.getHandle(PlayerPacketEvent.class);
public static final ListenerHandle<PlayerMoveEvent> PLAYER_MOVE = EventDispatcher.getHandle(PlayerMoveEvent.class);
public static final ListenerHandle<EntityTickEvent> ENTITY_TICK = EventDispatcher.getHandle(EntityTickEvent.class);
public static final ListenerHandle<InstanceTickEvent> INSTANCE_TICK = EventDispatcher.getHandle(InstanceTickEvent.class);
public static final ListenerHandle<PlayerChunkLoadEvent> PLAYER_CHUNK_LOAD = EventDispatcher.getHandle(PlayerChunkLoadEvent.class);
public static final ListenerHandle<PlayerChunkUnloadEvent> PLAYER_CHUNK_UNLOAD = EventDispatcher.getHandle(PlayerChunkUnloadEvent.class);
public static final ListenerHandle<InstanceChunkLoadEvent> INSTANCE_CHUNK_LOAD = EventDispatcher.getHandle(InstanceChunkLoadEvent.class);
public static final ListenerHandle<InventoryItemChangeEvent> INVENTORY_ITEM_CHANGE_EVENT = EventDispatcher.getHandle(InventoryItemChangeEvent.class);
public static final ListenerHandle<PlayerInventoryItemChangeEvent> PLAYER_INVENTORY_ITEM_CHANGE_EVENT = EventDispatcher.getHandle(PlayerInventoryItemChangeEvent.class);
}

View File

@ -0,0 +1,37 @@
package net.minestom.server.event;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Represents a key to a listenable event, retrievable from {@link EventNode#getHandle(Class)}.
* Useful to avoid map lookups.
* <p>
* It is recommended to store instances of this class in {@code static final} fields.
*
* @param <E> the event type
*/
@ApiStatus.Experimental
public sealed interface ListenerHandle<E extends Event> permits EventNodeImpl.Handle {
/**
* Calls the given event.
* Will try to fast exit the execution when possible if {@link #hasListener()} return {@code false}.
* <p>
* Anonymous and subclasses are not supported, events must have the exact type {@code E}.
*
* @param event the event to call
*/
void call(@NotNull E event);
/**
* Gets if any listener has been registered for the given handle.
* May trigger an update if the cached data is not correct.
* <p>
* Useful if you are able to avoid expensive computation in the case where
* the event is unused. Be aware that {@link #call(Event)}
* has similar optimization built-in.
*
* @return true if the event has 1 or more listeners
*/
boolean hasListener();
}

View File

@ -1,14 +1,14 @@
package net.minestom.server.event.entity; package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Called when a player does a left click on an entity or with * Called when a player does a left click on an entity or with
* {@link net.minestom.server.entity.EntityCreature#attack(Entity)}. * {@link net.minestom.server.entity.EntityCreature#attack(Entity)}.
*/ */
public class EntityAttackEvent implements EntityEvent { public class EntityAttackEvent implements EntityInstanceEvent {
private final Entity entity; private final Entity entity;
private final Entity target; private final Entity target;

View File

@ -4,13 +4,13 @@ import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.damage.DamageType; import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Called with {@link LivingEntity#damage(DamageType, float)}. * Called with {@link LivingEntity#damage(DamageType, float)}.
*/ */
public class EntityDamageEvent implements EntityEvent, CancellableEvent { public class EntityDamageEvent implements EntityInstanceEvent, CancellableEvent {
private final Entity entity; private final Entity entity;
private final DamageType damageType; private final DamageType damageType;

View File

@ -1,10 +1,10 @@
package net.minestom.server.event.entity; package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class EntityDeathEvent implements EntityEvent { public class EntityDeathEvent implements EntityInstanceEvent {
// TODO cause // TODO cause
private final Entity entity; private final Entity entity;

View File

@ -2,13 +2,13 @@ package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.TemporalUnit; import java.time.temporal.TemporalUnit;
public class EntityFireEvent implements EntityEvent, CancellableEvent { public class EntityFireEvent implements EntityInstanceEvent, CancellableEvent {
private final Entity entity; private final Entity entity;
private Duration duration; private Duration duration;

View File

@ -3,16 +3,16 @@ package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.ItemEntity;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Called when two {@link ItemEntity} are merging their {@link ItemStack} together to form a sole entity. * Called when two {@link ItemEntity} are merging their {@link ItemStack} together to form a sole entity.
*/ */
public class EntityItemMergeEvent implements EntityEvent, CancellableEvent { public class EntityItemMergeEvent implements EntityInstanceEvent, CancellableEvent {
private Entity entity; private final Entity entity;
private final ItemEntity merged; private final ItemEntity merged;
private ItemStack result; private ItemStack result;
@ -31,9 +31,8 @@ public class EntityItemMergeEvent implements EntityEvent, CancellableEvent {
* *
* @return the source ItemEntity * @return the source ItemEntity
*/ */
@NotNull
@Override @Override
public ItemEntity getEntity() { public @NotNull ItemEntity getEntity() {
return (ItemEntity) entity; return (ItemEntity) entity;
} }
@ -44,8 +43,7 @@ public class EntityItemMergeEvent implements EntityEvent, CancellableEvent {
* *
* @return the merged ItemEntity * @return the merged ItemEntity
*/ */
@NotNull public @NotNull ItemEntity getMerged() {
public ItemEntity getMerged() {
return merged; return merged;
} }
@ -54,8 +52,7 @@ public class EntityItemMergeEvent implements EntityEvent, CancellableEvent {
* *
* @return the item stack * @return the item stack
*/ */
@NotNull public @NotNull ItemStack getResult() {
public ItemStack getResult() {
return result; return result;
} }

View File

@ -1,11 +1,11 @@
package net.minestom.server.event.entity; package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import net.minestom.server.potion.Potion; import net.minestom.server.potion.Potion;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class EntityPotionAddEvent implements EntityEvent { public class EntityPotionAddEvent implements EntityInstanceEvent {
private final Entity entity; private final Entity entity;
private final Potion potion; private final Potion potion;

View File

@ -1,11 +1,11 @@
package net.minestom.server.event.entity; package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import net.minestom.server.potion.Potion; import net.minestom.server.potion.Potion;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class EntityPotionRemoveEvent implements EntityEvent { public class EntityPotionRemoveEvent implements EntityInstanceEvent {
private final Entity entity; private final Entity entity;
private final Potion potion; private final Potion potion;

View File

@ -4,13 +4,13 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile; import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Called with {@link EntityProjectile#shoot(Point, double, double)} * Called with {@link EntityProjectile#shoot(Point, double, double)}
*/ */
public class EntityShootEvent implements EntityEvent, CancellableEvent { public class EntityShootEvent implements EntityInstanceEvent, CancellableEvent {
private final Entity entity; private final Entity entity;
private final Entity projectile; private final Entity projectile;

View File

@ -1,14 +1,14 @@
package net.minestom.server.event.entity; package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Called when a new instance is set for an entity. * Called when a new instance is set for an entity.
*/ */
public class EntitySpawnEvent implements EntityEvent { public class EntitySpawnEvent implements EntityInstanceEvent {
private final Entity entity; private final Entity entity;
private final Instance spawnInstance; private final Instance spawnInstance;

View File

@ -1,14 +1,14 @@
package net.minestom.server.event.entity; package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Called when an entity ticks itself. * Called when an entity ticks itself.
* Same event instance used for all tick events for the same entity. * Same event instance used for all tick events for the same entity.
*/ */
public class EntityTickEvent implements EntityEvent { public class EntityTickEvent implements EntityInstanceEvent {
private final Entity entity; private final Entity entity;

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