diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..250f3d9
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,28 @@
+name: Tests
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '25'
+ cache: gradle
+ - run: chmod +x gradlew
+ - run: ./gradlew test --no-daemon
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: test-results
+ path: |
+ build/reports/tests/test
+ build/test-results/test
diff --git a/README.md b/README.md
index dafff63..a4a801c 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,19 @@ Create client-side entities, send packets, and change what individual players se
+
+
+
+
+
-[Features](#features) · [Requirements](#requirements) · [Showcases](#showcases) · [Persistence](#persistence) · [Credits](#credits)
+[](#features)
+[](#requirements)
+[](#showcases)
+[](#persistence)
+[](#credits)
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
index f3aab38..96bd873 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,7 +4,9 @@ plugins {
}
group = "org.crebs"
-version = "1.0"
+version = "1.0.1"
+
+val mockitoAgent by configurations.creating
repositories {
mavenCentral()
@@ -25,6 +27,16 @@ repositories {
}
dependencies {
+ testImplementation(platform("org.junit:junit-bom:6.0.0"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+ testRuntimeOnly("org.apache.logging.log4j:log4j-api:2.26.1")
+ testImplementation("org.mockito:mockito-core:5.15.2")
+ testImplementation("net.bytebuddy:byte-buddy:1.18.12")
+ testRuntimeOnly("net.bytebuddy:byte-buddy-agent:1.18.12")
+ mockitoAgent("org.mockito:mockito-core:5.15.2") {
+ isTransitive = false
+ }
implementation("io.github.tofaa2:spigot:4.0.0-alpha.4") {
exclude(group = "org.jetbrains", module = "annotations")
}
@@ -80,5 +92,12 @@ tasks.withType().configureEach {
}
tasks.test {
- enabled = false
+ useJUnitPlatform()
+ jvmArgumentProviders.add(CommandLineArgumentProvider {
+ listOf("-javaagent:${mockitoAgent.singleFile.absolutePath}")
+ })
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 249e583..9fa7f28 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 83ca3e2..91c4fc6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,9 @@
-#Mon Oct 19 03:04:29 GMT+08:00 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 1b6c787..249efbb 100644
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -27,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -55,7 +57,7 @@
# 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
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac
done
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
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.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -133,22 +132,29 @@ location of your Java installation."
fi
else
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.
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -165,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then
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.
+
+# 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"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
diff --git a/gradlew.bat b/gradlew.bat
index 107acd3..3185a43 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,19 +13,49 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+@rem Catch executions from older scripts and ensure they exit cleanly.
+@rem This can be removed once we can be reasonably confident that few people
+@rem will be migrating directly to this new wrapper.
+goto afterSafetyNet
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+goto exitWithErrorLevel
+:afterSafetyNet
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,15 +70,16 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+1>&2 echo.
+1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+1>&2 echo.
+1>&2 echo Please set the JAVA_HOME variable in your environment to match the
+1>&2 echo location of your Java installation.
-goto fail
+"%COMSPEC%" /c exit 1
+goto exitWithErrorLevel
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -56,34 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+1>&2 echo.
+1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+1>&2 echo.
+1>&2 echo Please set the JAVA_HOME variable in your environment to match the
+1>&2 echo location of your Java installation.
-goto fail
+"%COMSPEC%" /c exit 1
+goto exitWithErrorLevel
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
-:omega
+@rem This label must not be changed. We rely on old scripts being able to jump to this point.
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/src/main/java/org/crebs/skpackets/SkPackets.java b/src/main/java/org/crebs/skpackets/SkPackets.java
index 7e828f7..51b60a0 100644
--- a/src/main/java/org/crebs/skpackets/SkPackets.java
+++ b/src/main/java/org/crebs/skpackets/SkPackets.java
@@ -1,7 +1,6 @@
package org.crebs.skpackets;
import com.github.shanebeee.skr.Registration;
-
import org.bukkit.plugin.java.JavaPlugin;
import org.crebs.skpackets.api.entity.PacketEntityManager;
import org.crebs.skpackets.api.simple.FakeBlockManager;
@@ -9,8 +8,11 @@
import org.crebs.skpackets.api.simple.FakeInventoryManager;
import org.crebs.skpackets.api.team.PacketTeamManager;
import org.crebs.skpackets.elements.ElementCounts;
-import org.crebs.skpackets.elements.ElementRegistration;
import org.crebs.skpackets.elements.ElementRuntime;
+import org.crebs.skpackets.elements.entity.EntityModule;
+import org.crebs.skpackets.elements.general.GeneralModule;
+import org.crebs.skpackets.elements.simple.SimpleModule;
+import org.crebs.skpackets.elements.team.TeamModule;
public final class SkPackets extends JavaPlugin {
private static SkPackets instance;
@@ -67,7 +69,7 @@ public void onEnable() {
} catch (ClassNotFoundException exception) {
getLogger()
.severe(
- "PacketEvents 2.14 requires Adventure 4. Use Paper 26.1.2; Adventure 5"
+ "PacketEvents 2.14 requires Adventure 4. This server is incompatible; Adventure 5"
+ " servers are not supported yet.");
getServer().getPluginManager().disablePlugin(this);
return;
@@ -81,7 +83,12 @@ public void onEnable() {
skinRefresh = new org.crebs.skpackets.api.simple.PlayerSkinRefresh(this);
fakeEquipment = new FakeEquipmentManager(this);
registration = new Registration(getName(), true);
- ElementRegistration.register(registration);
+ registration.getAddon().loadModules(
+ new GeneralModule(registration),
+ new SimpleModule(registration),
+ new EntityModule(registration),
+ new TeamModule(registration));
+ registration.finalizeRegistration();
ElementCounts.log(registration, getServer().getConsoleSender());
}
diff --git a/src/main/java/org/crebs/skpackets/api/entity/PacketEntityManager.java b/src/main/java/org/crebs/skpackets/api/entity/PacketEntityManager.java
index 34af143..3dff542 100644
--- a/src/main/java/org/crebs/skpackets/api/entity/PacketEntityManager.java
+++ b/src/main/java/org/crebs/skpackets/api/entity/PacketEntityManager.java
@@ -1,21 +1,23 @@
package org.crebs.skpackets.api.entity;
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
-
import me.tofaa.entitylib.wrapper.WrapperEntity;
import me.tofaa.entitylib.wrapper.WrapperLivingEntity;
-
import org.bukkit.Bukkit;
import org.bukkit.Location;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDeathEvent;
+import org.bukkit.event.entity.EntityRemoveEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.event.world.EntitiesUnloadEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.*;
@@ -26,6 +28,7 @@ private record Tracked(
WrapperEntity entity, UUID world, boolean persistent, Set viewers) {}
private final Map entities = new HashMap<>();
+ private final RealEntityPassengerManager realPassengers = new RealEntityPassengerManager();
private final JavaPlugin plugin;
private boolean closed;
@@ -139,9 +142,25 @@ public void removeViewer(WrapperEntity entity, UUID viewer) {
checkThread();
Tracked tracked = requireTracked(entity);
tracked.viewers().remove(viewer);
+ realPassengers.removeViewer(entity, viewer);
entity.removeViewer(viewer);
}
+ public void updateRealPassengers(Entity vehicle, Collection passengers, boolean mount) {
+ checkThread();
+ if (mount) {
+ for (WrapperEntity passenger : passengers) {
+ if (passenger.getRiding() != null) passenger.getRiding().removePassenger(passenger);
+ }
+ }
+ realPassengers.update(vehicle, passengers, mount);
+ }
+
+ public void removeRealPassenger(WrapperEntity passenger) {
+ checkThread();
+ realPassengers.remove(passenger);
+ }
+
public void teleport(WrapperEntity entity, Location location) {
checkThread();
Tracked tracked = requireTracked(entity);
@@ -152,6 +171,7 @@ public void teleport(WrapperEntity entity, Location location) {
UUID world = location.getWorld().getUID();
boolean changedWorld = !tracked.world().equals(world);
if (changedWorld) {
+ realPassengers.remove(entity);
for (UUID viewer : Set.copyOf(entity.getViewers())) {
entity.removeViewer(viewer);
}
@@ -177,6 +197,7 @@ public void remove(WrapperEntity entity) {
}
requireTracked(entity);
entities.remove(entity.getEntityId());
+ realPassengers.remove(entity);
entity.remove();
}
@@ -205,7 +226,11 @@ private void restore(Player player) {
if (closed || !player.isOnline()) {
return;
}
- entities.values().removeIf(tracked -> !tracked.entity().isSpawned());
+ entities.values().removeIf(tracked -> {
+ if (tracked.entity().isSpawned()) return false;
+ realPassengers.remove(tracked.entity());
+ return true;
+ });
for (Tracked tracked : entities.values()) {
show(tracked, player);
}
@@ -213,6 +238,7 @@ private void restore(Player player) {
}
private void hide(Player player) {
+ realPassengers.forgetViewer(player.getUniqueId());
for (Tracked tracked : entities.values()) {
tracked.entity().removeViewer(player.getUniqueId());
}
@@ -226,6 +252,8 @@ public void onJoin(PlayerJoinEvent event) {
@EventHandler
public void onQuit(PlayerQuitEvent event) {
UUID id = event.getPlayer().getUniqueId();
+ realPassengers.forgetViewer(id);
+ realPassengers.forgetVehicle(event.getPlayer());
for (var iterator = entities.values().iterator(); iterator.hasNext(); ) {
Tracked tracked = iterator.next();
tracked.entity().handleViewerDisconnect(id);
@@ -233,6 +261,7 @@ public void onQuit(PlayerQuitEvent event) {
&& tracked.viewers().remove(id)
&& tracked.viewers().isEmpty()) {
iterator.remove();
+ realPassengers.remove(tracked.entity());
tracked.entity().remove();
}
}
@@ -240,16 +269,33 @@ public void onQuit(PlayerQuitEvent event) {
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent event) {
+ realPassengers.forgetVehicle(event.getPlayer());
hide(event.getPlayer());
restore(event.getPlayer());
}
@EventHandler
public void onRespawn(PlayerRespawnEvent event) {
+ realPassengers.forgetVehicle(event.getPlayer());
hide(event.getPlayer());
restore(event.getPlayer());
}
+ @EventHandler
+ public void onDeath(EntityDeathEvent event) {
+ realPassengers.forgetVehicle(event.getEntity());
+ }
+
+ @EventHandler
+ public void onRemove(EntityRemoveEvent event) {
+ realPassengers.forgetVehicle(event.getEntity());
+ }
+
+ @EventHandler
+ public void onUnload(EntitiesUnloadEvent event) {
+ event.getEntities().forEach(realPassengers::forgetVehicle);
+ }
+
private void checkThread() {
if (!Bukkit.isPrimaryThread()) {
throw new IllegalStateException("Use the packet entity API on the server thread");
@@ -262,6 +308,7 @@ private void checkThread() {
@Override
public void close() {
checkThread();
+ realPassengers.clear();
for (Tracked tracked : entities.values()) {
tracked.entity().remove();
}
diff --git a/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java b/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java
new file mode 100644
index 0000000..b44bb37
--- /dev/null
+++ b/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java
@@ -0,0 +1,86 @@
+package org.crebs.skpackets.api.entity;
+
+import com.github.retrooper.packetevents.PacketEvents;
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers;
+import me.tofaa.entitylib.wrapper.WrapperEntity;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+
+import java.util.*;
+
+final class RealEntityPassengerManager {
+ private record Key(UUID viewer, UUID vehicle) {}
+ private record Mount(Entity vehicle, Map passengers) {}
+
+ private final Map mounts = new LinkedHashMap<>();
+
+ void update(Entity vehicle, Collection passengers, boolean mount) {
+ Map changed = new LinkedHashMap<>();
+ for (WrapperEntity passenger : passengers) {
+ for (UUID viewer : passenger.getViewers()) {
+ Key key = new Key(viewer, vehicle.getUniqueId());
+ if (mount) {
+ mounts.forEach((oldKey, oldMount) -> {
+ if (oldKey.viewer().equals(viewer) && !oldKey.equals(key)
+ && oldMount.passengers().remove(passenger.getEntityId(), passenger)) {
+ changed.put(oldKey, oldMount);
+ }
+ });
+ }
+ Mount current = mounts.computeIfAbsent(key, ignored -> new Mount(vehicle, new LinkedHashMap<>()));
+ if (mount) current.passengers().put(passenger.getEntityId(), passenger);
+ else current.passengers().remove(passenger.getEntityId(), passenger);
+ changed.put(key, current);
+ }
+ }
+ refresh(changed);
+ }
+
+ void remove(WrapperEntity passenger) {
+ Map changed = new LinkedHashMap<>();
+ mounts.forEach((key, mount) -> {
+ if (mount.passengers().remove(passenger.getEntityId(), passenger)) changed.put(key, mount);
+ });
+ refresh(changed);
+ }
+
+ void removeViewer(WrapperEntity passenger, UUID viewer) {
+ Map changed = new LinkedHashMap<>();
+ mounts.forEach((key, mount) -> {
+ if (key.viewer().equals(viewer) && mount.passengers().remove(passenger.getEntityId(), passenger)) {
+ changed.put(key, mount);
+ }
+ });
+ refresh(changed);
+ }
+
+ void forgetViewer(UUID viewer) {
+ mounts.keySet().removeIf(key -> key.viewer().equals(viewer));
+ }
+
+ void forgetVehicle(Entity vehicle) {
+ mounts.keySet().removeIf(key -> key.vehicle().equals(vehicle.getUniqueId()));
+ }
+
+ void clear() {
+ Map changed = new LinkedHashMap<>(mounts);
+ changed.values().forEach(mount -> mount.passengers().clear());
+ refresh(changed);
+ }
+
+ private void refresh(Map changed) {
+ changed.forEach((key, mount) -> {
+ mount.passengers().values().removeIf(passenger -> !passenger.isSpawned() || !passenger.getViewers().contains(key.viewer()));
+ Player viewer = Bukkit.getPlayer(key.viewer());
+ if (viewer != null && viewer.isOnline() && viewer.getWorld().equals(mount.vehicle().getWorld())) {
+ LinkedHashSet ids = new LinkedHashSet<>();
+ mount.vehicle().getPassengers().forEach(passenger -> ids.add(passenger.getEntityId()));
+ ids.addAll(mount.passengers().keySet());
+ PacketEvents.getAPI().getPlayerManager().sendPacket(viewer,
+ new WrapperPlayServerSetPassengers(mount.vehicle().getEntityId(), ids.stream().mapToInt(Integer::intValue).toArray()));
+ }
+ if (mount.passengers().isEmpty()) mounts.remove(key);
+ });
+ }
+}
diff --git a/src/main/java/org/crebs/skpackets/api/entity/SkinManager.java b/src/main/java/org/crebs/skpackets/api/entity/SkinManager.java
index 42346ac..83cfe7f 100644
--- a/src/main/java/org/crebs/skpackets/api/entity/SkinManager.java
+++ b/src/main/java/org/crebs/skpackets/api/entity/SkinManager.java
@@ -44,11 +44,13 @@ public class SkinManager {
if (user == null) return null;
List properties = user.getProfile().getTextureProperties();
- return new Skin(properties);
+ return properties.isEmpty() ? null : new Skin(properties);
}
public static @org.jetbrains.annotations.Nullable Skin getOfflinePlayer(final String playerName) {
+ if (playerName == null || playerName.isBlank()) return null;
+
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().equalsIgnoreCase(playerName)) {
return getPlayer(player);
@@ -61,7 +63,9 @@ public class SkinManager {
}
UUID uuid = MojangAPIUtil.requestPlayerUUID(playerName);
+ if (uuid == null) return null;
List properties = MojangAPIUtil.requestPlayerTextureProperties(uuid);
+ if (properties == null || properties.isEmpty()) return null;
skin = new Skin(properties);
skinCache.put(playerName, skin);
diff --git a/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java b/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java
index 9e49ce8..5493ef2 100644
--- a/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java
+++ b/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java
@@ -77,7 +77,9 @@ public static EntityType getType(int entityId) {
}
@Override
+ @SuppressWarnings("ConstantValue")
public void onPacketSend(PacketSendEvent event) {
+ if (event.getUser().getUUID() == null) return;
if (event.getPacketType() == PacketType.Play.Server.SPAWN_ENTITY) {
handleSpawn(event);
} else if (event.getPacketType() == PacketType.Play.Server.DESTROY_ENTITIES) {
@@ -110,7 +112,9 @@ private void handleDestroy(PacketSendEvent event) {
}
@Override
+ @SuppressWarnings("ConstantValue")
public void onUserDisconnect(UserDisconnectEvent event) {
- PLAYER_ENTITIES.remove(event.getUser().getUUID());
+ UUID userUuid = event.getUser().getUUID();
+ if (userUuid != null) PLAYER_ENTITIES.remove(userUuid);
}
}
diff --git a/src/main/java/org/crebs/skpackets/api/general/PlayerSkinRegistry.java b/src/main/java/org/crebs/skpackets/api/general/PlayerSkinRegistry.java
index a2a03cb..0604772 100644
--- a/src/main/java/org/crebs/skpackets/api/general/PlayerSkinRegistry.java
+++ b/src/main/java/org/crebs/skpackets/api/general/PlayerSkinRegistry.java
@@ -66,12 +66,11 @@ public void onQuit(PlayerQuitEvent event) {
skins.remove(event.getPlayer().getUniqueId());
}
- public static Skin getSkin(UUID ownerId) {
- Skin skin = skins.get(ownerId);
- if (skin == null) {
- throw new IllegalStateException("No Skin found for " + ownerId);
- }
+ public static void clear() {
+ skins.clear();
+ }
- return skin;
+ public static @org.jetbrains.annotations.Nullable Skin getSkin(UUID ownerId) {
+ return skins.get(ownerId);
}
}
diff --git a/src/main/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistry.java b/src/main/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistry.java
index f6ca430..b73ba70 100644
--- a/src/main/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistry.java
+++ b/src/main/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistry.java
@@ -22,48 +22,60 @@
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon;
-
import org.jetbrains.annotations.Nullable;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.BiConsumer;
+import java.util.*;
public class PacketTypeRegistry {
- private static final Map CLIENT_BOUND_PACKETS =
- new ConcurrentHashMap<>();
- private static final Map SERVER_BOUND_PACKETS =
- new ConcurrentHashMap<>();
+ private static final Map CLIENT_BOUND_PACKETS = new HashMap<>();
+ private static final Map SERVER_BOUND_PACKETS = new HashMap<>();
+ private static final Map PROTOCOL_STATES = new HashMap<>();
+ private static final List STATES = List.of("play", "configuration", "login", "handshaking", "status");
private static final List ALL_PACKETS;
static {
- BiConsumer