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 View skPackets documentation on SkDocs + + View skPackets documentation on SkriptHub + + +
-[Features](#features) · [Requirements](#requirements) · [Showcases](#showcases) · [Persistence](#persistence) · [Credits](#credits) +[![Features](https://img.shields.io/badge/Navigation-Features-007ACC?style=for-the-badge)](#features) +[![Requirements](https://img.shields.io/badge/Navigation-Requirements-007ACC?style=for-the-badge)](#requirements) +[![Showcases](https://img.shields.io/badge/Navigation-Showcases-007ACC?style=for-the-badge)](#showcases) +[![Persistence](https://img.shields.io/badge/Navigation-Persistence-007ACC?style=for-the-badge)](#persistence) +[![Credits](https://img.shields.io/badge/Navigation-Credits-007ACC?style=for-the-badge)](#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, PacketTypeCommon[]> populateMap = - (map, types) -> { - for (PacketTypeCommon type : types) { - map.put(type.getName().toUpperCase(Locale.ENGLISH), type); - } - }; + List packets = new ArrayList<>(); + register(CLIENT_BOUND_PACKETS, "play", PacketType.Play.Server.values(), packets); + register(CLIENT_BOUND_PACKETS, "configuration", PacketType.Configuration.Server.values(), packets); + register(CLIENT_BOUND_PACKETS, "login", PacketType.Login.Server.values(), packets); + register(CLIENT_BOUND_PACKETS, "handshaking", PacketType.Handshaking.Server.values(), packets); + register(CLIENT_BOUND_PACKETS, "status", PacketType.Status.Server.values(), packets); + register(SERVER_BOUND_PACKETS, "play", PacketType.Play.Client.values(), packets); + register(SERVER_BOUND_PACKETS, "configuration", PacketType.Configuration.Client.values(), packets); + register(SERVER_BOUND_PACKETS, "login", PacketType.Login.Client.values(), packets); + register(SERVER_BOUND_PACKETS, "handshaking", PacketType.Handshaking.Client.values(), packets); + register(SERVER_BOUND_PACKETS, "status", PacketType.Status.Client.values(), packets); + ALL_PACKETS = List.copyOf(packets); + } - populateMap.accept(CLIENT_BOUND_PACKETS, PacketType.Play.Server.values()); - populateMap.accept(CLIENT_BOUND_PACKETS, PacketType.Configuration.Server.values()); - populateMap.accept(CLIENT_BOUND_PACKETS, PacketType.Login.Server.values()); - populateMap.accept(CLIENT_BOUND_PACKETS, PacketType.Handshaking.Server.values()); - populateMap.accept(CLIENT_BOUND_PACKETS, PacketType.Status.Server.values()); + private static void register(Map registry, String state, PacketTypeCommon[] types, List packets) { + for (PacketTypeCommon type : types) { + String name = normalize(type.getName()); + registry.putIfAbsent(name, type); + registry.put(state + ":" + name, type); + PROTOCOL_STATES.put(type, state); + packets.add(type); + } + } - populateMap.accept(SERVER_BOUND_PACKETS, PacketType.Play.Client.values()); - populateMap.accept(SERVER_BOUND_PACKETS, PacketType.Configuration.Client.values()); - populateMap.accept(SERVER_BOUND_PACKETS, PacketType.Login.Client.values()); - populateMap.accept(SERVER_BOUND_PACKETS, PacketType.Handshaking.Client.values()); - populateMap.accept(SERVER_BOUND_PACKETS, PacketType.Status.Client.values()); + private static String normalize(String name) { + return name.trim().toLowerCase(Locale.ENGLISH).replaceAll("\\s+", "_"); + } - List packets = new ArrayList<>(); - packets.addAll(CLIENT_BOUND_PACKETS.values()); - packets.addAll(SERVER_BOUND_PACKETS.values()); - ALL_PACKETS = List.copyOf(packets); + private static @Nullable PacketTypeCommon lookup(Map registry, String rawName) { + String name = normalize(rawName); + PacketTypeCommon type = registry.get(name); + if (type != null) { + return type; + } + for (String state : STATES) { + String prefix = state + "_"; + if (name.startsWith(prefix)) { + return registry.get(state + ":" + name.substring(prefix.length())); + } + } + return null; } public static List getAllPackets() { @@ -71,24 +83,19 @@ public static List getAllPackets() { } public static @Nullable PacketTypeCommon getPacket(String rawName, boolean isSend) { - String key = rawName.replace(" ", "_").toUpperCase(Locale.ENGLISH); - - if (isSend) { - return CLIENT_BOUND_PACKETS.get(key); - } else { - return SERVER_BOUND_PACKETS.get(key); - } + return lookup(isSend ? CLIENT_BOUND_PACKETS : SERVER_BOUND_PACKETS, rawName); } public static @Nullable PacketTypeCommon getPacket(String rawName) { - String key = rawName.replace(" ", "_").toUpperCase(Locale.ENGLISH); - - PacketTypeCommon type = CLIENT_BOUND_PACKETS.get(key); + PacketTypeCommon type = getPacket(rawName, true); + return type != null ? type : getPacket(rawName, false); + } - if (type == null) { - type = SERVER_BOUND_PACKETS.get(key); + public static String getProtocolState(PacketTypeCommon type) { + String state = PROTOCOL_STATES.get(type); + if (state == null) { + throw new IllegalArgumentException("Unknown packet type: " + type); } - - return type; + return state; } } diff --git a/src/main/java/org/crebs/skpackets/api/simple/ChatSessionListener.java b/src/main/java/org/crebs/skpackets/api/simple/ChatSessionListener.java index e0c32e1..d7b7aab 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/ChatSessionListener.java +++ b/src/main/java/org/crebs/skpackets/api/simple/ChatSessionListener.java @@ -30,13 +30,13 @@ import org.jetbrains.annotations.Nullable; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.Map; import java.util.UUID; public class ChatSessionListener implements PacketListener { - private static final Map sessions = new HashMap<>(); + private static final Map sessions = new ConcurrentHashMap<>(); @Override public void onPacketSend(PacketSendEvent event) { @@ -55,11 +55,15 @@ public void onPacketSend(PacketSendEvent event) { } @Override + @SuppressWarnings("ConstantValue") public void onUserDisconnect(UserDisconnectEvent event) { - sessions.remove(event.getUser().getUUID()); + UUID ownerId = event.getUser().getUUID(); + if (ownerId != null) { + sessions.remove(ownerId); + } } public static @Nullable RemoteChatSession getChatSession(UUID ownerId) { - return sessions.getOrDefault(ownerId, null); + return ownerId == null ? null : sessions.get(ownerId); } } diff --git a/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java b/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java index 0be6710..c346806 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java +++ b/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java @@ -154,10 +154,11 @@ private void send(Player viewer, Position pos, BlockData data) { } @Override + @SuppressWarnings("ConstantValue") public void onPacketSend(@org.jetbrains.annotations.NotNull PacketSendEvent event) { if (closed || event.isCancelled()) return; UUID id = event.getUser().getUUID(); - if (!active.contains(id)) return; + if (id == null || !active.contains(id)) return; UUID world = worlds.get(id); if (world == null) return; if (event.getPacketType() == PacketType.Play.Server.CHUNK_DATA) { diff --git a/src/main/java/org/crebs/skpackets/api/simple/FakeInventoryManager.java b/src/main/java/org/crebs/skpackets/api/simple/FakeInventoryManager.java index 43e050d..418bca5 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/FakeInventoryManager.java +++ b/src/main/java/org/crebs/skpackets/api/simple/FakeInventoryManager.java @@ -57,6 +57,7 @@ public void onPacketSend(@org.jetbrains.annotations.NotNull PacketSendEvent even && event.getPacketType() != PacketType.Play.Server.SET_SLOT && event.getPacketType() != PacketType.Play.Server.SET_PLAYER_INVENTORY) return; UUID id = event.getUser().getUUID(); + if (id == null) return; if (event.getPacketType() == PacketType.Play.Server.SET_PLAYER_INVENTORY) { var packet = new WrapperPlayServerSetPlayerInventory(event); var values = overrides.get(new Key(id, 0)); @@ -231,6 +232,7 @@ public void onPacketReceive(@org.jetbrains.annotations.NotNull PacketReceiveEven || event.isCancelled() || event.getPacketType() != PacketType.Play.Client.CLICK_WINDOW) return; UUID id = event.getUser().getUUID(); + if (id == null) return; Window tracked = containers.get(id); if (!overrides.containsKey(new Key(id, 0)) && (tracked == null || !overrides.containsKey(new Key(id, tracked.id())))) return; diff --git a/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityListener.java b/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityListener.java index 6287f37..4ac4929 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityListener.java +++ b/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityListener.java @@ -22,14 +22,23 @@ import com.github.retrooper.packetevents.event.PacketListener; import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; -import org.crebs.skpackets.api.entity.metadata.EntityMeta; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityRemoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.world.EntitiesUnloadEvent; +import java.util.ArrayList; import java.util.UUID; -public class GlowingEntityListener implements PacketListener { +public class GlowingEntityListener implements PacketListener, Listener { @Override public void onPacketSend(@org.jetbrains.annotations.NotNull PacketSendEvent event) { @@ -42,17 +51,49 @@ public void onPacketSend(@org.jetbrains.annotations.NotNull PacketSendEvent even UUID playerUUID = event.getUser().getUUID(); int entityId = packet.getEntityId(); - if (GlowingEntityManager.isGlowingFor(entityId, playerUUID)) { - EntityMeta meta = - EntityMeta.createMeta( - packet.getEntityId(), - com.github.retrooper.packetevents.protocol.entity.type.EntityTypes - .ENTITY); - meta.readPacket(packet); - meta.entity().setFlag(me.tofaa.entitylib.meta.MetaFields.Entity.GLOWING, true); - - packet.setEntityMetadata(meta.entityData()); - event.markForReEncode(true); + if (!GlowingEntityManager.isGlowingFor(entityId, playerUUID)) { + return; + } + + var metadata = packet.getEntityMetadata(); + for (int i = 0; i < metadata.size(); i++) { + var data = metadata.get(i); + if (data.getIndex() != 0 || !(data.getValue() instanceof Byte flags)) { + continue; + } + if ((flags & 0x40) == 0) { + var updated = new ArrayList>(metadata); + updated.set(i, new EntityData<>(0, EntityDataTypes.BYTE, (byte) (flags | 0x40))); + packet.setEntityMetadata(updated); + event.markForReEncode(true); + } + return; } } + + @Override + public void onUserDisconnect(UserDisconnectEvent event) { + GlowingEntityManager.forgetViewer(event.getUser().getUUID()); + } + + @EventHandler + public void onEntityDeath(EntityDeathEvent event) { + GlowingEntityManager.forgetEntity(event.getEntity().getEntityId()); + } + + @EventHandler + public void onEntityRemove(EntityRemoveEvent event) { + GlowingEntityManager.forgetEntity(event.getEntity().getEntityId()); + } + + @EventHandler + public void onEntitiesUnload(EntitiesUnloadEvent event) { + event.getEntities().forEach(entity -> GlowingEntityManager.forgetEntity(entity.getEntityId())); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + GlowingEntityManager.forgetEntity(event.getPlayer().getEntityId()); + GlowingEntityManager.forgetViewer(event.getPlayer().getUniqueId()); + } } diff --git a/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityManager.java b/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityManager.java index 3bb451c..bec5403 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityManager.java +++ b/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityManager.java @@ -57,18 +57,20 @@ public static void clear() { } public static void setGlowingReceivers(int glowingEntityId, List glowingReceivers) { + if (glowingReceivers == null) { + return; + } Entity glowingEntity = getById(glowingEntityId); - if (glowingEntity == null || glowingReceivers == null) { + if (glowingEntity == null) { + glowingMap.remove(glowingEntityId); return; } Set oldReceivers = glowingMap.get(glowingEntityId); + Set toRemove = new HashSet<>(); if (oldReceivers != null) { - Set toRemove = new HashSet<>(oldReceivers); + toRemove.addAll(oldReceivers); glowingReceivers.forEach(toRemove::remove); - if (!toRemove.isEmpty()) { - sendGlowPacket(glowingEntity, toRemove, false); - } } Set newReceivers = new HashSet<>(glowingReceivers); @@ -84,6 +86,10 @@ public static void setGlowingReceivers(int glowingEntityId, List glowingRe glowingMap.put(glowingEntityId, receivers); } + if (!toRemove.isEmpty()) { + sendGlowPacket(glowingEntity, toRemove, false); + } + if (!newReceivers.isEmpty()) { sendGlowPacket(glowingEntity, newReceivers, true); } @@ -117,10 +123,9 @@ public static void addGlowingReceiver(int glowingEntity, UUID playerToAdd) { addGlowingReceivers(glowingEntity, Collections.singletonList(playerToAdd)); } - public static void removeGlowingReceivers( - int glowingEntityId, Collection playersToRemove) { + public static void removeGlowingReceivers(int glowingEntityId, Collection playersToRemove) { Entity glowingEntity = getById(glowingEntityId); - if (glowingEntity == null || playersToRemove == null || playersToRemove.isEmpty()) { + if (playersToRemove == null || playersToRemove.isEmpty()) { return; } @@ -156,6 +161,20 @@ public static void clearGlowingReceivers(int glowingEntityId) { } } + public static void forgetEntity(int entityId) { + glowingMap.remove(entityId); + } + + public static void forgetViewer(UUID viewerId) { + if (viewerId == null) { + return; + } + glowingMap.forEach((entityId, receivers) -> glowingMap.computeIfPresent(entityId, (id, current) -> { + current.remove(viewerId); + return current.isEmpty() ? null : current; + })); + } + public static boolean isGlowingFor(int entityId, UUID receiverId) { if (receiverId == null) { return false; diff --git a/src/main/java/org/crebs/skpackets/api/simple/PlayerSkinRefresh.java b/src/main/java/org/crebs/skpackets/api/simple/PlayerSkinRefresh.java index 3d7e3fe..d9c7ab9 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/PlayerSkinRefresh.java +++ b/src/main/java/org/crebs/skpackets/api/simple/PlayerSkinRefresh.java @@ -88,6 +88,7 @@ static void request(Player player) { public void onPacketSend(@org.jetbrains.annotations.NotNull PacketSendEvent event) { if (closed || event.isCancelled()) return; UUID id = event.getUser().getUUID(); + if (id == null) return; if (event.getPacketType() == PacketType.Play.Server.JOIN_GAME) { var packet = new WrapperPlayServerJoinGame(event); worldStates.put(id, new WorldState(packet.getWorldName(), packet.isDebug(), packet.isFlat())); @@ -116,6 +117,7 @@ public void onPacketReceive(@org.jetbrains.annotations.NotNull PacketReceiveEven || event.isCancelled() || event.getPacketType() != PacketType.Play.Client.TELEPORT_CONFIRM) return; UUID id = event.getUser().getUUID(); + if (id == null) return; int teleport = new WrapperPlayClientTeleportConfirm(event).getTeleportId(); Set ours = skinTeleports.get(id); if (ours != null && ours.remove(teleport)) { diff --git a/src/main/java/org/crebs/skpackets/api/util/field/FieldValues.java b/src/main/java/org/crebs/skpackets/api/util/field/FieldValues.java index 8015d84..b10fa5d 100644 --- a/src/main/java/org/crebs/skpackets/api/util/field/FieldValues.java +++ b/src/main/java/org/crebs/skpackets/api/util/field/FieldValues.java @@ -83,41 +83,38 @@ public static void set(T[] source, continue; } - Object newValue; - Class expected = accessor.expectedType(); - - if (expected.isArray()) { - Class componentType = expected.getComponentType(); - Object typedArray = Array.newInstance(componentType, delta.length); - for (int i = 0; i < delta.length; i++) { - Array.set(typedArray, i, delta[i]); - } - newValue = typedArray; - } else { - if (delta.length == 1) { - newValue = delta[0]; + try { + Object newValue; + Class expected = accessor.expectedType(); + + if (expected.isArray()) { + Class componentType = expected.getComponentType(); + Object typedArray = Array.newInstance(componentType, delta.length); + for (int i = 0; i < delta.length; i++) { + Array.set(typedArray, i, delta[i]); + } + newValue = typedArray; } else { - newValue = delta; + if (delta.length == 1) { + newValue = delta[0]; + } else { + newValue = delta; + } } - } - - boolean isCompatible = expected == Object.class || expected.isInstance(newValue); - if (!isCompatible && expected.isArray() && newValue.getClass().isArray()) { - isCompatible = true; - } - if (!isCompatible) { - Skript.warning( - "Cannot set the " + kind + " field '" - + accessor.name() - + "' to a value of type " - + newValue.getClass().getSimpleName() - + ". Expected type: " - + expected.getSimpleName()); - continue; - } + boolean isCompatible = expected == Object.class || expected.isInstance(newValue); + + if (!isCompatible) { + Skript.warning( + "Cannot set the " + kind + " field '" + + accessor.name() + + "' to a value of type " + + newValue.getClass().getSimpleName() + + ". Expected type: " + + expected.getSimpleName()); + continue; + } - try { ((BiConsumer) accessor.setter()).accept(entity, newValue); } catch (IllegalArgumentException exception) { Skript.error(exception.getMessage()); diff --git a/src/main/java/org/crebs/skpackets/elements/ElementRegistration.java b/src/main/java/org/crebs/skpackets/elements/ElementRegistration.java deleted file mode 100644 index 24c641e..0000000 --- a/src/main/java/org/crebs/skpackets/elements/ElementRegistration.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.crebs.skpackets.elements; - -import com.github.shanebeee.skr.Registration; - -import org.crebs.skpackets.elements.entity.EntityRegister; -import org.crebs.skpackets.elements.general.GeneralElementRegistration; -import org.crebs.skpackets.elements.simple.SimpleElementRegistration; -import org.crebs.skpackets.elements.team.TeamRegister; - -public final class ElementRegistration { - - public static void register(Registration registration) { - GeneralElementRegistration.register(registration); - SimpleElementRegistration.register(registration); - EntityRegister.register(registration); - TeamRegister.register(registration); - registration.finalizeRegistration(); - } -} diff --git a/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java b/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java index 28ab2d0..c3ddc04 100644 --- a/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java +++ b/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java @@ -49,11 +49,12 @@ public ElementRuntime(SkPackets plugin) { PacketEvents.getAPI() .getEventManager() .registerListener(new EntityTracker(), PacketListenerPriority.MONITOR)); + var glowingListener = new GlowingEntityListener(); + plugin.getServer().getPluginManager().registerEvents(glowingListener, plugin); listeners.add( PacketEvents.getAPI() .getEventManager() - .registerListener( - new GlowingEntityListener(), PacketListenerPriority.NORMAL)); + .registerListener(glowingListener, PacketListenerPriority.NORMAL)); listeners.add( PacketEvents.getAPI() .getEventManager() @@ -76,6 +77,7 @@ public void close() { } listeners.clear(); EntityTracker.clear(); + PlayerSkinRegistry.clear(); GlowingEntityManager.clear(); EntityLib.shutdown(); } diff --git a/src/main/java/org/crebs/skpackets/elements/RegistrationModule.java b/src/main/java/org/crebs/skpackets/elements/RegistrationModule.java new file mode 100644 index 0000000..71322d7 --- /dev/null +++ b/src/main/java/org/crebs/skpackets/elements/RegistrationModule.java @@ -0,0 +1,18 @@ +package org.crebs.skpackets.elements; + +import com.github.shanebeee.skr.Registration; +import org.skriptlang.skript.addon.AddonModule; + +public interface RegistrationModule extends AddonModule { + + default void register(Registration registration, Registrar... registrars) { + for (Registrar registrar : registrars) { + registrar.register(registration); + } + } + + @FunctionalInterface + interface Registrar { + void register(Registration registration); + } +} diff --git a/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java b/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java new file mode 100644 index 0000000..0011d4b --- /dev/null +++ b/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java @@ -0,0 +1,51 @@ +package org.crebs.skpackets.elements.entity; + +import com.github.shanebeee.skr.Registration; +import org.crebs.skpackets.elements.RegistrationModule; +import org.crebs.skpackets.elements.entity.effects.*; +import org.crebs.skpackets.elements.entity.expressions.*; +import org.crebs.skpackets.elements.entity.expressions.entity.FakeEntityExpressions; +import org.crebs.skpackets.elements.entity.expressions.meta.MetaExpressions; +import org.crebs.skpackets.elements.entity.expressions.metadata.NativeMetadataExpressions; +import org.crebs.skpackets.elements.entity.expressions.prop.ExprVisibleFakeEntities; +import org.crebs.skpackets.elements.entity.expressions.prop.living.ExprFakeLivingEntityAttribute; +import org.crebs.skpackets.elements.entity.field.entity.FakeEntityFieldRegistry; +import org.crebs.skpackets.elements.entity.field.meta.MetaFieldRegistry; +import org.crebs.skpackets.elements.entity.section.SecExprNewFakeEntity; +import org.crebs.skpackets.elements.entity.section.SecExprNewMeta; +import org.crebs.skpackets.elements.entity.types.PacketEntity; +import org.skriptlang.skript.addon.SkriptAddon; + +public record EntityModule(Registration registration) implements RegistrationModule { + + @Override + public String name() { + return "entity"; + } + + @Override + public void load(SkriptAddon addon) { + FakeEntityFieldRegistry.INSTANCE.registerAll(); + MetaFieldRegistry.INSTANCE.registerAll(); + register(registration, + Types::register, + PacketEntity::register, + SecExprNewFakeEntity::register, + SecExprNewMeta::register, + EffRideFakeEntity::register, + EffSpawnFakeEntity::register, + EffKillPacketEntity::register, + EffPacketEntityViewers::register, + EffTeleportPacketEntity::register, + ExprFakeLivingEntityAttribute::register, + FakeEntityExpressions::register, + MetaExpressions::register, + ExprVisibleFakeEntities::register, + ExprFakeEntitiesAll::register, + ExprFakeEntityFromId::register, + ExprFakeEntityFromUuid::register, + ExprEntityId::register, + ExprFakeEquipment::register, + NativeMetadataExpressions::register); + } +} diff --git a/src/main/java/org/crebs/skpackets/elements/entity/EntityRegister.java b/src/main/java/org/crebs/skpackets/elements/entity/EntityRegister.java deleted file mode 100644 index 5695d19..0000000 --- a/src/main/java/org/crebs/skpackets/elements/entity/EntityRegister.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.crebs.skpackets.elements.entity; - -import com.github.shanebeee.skr.Registration; -import org.crebs.skpackets.elements.entity.effects.*; -import org.crebs.skpackets.elements.entity.expressions.*; -import org.crebs.skpackets.elements.entity.expressions.entity.FakeEntityExpressions; -import org.crebs.skpackets.elements.entity.expressions.meta.MetaExpressions; -import org.crebs.skpackets.elements.entity.expressions.metadata.NativeMetadataExpressions; -import org.crebs.skpackets.elements.entity.expressions.prop.ExprVisibleFakeEntities; -import org.crebs.skpackets.elements.entity.expressions.prop.living.ExprFakeLivingEntityAttribute; -import org.crebs.skpackets.elements.entity.field.entity.FakeEntityFieldRegistry; -import org.crebs.skpackets.elements.entity.field.meta.MetaFieldRegistry; -import org.crebs.skpackets.elements.entity.section.SecExprNewFakeEntity; -import org.crebs.skpackets.elements.entity.section.SecExprNewMeta; -import org.crebs.skpackets.elements.entity.types.PacketEntity; - -public final class EntityRegister { - - public static void register(Registration registration) { - FakeEntityFieldRegistry.INSTANCE.registerAll(); - MetaFieldRegistry.INSTANCE.registerAll(); - registerTypes(registration); - registerSections(registration); - registerEffects(registration); - registerExpressions(registration); - } - - private static void registerSections(Registration registration) { - SecExprNewFakeEntity.register(registration); - SecExprNewMeta.register(registration); - } - - private static void registerEffects(Registration registration) { - EffRideFakeEntity.register(registration); - EffSpawnFakeEntity.register(registration); - EffKillPacketEntity.register(registration); - EffPacketEntityViewers.register(registration); - EffTeleportPacketEntity.register(registration); - } - - private static void registerExpressions(Registration registration) { - ExprFakeLivingEntityAttribute.register(registration); - FakeEntityExpressions.register(registration); - MetaExpressions.register(registration); - ExprVisibleFakeEntities.register(registration); - ExprFakeEntitiesAll.register(registration); - ExprFakeEntityFromId.register(registration); - ExprFakeEntityFromUuid.register(registration); - ExprEntityId.register(registration); - ExprFakeEquipment.register(registration); - NativeMetadataExpressions.register(registration); - } - - private static void registerTypes(Registration registration) { - Types.register(registration); - PacketEntity.register(registration); - } -} diff --git a/src/main/java/org/crebs/skpackets/elements/entity/effects/EffRideFakeEntity.java b/src/main/java/org/crebs/skpackets/elements/entity/effects/EffRideFakeEntity.java index 0245964..e5363e8 100644 --- a/src/main/java/org/crebs/skpackets/elements/entity/effects/EffRideFakeEntity.java +++ b/src/main/java/org/crebs/skpackets/elements/entity/effects/EffRideFakeEntity.java @@ -25,7 +25,6 @@ import ch.njol.skript.lang.SkriptParser; import ch.njol.util.Kleenean; -import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers; import com.github.shanebeee.skr.Registration; import me.tofaa.entitylib.wrapper.WrapperEntity; @@ -34,7 +33,7 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -import java.util.List; +import java.util.Arrays; public class EffRideFakeEntity extends Effect { @@ -102,6 +101,7 @@ protected void execute(Event event) { if (vehicle instanceof WrapperEntity fakeVehicle) { for (WrapperEntity passenger : passengers) { if (mount) { + org.crebs.skpackets.SkPackets.getInstance().getPacketEntities().removeRealPassenger(passenger); fakeVehicle.addPassenger(passenger); } else { fakeVehicle.removePassenger(passenger); @@ -111,32 +111,8 @@ protected void execute(Event event) { } if (vehicle instanceof Entity realVehicle) { - int vehicleId = realVehicle.getEntityId(); - - List realPassengers = realVehicle.getPassengers(); - int[] passengerIds; - - if (mount) { - passengerIds = new int[realPassengers.size() + passengers.length]; - for (int i = 0; i < realPassengers.size(); i++) { - passengerIds[i] = realPassengers.get(i).getEntityId(); - } - for (int i = 0; i < passengers.length; i++) { - passengerIds[realPassengers.size() + i] = passengers[i].getEntityId(); - } - } else { - passengerIds = new int[realPassengers.size()]; - for (int i = 0; i < realPassengers.size(); i++) { - passengerIds[i] = realPassengers.get(i).getEntityId(); - } - } - - WrapperPlayServerSetPassengers packet = - new WrapperPlayServerSetPassengers(vehicleId, passengerIds); - - for (WrapperEntity passenger : passengers) { - passenger.sendPacketToViewers(packet); - } + org.crebs.skpackets.SkPackets.getInstance().getPacketEntities() + .updateRealPassengers(realVehicle, Arrays.asList(passengers), mount); } } diff --git a/src/main/java/org/crebs/skpackets/elements/entity/expressions/entity/ExprFakeEntityEntityId.java b/src/main/java/org/crebs/skpackets/elements/entity/expressions/entity/ExprFakeEntityEntityId.java index d76ea0b..75efcef 100644 --- a/src/main/java/org/crebs/skpackets/elements/entity/expressions/entity/ExprFakeEntityEntityId.java +++ b/src/main/java/org/crebs/skpackets/elements/entity/expressions/entity/ExprFakeEntityEntityId.java @@ -37,7 +37,7 @@ public static void register(Registration registration) { .newPropertyExpression( ExprFakeEntityEntityId.class, Object.class, - "(fake|packet) entity [field] [entity] id", + "(fake entity [field] [entity] id|packet entity field [entity] id|packet entity entity id)", "packetentity") .name("Fake Entity - Entity ID") .description( diff --git a/src/main/java/org/crebs/skpackets/elements/entity/section/SecExprNewFakeEntity.java b/src/main/java/org/crebs/skpackets/elements/entity/section/SecExprNewFakeEntity.java index e77cb62..1ddca05 100644 --- a/src/main/java/org/crebs/skpackets/elements/entity/section/SecExprNewFakeEntity.java +++ b/src/main/java/org/crebs/skpackets/elements/entity/section/SecExprNewFakeEntity.java @@ -184,6 +184,7 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { + if (generic || this.type == null) return "a new fake entity"; String type = this.type.getName().getKey().toLowerCase(Locale.ENGLISH).replace("_", " "); return String.format("a new fake %s entity", type); } diff --git a/src/main/java/org/crebs/skpackets/elements/general/GeneralElementRegistration.java b/src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java similarity index 59% rename from src/main/java/org/crebs/skpackets/elements/general/GeneralElementRegistration.java rename to src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java index f5fa3c2..136b43f 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/GeneralElementRegistration.java +++ b/src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java @@ -21,6 +21,7 @@ package org.crebs.skpackets.elements.general; import com.github.shanebeee.skr.Registration; +import org.crebs.skpackets.elements.RegistrationModule; import org.crebs.skpackets.elements.general.condition.CondPacketTypeIsBound; import org.crebs.skpackets.elements.general.effects.EffCancelPacket; import org.crebs.skpackets.elements.general.effects.EffFetchSkin; @@ -34,51 +35,31 @@ import org.crebs.skpackets.elements.general.expressions.prop.ExprPlayerSkin; import org.crebs.skpackets.elements.general.field.PacketFieldRegistry; import org.crebs.skpackets.elements.general.section.SecExprNewPacket; +import org.skriptlang.skript.addon.SkriptAddon; -public final class GeneralElementRegistration { +public record GeneralModule(Registration registration) implements RegistrationModule { - public static void register(Registration registration) { - registerFields(); - registerTypes(registration); - registerConditions(registration); - registerEffects(registration); - registerEvents(registration); - registerSections(registration); - registerExpressions(registration); + @Override + public String name() { + return "general"; } - private static void registerFields() { + @Override + public void load(SkriptAddon addon) { PacketFieldRegistry.INSTANCE.registerAll(); - } - - private static void registerTypes(Registration registration) { - Types.register(registration); - } - - private static void registerConditions(Registration registration) { - CondPacketTypeIsBound.register(registration); - } - - private static void registerEffects(Registration registration) { - EffCancelPacket.register(registration); - EffFetchSkin.register(registration); - EffSendOrReceivePacket.register(registration); - } - - private static void registerEvents(Registration registration) { - EvtPacketSendOrReceive.register(registration); - EvtNettyPacket.register(registration); - } - - private static void registerSections(Registration registration) { - SecExprNewPacket.register(registration); - } - - private static void registerExpressions(Registration registration) { - ExprEntityId.register(registration); - ExprPacketField.register(registration); - ExprPacketPacketType.register(registration); - ExprPlayerSkin.register(registration); - ExprSkinFromValue.register(registration); + register(registration, + Types::register, + CondPacketTypeIsBound::register, + EffCancelPacket::register, + EffFetchSkin::register, + EffSendOrReceivePacket::register, + EvtPacketSendOrReceive::register, + EvtNettyPacket::register, + SecExprNewPacket::register, + ExprEntityId::register, + ExprPacketField::register, + ExprPacketPacketType::register, + ExprPlayerSkin::register, + ExprSkinFromValue::register); } } diff --git a/src/main/java/org/crebs/skpackets/elements/general/Types.java b/src/main/java/org/crebs/skpackets/elements/general/Types.java index 5743718..8c98e4a 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/Types.java +++ b/src/main/java/org/crebs/skpackets/elements/general/Types.java @@ -50,6 +50,44 @@ public class Types { + public static class PacketTypeParser extends Parser { + @Override + public @Nullable PacketTypeCommon parse(String input, ParseContext context) { + input = input.trim().toLowerCase(Locale.ENGLISH).replaceAll("\\s+", " "); + boolean clientbound; + String name; + if (input.startsWith("clientbound ")) { + clientbound = true; + name = input.substring("clientbound ".length()); + } else if (input.startsWith("serverbound ")) { + clientbound = false; + name = input.substring("serverbound ".length()); + } else { + return null; + } + PacketTypeCommon type = PacketTypeRegistry.getPacket(name, clientbound); + if (type == null && name.endsWith(" packet")) { + return PacketTypeRegistry.getPacket(name.substring(0, name.length() - " packet".length()).trim(), clientbound); + } + return type; + } + + @Override + public String toString(PacketTypeCommon type, int flags) { + String direction = type.getSide() == PacketSide.SERVER ? "clientbound" : "serverbound"; + String state = PacketTypeRegistry.getProtocolState(type); + return direction + " " + (state.equals("play") ? "" : state + " ") + + type.getName().toLowerCase(Locale.ENGLISH).replace('_', ' '); + } + + @Override + public String toVariableNameString(PacketTypeCommon type) { + String direction = type.getSide() == PacketSide.SERVER ? "clientbound" : "serverbound"; + return "packettype:" + direction + ":" + PacketTypeRegistry.getProtocolState(type) + ":" + + type.getName().toLowerCase(Locale.ENGLISH).replace('_', ' '); + } + } + @SuppressWarnings("UnstableApiUsage") public static void register(Registration reg) { reg.newType(PacketWrapper.class, "packet") @@ -91,7 +129,8 @@ public String toVariableNameString(PacketWrapper packet) { .user("packet ?types?") .name("General - Packet Type") .description( - "Represents a specific type of packet (e.g. clientbound chunk data packet)") + "Represents a specific packet type. Unqualified names prefer gameplay packets. " + + "Use a state qualifier for other phases, such as clientbound configuration keep alive packet.") .examples( """ on any packet: @@ -100,55 +139,7 @@ public String toVariableNameString(PacketWrapper packet) { """) .since("1.0.0") .supplier(() -> PacketTypeRegistry.getAllPackets().iterator()) - .parser( - new Parser<>() { - - @Override - public @Nullable PacketTypeCommon parse( - String input, ParseContext context) { - input = input.trim().toLowerCase(Locale.ENGLISH); - if (input.endsWith(" packet")) { - input = - input.substring(0, input.length() - " packet".length()) - .trim() - .toLowerCase(Locale.ENGLISH); - } - - boolean isClientBound; - String name; - - if (input.startsWith("clientbound ")) { - isClientBound = true; - name = input.substring("clientbound ".length()); - } else if (input.startsWith("serverbound ")) { - isClientBound = false; - name = input.substring("serverbound ".length()); - } else { - return null; - } - - return PacketTypeRegistry.getPacket(name, isClientBound); - } - - @Override - public String toString(PacketTypeCommon type, int flags) { - return (type.getSide().equals(PacketSide.SERVER) - ? "clientbound" - : "serverbound") - + " " - + type.getName() - .toLowerCase(Locale.ENGLISH) - .replace("_", " "); - } - - @Override - public String toVariableNameString(PacketTypeCommon type) { - return "packettype:" - + type.getName() - .toLowerCase(Locale.ENGLISH) - .replace("_", " "); - } - }) + .parser(new PacketTypeParser()) .register(); reg.newType(BlockEntityType.class, "blockentitytype") diff --git a/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityId.java b/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityId.java index 345df93..e830df9 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityId.java +++ b/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityId.java @@ -21,20 +21,19 @@ package org.crebs.skpackets.elements.general.expressions.prop; import ch.njol.skript.expressions.base.SimplePropertyExpression; - import com.github.shanebeee.skr.Registration; - +import me.tofaa.entitylib.wrapper.WrapperEntity; import org.bukkit.entity.Entity; import org.jetbrains.annotations.Nullable; -public class ExprEntityId extends SimplePropertyExpression { +public class ExprEntityId extends SimplePropertyExpression { public static void register(Registration reg) { reg.newPropertyExpression( ExprEntityId.class, Integer.class, "(protocol|packet) [entity][ ]id", - "entity") + "entity/packetentity") .name("General - Entity ID Of Entity") .description("Gets the entity id of an entity on the server") .examples( @@ -51,8 +50,12 @@ public static void register(Registration reg) { } @Override - public @Nullable Integer convert(Entity entity) { - return entity.getEntityId(); + public @Nullable Integer convert(Object entity) { + return switch (entity) { + case Entity real -> real.getEntityId(); + case WrapperEntity fake -> fake.getEntityId(); + default -> null; + }; } @Override diff --git a/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketField.java b/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketField.java index d254aa6..f13c8d8 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketField.java +++ b/src/main/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketField.java @@ -41,7 +41,9 @@ import org.crebs.skpackets.elements.general.field.PacketFieldRegistry; import org.jetbrains.annotations.Nullable; +import java.util.List; import java.util.Locale; +import java.util.Objects; @SuppressWarnings("rawtypes") public class ExprPacketField extends PropertyExpression { @@ -70,101 +72,47 @@ public static void register(Registration reg) { .register(); } - private FieldAccessor, ?> fieldAccessor; private String fieldName; - private boolean globalMode = false; + private List, ?>> accessors = List.of(); + private Class returnType = Object.class; @SuppressWarnings("unchecked") @Override - public boolean init( - Expression[] exprs, - int matchedPattern, - Kleenean isDelayed, - SkriptParser.ParseResult parseResult) { - String fieldName = parseResult.regexes.getFirst().group().trim(); - PacketSendOrReceiveParserData data = - getParser().getData(PacketSendOrReceiveParserData.class); - - if (getParser().isCurrentEvent(PacketSendOrReceiveEvent.class) - && data.getPacketType() != null) { - PacketTypeCommon eventPacketType = data.getPacketType(); - if (eventPacketType == null) { - return false; - } - - FieldSchema> schema = - PacketFieldRegistry.INSTANCE.getSchema(eventPacketType); - if (schema == null) { - Skript.error( - "No fields are currently registered for the " - + eventPacketType - .getName() - .toLowerCase(Locale.ENGLISH) - .replace("_", " ") - + " packet."); - return false; - } - - this.fieldAccessor = schema.getAccessor(fieldName); - if (this.fieldAccessor == null) { - Skript.error( - "The field '" - + fieldName - + "' does not exist in a " - + eventPacketType - .getName() - .toLowerCase(Locale.ENGLISH) - .replace("_", " ") - + " packet."); - return false; - } - - this.globalMode = false; - - } else { - boolean isValidField = false; - for (FieldSchema> def : - PacketFieldRegistry.INSTANCE.getAllSchemas()) { - FieldAccessor, ?> candidate = def.getAccessor(fieldName); - if (candidate != null) { - this.fieldAccessor = candidate; - isValidField = true; - break; - } - } - - if (!isValidField) { - Skript.error( - "The packet field '" - + fieldName - + "' is not registered or does not exist. Consider checking your" - + " spelling."); - return false; + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + fieldName = parseResult.regexes.getFirst().group().trim(); + accessors = PacketFieldRegistry.INSTANCE.getAllSchemas().stream() + ., ?>>map(schema -> schema.getAccessor(fieldName)) + .filter(Objects::nonNull) + .toList(); + if (accessors.isEmpty()) { + Skript.error("The packet field '" + fieldName + "' is not registered or does not exist. Consider checking your spelling."); + return false; + } + returnType = valueType(accessors.getFirst()); + for (var accessor : accessors) { + Class candidate = valueType(accessor); + while (!returnType.isAssignableFrom(candidate)) { + returnType = returnType.isInterface() ? Object.class : returnType.getSuperclass(); } - - this.fieldName = fieldName; - this.globalMode = true; } - setExpr((Expression) exprs[0]); return true; } + private static Class valueType(FieldAccessor accessor) { + Class expected = accessor.expectedType(); + Class type = expected.isArray() ? expected.getComponentType() : expected; + return Primitives.wrap(type); + } + @SuppressWarnings("UnstableApiUsage") private @Nullable FieldAccessor, ?> resolveAccessor(PacketWrapper wrapper) { - if (!globalMode) { - return this.fieldAccessor; - } PacketTypeCommon type = wrapper.getPacketTypeData().getPacketType(); if (type == null) { return null; } - FieldSchema> schema = - PacketFieldRegistry.INSTANCE.getSchema(type); - if (schema == null) { - return null; - } - return schema.getAccessor(this.fieldName); + FieldSchema> schema = PacketFieldRegistry.INSTANCE.getSchema(type); + return schema == null ? null : schema.getAccessor(fieldName); } @Override @@ -174,45 +122,31 @@ protected Object[] get(Event event, PacketWrapper[] source) { @Override public Class[] acceptChange(Changer.ChangeMode mode) { - if (this.fieldAccessor == null || this.fieldAccessor.setter() == null) { - String name = - globalMode - ? fieldName - : (this.fieldAccessor != null ? this.fieldAccessor.name() : "unknown"); - Skript.error("Cannot set " + name + " because it is a read-only field."); + if (accessors.stream().noneMatch(accessor -> accessor.setter() != null)) { + Skript.error("Cannot set " + fieldName + " because it is a read-only field."); return null; } - if (mode != Changer.ChangeMode.SET) { return null; } - - PacketSendOrReceiveParserData data = - getParser().getData(PacketSendOrReceiveParserData.class); - + PacketSendOrReceiveParserData data = getParser().getData(PacketSendOrReceiveParserData.class); if (getParser().isCurrentEvent(PacketSendOrReceiveEvent.class)) { if (data.getProcessType() != EvtPacketSendOrReceive.ProcessType.NETTY) { - Skript.error( - "You can't alter packets in a " - + (data.getProcessType() == null - ? "unknown" - : data.getProcessType() - .toString() - .toLowerCase(Locale.ENGLISH)) - + " processed event, the packets have already been processed at" - + " that point. Use a netty processed event instead."); + Skript.error("You can't alter packets in a " + + (data.getProcessType() == null ? "unknown" : data.getProcessType().toString().toLowerCase(Locale.ENGLISH)) + + " processed event, the packets have already been processed at that point. Use a netty processed event instead."); return null; - } else if (data.getPriority() == PacketListenerPriority.MONITOR) { - Skript.error( - "You can't alter packets when using the \"monitor\" listening priority."); + } + if (data.getPriority() == PacketListenerPriority.MONITOR) { + Skript.error("You can't alter packets when using the \"monitor\" listening priority."); return null; } } - - Class expected = this.fieldAccessor.expectedType(); - Class typeToAccept = expected.isArray() ? expected.getComponentType() : expected; - - return new Class[] {Primitives.wrap(typeToAccept)}; + return accessors.stream() + .filter(accessor -> accessor.setter() != null) + .map(ExprPacketField::valueType) + .distinct() + .toArray(Class[]::new); } @Override @@ -220,31 +154,22 @@ public void change(Event event, Object[] delta, Changer.ChangeMode mode) { if (mode != Changer.ChangeMode.SET || delta == null || delta.length == 0) { return; } - FieldValues.set(getExpr().getArray(event), this::resolveAccessor, delta, "packet"); } @Override public boolean isSingle() { - if (this.fieldAccessor == null) { - return true; - } - return getExpr().isSingle() && !this.fieldAccessor.expectedType().isArray(); + return getExpr().isSingle() && accessors.stream().noneMatch(accessor -> accessor.expectedType().isArray()); } @Override public Class getReturnType() { - if (this.fieldAccessor == null) { - return Object.class; - } - Class expected = this.fieldAccessor.expectedType(); - return expected.isArray() ? expected.getComponentType() : expected; + return returnType; } @Override public String toString(@Nullable Event event, boolean debug) { String wrapper = getExpr() != null ? getExpr().toString(event, debug) : "packet"; - String name = globalMode ? fieldName : this.fieldAccessor.name(); - return "packet field " + name + " of " + wrapper; + return "packet field " + fieldName + " of " + wrapper; } } diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldRegistry.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldRegistry.java index 13ae4c8..0cc4029 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldRegistry.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldRegistry.java @@ -56,7 +56,7 @@ private PacketFieldRegistry() { } } catch (ClassNotFoundException ignored) { LogUtil.error( - "SkBee not found, PacketEventsSK elements depending on NBT will not be" + "SkBee NBT API not found, skPackets elements depending on NBT will not be" + " registered"); } catch (Exception e) { throw new IllegalStateException("Failed to hook into SkBee NBT", e); diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrar.java b/src/main/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrar.java index 41e4b74..1cf000b 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrar.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrar.java @@ -87,7 +87,7 @@ public void register() { .requiredField( Boolean.class, w -> w.isSneaking().orElse(false), - WrapperPlayClientInteractEntity::setSneaking, + (w, sneaking) -> w.setSneaking(Boolean.TRUE.equals(sneaking)), "sneaking state", "sneaking") .constructor( diff --git a/src/main/java/org/crebs/skpackets/elements/simple/SimpleElementRegistration.java b/src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java similarity index 68% rename from src/main/java/org/crebs/skpackets/elements/simple/SimpleElementRegistration.java rename to src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java index bb10217..46cedd8 100644 --- a/src/main/java/org/crebs/skpackets/elements/simple/SimpleElementRegistration.java +++ b/src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java @@ -21,25 +21,26 @@ package org.crebs.skpackets.elements.simple; import com.github.shanebeee.skr.Registration; +import org.crebs.skpackets.elements.RegistrationModule; import org.crebs.skpackets.elements.simple.effects.*; import org.crebs.skpackets.elements.simple.expressions.ExprGlowReceivers; +import org.skriptlang.skript.addon.SkriptAddon; -public final class SimpleElementRegistration { +public record SimpleModule(Registration registration) implements RegistrationModule { - public static void register(Registration registration) { - registerEffects(registration); - registerExpressions(registration); + @Override + public String name() { + return "simple"; } - private static void registerEffects(Registration registration) { - EffDisplayedSkin.register(registration); - EffGlow.register(registration); - EffFakeBlock.register(registration); - EffFakeEquipment.register(registration); - EffFakeInventorySlot.register(registration); - } - - private static void registerExpressions(Registration registration) { - ExprGlowReceivers.register(registration); + @Override + public void load(SkriptAddon addon) { + register(registration, + EffDisplayedSkin::register, + EffGlow::register, + EffFakeBlock::register, + EffFakeEquipment::register, + EffFakeInventorySlot::register, + ExprGlowReceivers::register); } } diff --git a/src/main/java/org/crebs/skpackets/elements/simple/effects/EffGlow.java b/src/main/java/org/crebs/skpackets/elements/simple/effects/EffGlow.java index 4732db0..168e407 100644 --- a/src/main/java/org/crebs/skpackets/elements/simple/effects/EffGlow.java +++ b/src/main/java/org/crebs/skpackets/elements/simple/effects/EffGlow.java @@ -92,12 +92,15 @@ protected void execute(Event event) { Set viewerUuids; - if (viewers == null || viewers.length == 0) { + if (viewersExpr == null) { viewerUuids = Bukkit.getOnlinePlayers().stream() .map(Player::getUniqueId) .collect(Collectors.toSet()); } else { + if (viewers == null || viewers.length == 0) { + return; + } viewerUuids = Arrays.stream(viewers).map(Player::getUniqueId).collect(Collectors.toSet()); } diff --git a/src/main/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceivers.java b/src/main/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceivers.java index 18b3a84..242f7a4 100644 --- a/src/main/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceivers.java +++ b/src/main/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceivers.java @@ -138,6 +138,11 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { } } + @Override + public boolean isSingle() { + return false; + } + @Override public Class getReturnType() { return Player.class; diff --git a/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java b/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java new file mode 100644 index 0000000..cae8b47 --- /dev/null +++ b/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java @@ -0,0 +1,34 @@ +package org.crebs.skpackets.elements.team; + +import com.github.shanebeee.skr.Registration; +import org.crebs.skpackets.elements.RegistrationModule; +import org.crebs.skpackets.elements.team.effects.EffTeamMembers; +import org.crebs.skpackets.elements.team.expressions.*; +import org.crebs.skpackets.elements.team.section.SecNewPacketTeam; +import org.skriptlang.skript.addon.SkriptAddon; + +public record TeamModule(Registration registration) implements RegistrationModule { + + @Override + public String name() { + return "team"; + } + + @Override + public void load(SkriptAddon addon) { + register(registration, + EffTeamMembers::register, + SecNewPacketTeam::register, + ExprPacketTeam::register, + ExprTeamName::register, + ExprTeamEntries::register, + ExprTeamDisplayName::register, + ExprTeamPrefix::register, + ExprTeamSuffix::register, + ExprTeamColor::register, + ExprTeamNameTagVisibility::register, + ExprTeamCollisionRule::register, + ExprTeamFriendlyFire::register, + ExprTeamSeeInvisible::register); + } +} diff --git a/src/main/java/org/crebs/skpackets/elements/team/TeamRegister.java b/src/main/java/org/crebs/skpackets/elements/team/TeamRegister.java deleted file mode 100644 index 65ad5a2..0000000 --- a/src/main/java/org/crebs/skpackets/elements/team/TeamRegister.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.crebs.skpackets.elements.team; - -import com.github.shanebeee.skr.Registration; -import org.crebs.skpackets.elements.team.expressions.*; -import org.crebs.skpackets.elements.team.section.SecNewPacketTeam; - -public final class TeamRegister { - - public static void register(Registration registration) { - org.crebs.skpackets.elements.team.effects.EffTeamMembers.register(registration); - registerSections(registration); - registerExpressions(registration); - } - - private static void registerSections(Registration registration) { - SecNewPacketTeam.register(registration); - } - - private static void registerExpressions(Registration registration) { - ExprPacketTeam.register(registration); - ExprTeamName.register(registration); - ExprTeamEntries.register(registration); - ExprTeamDisplayName.register(registration); - ExprTeamPrefix.register(registration); - ExprTeamSuffix.register(registration); - ExprTeamColor.register(registration); - ExprTeamNameTagVisibility.register(registration); - ExprTeamCollisionRule.register(registration); - ExprTeamFriendlyFire.register(registration); - ExprTeamSeeInvisible.register(registration); - } -} diff --git a/src/main/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeam.java b/src/main/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeam.java index fabd24c..bf2e36b 100644 --- a/src/main/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeam.java +++ b/src/main/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeam.java @@ -2,16 +2,16 @@ import ch.njol.skript.Skript; import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.util.Kleenean; -import org.crebs.skpackets.elements.team.PacketTeam; import com.github.shanebeee.skr.Registration; import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; import org.crebs.skpackets.SkPackets; +import org.crebs.skpackets.elements.team.PacketTeam; import org.crebs.skpackets.elements.team.TeamEntityEntry; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; diff --git a/src/main/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpression.java b/src/main/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpression.java index d104441..0ac6483 100644 --- a/src/main/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpression.java +++ b/src/main/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpression.java @@ -2,9 +2,9 @@ import ch.njol.skript.Skript; import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.util.Kleenean; import com.github.retrooper.packetevents.wrapper.PacketWrapper; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams; diff --git a/src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java b/src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java new file mode 100644 index 0000000..a4c9828 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java @@ -0,0 +1,211 @@ +package org.crebs.skpackets.api.entity; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +class RealEntityPassengerManagerTest { + private record Sent(UUID viewer, int vehicle, int[] passengers) {} + + @BeforeAll + static void initializePacketEvents() { + PacketEventsTestBootstrap.initialize(); + } + + @Test + void sequentialMountsAndDismountKeepOtherFakeAndRealPassengers() { + try (Fixture fixture = new Fixture()) { + WrapperEntity first = fixture.passenger(11, fixture.player1Id); + WrapperEntity second = fixture.passenger(12, fixture.player1Id); + fixture.manager.update(fixture.vehicle, List.of(first), true); + fixture.assertLast(fixture.player1Id, 1, 99, 11); + fixture.manager.update(fixture.vehicle, List.of(second), true); + fixture.assertLast(fixture.player1Id, 1, 99, 11, 12); + fixture.manager.update(fixture.vehicle, List.of(first), false); + fixture.assertLast(fixture.player1Id, 1, 99, 12); + fixture.manager.update(fixture.vehicle, List.of(second), false); + fixture.assertLast(fixture.player1Id, 1, 99); + } + } + + @Test + void eachViewerReceivesOnlyItsVisiblePassengersWithoutDuplicatePackets() { + try (Fixture fixture = new Fixture()) { + WrapperEntity shared = fixture.passenger(11, fixture.player1Id, fixture.player2Id); + WrapperEntity privatePassenger = fixture.passenger(12, fixture.player1Id); + fixture.manager.update(fixture.vehicle, List.of(shared, privatePassenger), true); + assertEquals(2, fixture.sent.size()); + fixture.assertPacket(fixture.player1Id, 1, 99, 11, 12); + fixture.assertPacket(fixture.player2Id, 1, 99, 11); + fixture.sent.clear(); + fixture.manager.update(fixture.vehicle, List.of(shared), false); + assertEquals(2, fixture.sent.size()); + fixture.assertPacket(fixture.player1Id, 1, 99, 12); + fixture.assertPacket(fixture.player2Id, 1, 99); + } + } + + @Test + void repeatedMountDoesNotDuplicatePassenger() { + try (Fixture fixture = new Fixture()) { + WrapperEntity passenger = fixture.passenger(11, fixture.player1Id); + fixture.manager.update(fixture.vehicle, List.of(passenger), true); + fixture.manager.update(fixture.vehicle, List.of(passenger), true); + fixture.assertLast(fixture.player1Id, 1, 99, 11); + } + } + + @Test + void switchingVehiclesRestoresOldVehicleAndKeepsItsOtherRiders() { + try (Fixture fixture = new Fixture()) { + WrapperEntity first = fixture.passenger(11, fixture.player1Id); + WrapperEntity second = fixture.passenger(12, fixture.player1Id); + Entity otherVehicle = fixture.vehicle(2); + fixture.manager.update(fixture.vehicle, List.of(first, second), true); + fixture.sent.clear(); + fixture.manager.update(otherVehicle, List.of(first), true); + assertEquals(2, fixture.sent.size()); + fixture.assertPacket(fixture.player1Id, 1, 99, 12); + fixture.assertPacket(fixture.player1Id, 2, 11); + } + } + + @Test + void removingFakeEntityKeepsTheRemainingRider() { + try (Fixture fixture = new Fixture()) { + WrapperEntity first = fixture.passenger(11, fixture.player1Id); + WrapperEntity second = fixture.passenger(12, fixture.player1Id); + fixture.manager.update(fixture.vehicle, List.of(first, second), true); + fixture.manager.remove(first); + fixture.assertLast(fixture.player1Id, 1, 99, 12); + fixture.manager.clear(); + fixture.assertLast(fixture.player1Id, 1, 99); + } + } + + @Test + void removingViewerDoesNotChangeOtherViewersMounts() { + try (Fixture fixture = new Fixture()) { + WrapperEntity shared = fixture.passenger(11, fixture.player1Id, fixture.player2Id); + fixture.manager.update(fixture.vehicle, List.of(shared), true); + fixture.sent.clear(); + fixture.manager.removeViewer(shared, fixture.player1Id); + assertEquals(1, fixture.sent.size()); + fixture.assertLast(fixture.player1Id, 1, 99); + fixture.manager.clear(); + fixture.assertLast(fixture.player2Id, 1, 99); + } + } + + @Test + void discardedViewerAndVehicleStateDoesNotBringBackOldRiders() { + try (Fixture fixture = new Fixture()) { + WrapperEntity first = fixture.passenger(11, fixture.player1Id); + WrapperEntity second = fixture.passenger(12, fixture.player1Id); + fixture.manager.update(fixture.vehicle, List.of(first), true); + fixture.manager.forgetViewer(fixture.player1Id); + fixture.manager.update(fixture.vehicle, List.of(second), true); + fixture.assertLast(fixture.player1Id, 1, 99, 12); + fixture.manager.forgetVehicle(fixture.vehicle); + fixture.manager.update(fixture.vehicle, List.of(first), true); + fixture.assertLast(fixture.player1Id, 1, 99, 11); + } + } + + private static final class Fixture implements AutoCloseable { + final RealEntityPassengerManager manager = new RealEntityPassengerManager(); + final UUID player1Id = UUID.randomUUID(); + final UUID player2Id = UUID.randomUUID(); + final World world = mock(World.class); + final Entity vehicle = vehicle(1); + final List sent = new ArrayList<>(); + final MockedStatic bukkit; + final MockedStatic packets; + + Fixture() { + Player player1 = player(player1Id); + Player player2 = player(player2Id); + Entity realPassenger = mock(Entity.class); + when(realPassenger.getEntityId()).thenReturn(99); + when(vehicle.getPassengers()).thenReturn(List.of(realPassenger)); + PacketEventsAPI api = mock(PacketEventsAPI.class, RETURNS_DEEP_STUBS); + var settings = PacketEvents.getAPI().getSettings(); + var version = PacketEvents.getAPI().getServerManager().getVersion(); + when(api.getSettings()).thenReturn(settings); + when(api.getServerManager().getVersion()).thenReturn(version); + var playerManager = api.getPlayerManager(); + doAnswer(call -> { + Player viewer = call.getArgument(0); + WrapperPlayServerSetPassengers packet = call.getArgument(1); + sent.add(new Sent(viewer.getUniqueId(), packet.getEntityId(), packet.getPassengers())); + return null; + }).when(playerManager).sendPacket(any(Player.class), any(PacketWrapper.class)); + bukkit = mockStatic(Bukkit.class); + packets = mockStatic(PacketEvents.class); + bukkit.when(() -> Bukkit.getPlayer(player1Id)).thenReturn(player1); + bukkit.when(() -> Bukkit.getPlayer(player2Id)).thenReturn(player2); + packets.when(PacketEvents::getAPI).thenReturn(api); + } + + Player player(UUID id) { + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(id); + when(player.isOnline()).thenReturn(true); + when(player.getWorld()).thenReturn(world); + return player; + } + + Entity vehicle(int id) { + Entity vehicle = mock(Entity.class); + when(vehicle.getEntityId()).thenReturn(id); + when(vehicle.getUniqueId()).thenReturn(UUID.randomUUID()); + when(vehicle.getWorld()).thenReturn(world); + return vehicle; + } + + WrapperEntity passenger(int id, UUID... viewers) { + WrapperEntity passenger = mock(WrapperEntity.class); + when(passenger.getEntityId()).thenReturn(id); + when(passenger.getViewers()).thenReturn(Set.of(viewers)); + when(passenger.isSpawned()).thenReturn(true); + return passenger; + } + + void assertLast(UUID viewer, int vehicle, int... passengers) { + Sent packet = sent.getLast(); + assertEquals(viewer, packet.viewer()); + assertEquals(vehicle, packet.vehicle()); + assertArrayEquals(passengers, packet.passengers()); + } + + void assertPacket(UUID viewer, int vehicle, int... passengers) { + Sent packet = sent.stream().filter(value -> value.viewer().equals(viewer) && value.vehicle() == vehicle).findFirst().orElseThrow(); + assertArrayEquals(passengers, packet.passengers()); + } + + @Override + public void close() { + packets.close(); + bukkit.close(); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/api/entity/SkinManagerTest.java b/src/test/java/org/crebs/skpackets/api/entity/SkinManagerTest.java new file mode 100644 index 0000000..f75fb99 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/entity/SkinManagerTest.java @@ -0,0 +1,44 @@ +package org.crebs.skpackets.api.entity; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.util.MojangAPIUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.crebs.skpackets.api.general.PlayerSkinRegistry; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SkinManagerTest { + @Test + void missingPlayerOrTexturesReturnNoSkin() { + try (var bukkit = mockStatic(Bukkit.class); var mojang = mockStatic(MojangAPIUtil.class)) { + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of()); + assertNull(SkinManager.getOfflinePlayer(null)); + assertNull(SkinManager.getOfflinePlayer(" ")); + assertNull(SkinManager.getOfflinePlayer("missingPlayer")); + mojang.verify(() -> MojangAPIUtil.requestPlayerTextureProperties(null), never()); + UUID id = UUID.randomUUID(); + mojang.when(() -> MojangAPIUtil.requestPlayerUUID("noTextures")).thenReturn(id); + mojang.when(() -> MojangAPIUtil.requestPlayerTextureProperties(id)).thenReturn(List.of()); + assertNull(SkinManager.getOfflinePlayer("noTextures")); + } + } + + @Test + void onlinePlayerWithoutTexturesAndUncachedOriginalSkinReturnNoSkin() { + PacketEventsAPI api = mock(PacketEventsAPI.class, RETURNS_DEEP_STUBS); + Player player = mock(Player.class); + when(api.getPlayerManager().getUser(player).getProfile().getTextureProperties()).thenReturn(List.of()); + try (var packets = mockStatic(PacketEvents.class)) { + packets.when(PacketEvents::getAPI).thenReturn(api); + assertNull(SkinManager.getPlayer(player)); + } + assertNull(PlayerSkinRegistry.getSkin(UUID.randomUUID())); + } +} diff --git a/src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java b/src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java new file mode 100644 index 0000000..5ffbf31 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java @@ -0,0 +1,102 @@ +package org.crebs.skpackets.api.general.packet; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import org.crebs.skpackets.api.simple.PlayerSkinRefresh; +import org.crebs.skpackets.api.simple.FakeInventoryManager; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import org.bukkit.Bukkit; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.crebs.skpackets.api.general.EntityTracker; +import org.crebs.skpackets.api.simple.ChatSessionListener; +import org.crebs.skpackets.api.simple.FakeBlockManager; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class PacketEventsNullUuidTest { + @BeforeAll + static void initializePacketEvents() { + PacketEventsTestBootstrap.initialize(); + } + + @SuppressWarnings("DataFlowIssue") + private User connectingUser() throws ReflectiveOperationException { + User user = mock(User.class, CALLS_REAL_METHODS); + var profile = User.class.getDeclaredField("profile"); + profile.setAccessible(true); + profile.set(user, new UserProfile(null, null)); + return user; + } + + @Test + void unassignedPacketEventsProfileCanDisconnectWithoutCrashingCaches() throws ReflectiveOperationException { + User user = connectingUser(); + assertNull(user.getUUID()); + UserDisconnectEvent disconnect = new UserDisconnectEvent(user); + assertDoesNotThrow(() -> new ChatSessionListener().onUserDisconnect(disconnect)); + assertDoesNotThrow(() -> new EntityTracker().onUserDisconnect(disconnect)); + } + + @Test + void skinRefreshIgnoresPacketsWithoutAssignedUuid() throws ReflectiveOperationException { + User user = connectingUser(); + PacketEventsAPI api = mock(PacketEventsAPI.class, RETURNS_DEEP_STUBS); + try (var packets = mockStatic(PacketEvents.class); var bukkit = mockStatic(Bukkit.class)) { + packets.when(PacketEvents::getAPI).thenReturn(api); + bukkit.when(Bukkit::getPluginManager).thenReturn(mock(PluginManager.class)); + FakeInventoryManager inventory = new FakeInventoryManager(mock(JavaPlugin.class)); + PacketSendEvent inventorySent = mock(PacketSendEvent.class); + when(inventorySent.getUser()).thenReturn(user); + when(inventorySent.getPacketType()).thenReturn(PacketType.Play.Server.WINDOW_ITEMS); + assertDoesNotThrow(() -> inventory.onPacketSend(inventorySent)); + PacketReceiveEvent inventoryReceived = mock(PacketReceiveEvent.class); + when(inventoryReceived.getUser()).thenReturn(user); + when(inventoryReceived.getPacketType()).thenReturn(PacketType.Play.Client.CLICK_WINDOW); + assertDoesNotThrow(() -> inventory.onPacketReceive(inventoryReceived)); + inventory.close(); + PlayerSkinRefresh refresh = new PlayerSkinRefresh(mock(JavaPlugin.class)); + try { + PacketSendEvent sent = mock(PacketSendEvent.class); + when(sent.getUser()).thenReturn(user); + when(sent.getPacketType()).thenReturn(PacketType.Play.Server.JOIN_GAME); + assertDoesNotThrow(() -> refresh.onPacketSend(sent)); + PacketReceiveEvent received = mock(PacketReceiveEvent.class); + when(received.getUser()).thenReturn(user); + when(received.getPacketType()).thenReturn(PacketType.Play.Client.TELEPORT_CONFIRM); + assertDoesNotThrow(() -> refresh.onPacketReceive(received)); + } finally { + refresh.close(); + } + } + } + + @Test + void packetListenersIgnoreUnassignedProfilesBeforeLookingUpUuidMaps() throws ReflectiveOperationException { + User user = connectingUser(); + PacketSendEvent event = mock(PacketSendEvent.class); + when(event.getUser()).thenReturn(user); + assertDoesNotThrow(() -> new EntityTracker().onPacketSend(event)); + PacketEventsAPI api = mock(PacketEventsAPI.class, RETURNS_DEEP_STUBS); + PluginManager plugins = mock(PluginManager.class); + JavaPlugin plugin = mock(JavaPlugin.class); + try (var packets = mockStatic(PacketEvents.class); var bukkit = mockStatic(Bukkit.class)) { + packets.when(PacketEvents::getAPI).thenReturn(api); + bukkit.when(Bukkit::getPluginManager).thenReturn(plugins); + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of()); + FakeBlockManager manager = new FakeBlockManager(plugin); + assertDoesNotThrow(() -> manager.onPacketSend(event)); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistryTest.java b/src/test/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistryTest.java new file mode 100644 index 0000000..1867f7e --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistryTest.java @@ -0,0 +1,109 @@ +package org.crebs.skpackets.api.general.packet; + +import ch.njol.skript.lang.ParseContext; +import com.github.retrooper.packetevents.protocol.PacketSide; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import org.crebs.skpackets.elements.general.Types.PacketTypeParser; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class PacketTypeRegistryTest { + private final PacketTypeParser parser = new PacketTypeParser(); + + @BeforeAll + static void initializePacketEvents() { + PacketEventsTestBootstrap.initialize(); + } + + @Test + void unqualifiedNamesResolveEveryGameplayPacketInItsDirection() { + for (PacketTypeCommon type : PacketType.Play.Server.values()) { + assertSame(type, PacketTypeRegistry.getPacket(type.getName(), true), type.getName()); + } + for (PacketTypeCommon type : PacketType.Play.Client.values()) { + assertSame(type, PacketTypeRegistry.getPacket(type.getName(), false), type.getName()); + } + } + + @Test + void explicitStateQualifiersResolveEveryProtocolVariant() { + assertState("play", PacketType.Play.Server.values()); + assertState("play", PacketType.Play.Client.values()); + assertState("configuration", PacketType.Configuration.Server.values()); + assertState("configuration", PacketType.Configuration.Client.values()); + assertState("login", PacketType.Login.Server.values()); + assertState("login", PacketType.Login.Client.values()); + assertState("handshaking", PacketType.Handshaking.Server.values()); + assertState("handshaking", PacketType.Handshaking.Client.values()); + assertState("status", PacketType.Status.Server.values()); + assertState("status", PacketType.Status.Client.values()); + } + + @Test + void allPacketTypesKeepEveryDirectionAndProtocolStateExactlyOnce() { + List expected = new ArrayList<>(); + expected.addAll(Arrays.asList(PacketType.Play.Server.values())); + expected.addAll(Arrays.asList(PacketType.Play.Client.values())); + expected.addAll(Arrays.asList(PacketType.Configuration.Server.values())); + expected.addAll(Arrays.asList(PacketType.Configuration.Client.values())); + expected.addAll(Arrays.asList(PacketType.Login.Server.values())); + expected.addAll(Arrays.asList(PacketType.Login.Client.values())); + expected.addAll(Arrays.asList(PacketType.Handshaking.Server.values())); + expected.addAll(Arrays.asList(PacketType.Handshaking.Client.values())); + expected.addAll(Arrays.asList(PacketType.Status.Server.values())); + expected.addAll(Arrays.asList(PacketType.Status.Client.values())); + assertEquals(expected.size(), PacketTypeRegistry.getAllPackets().size()); + assertEquals(new HashSet<>(expected), new HashSet<>(PacketTypeRegistry.getAllPackets())); + } + + @Test + void displayNamesRoundTripEveryPacketTypeWithoutChangingGameplayNames() { + assertEquals("clientbound keep alive", parser.toString(PacketType.Play.Server.KEEP_ALIVE, 0)); + assertEquals("serverbound keep alive", parser.toString(PacketType.Play.Client.KEEP_ALIVE, 0)); + assertEquals("clientbound configuration keep alive", parser.toString(PacketType.Configuration.Server.KEEP_ALIVE, 0)); + for (PacketTypeCommon type : PacketTypeRegistry.getAllPackets()) { + String text = parser.toString(type, 0); + assertSame(type, parser.parse(text, ParseContext.DEFAULT), text); + assertSame(type, parser.parse(text + " packet", ParseContext.DEFAULT), text); + } + } + + @Test + void variableKeysDistinguishEveryDirectionAndProtocolState() { + var keys = new HashSet(); + for (PacketTypeCommon type : PacketTypeRegistry.getAllPackets()) { + String key = parser.toVariableNameString(type); + assertTrue(keys.add(key), "Duplicate packet type variable key: " + key); + } + assertEquals("packettype:clientbound:play:keep alive", parser.toVariableNameString(PacketType.Play.Server.KEEP_ALIVE)); + assertEquals("packettype:serverbound:play:keep alive", parser.toVariableNameString(PacketType.Play.Client.KEEP_ALIVE)); + assertEquals("packettype:clientbound:configuration:keep alive", parser.toVariableNameString(PacketType.Configuration.Server.KEEP_ALIVE)); + } + + @Test + void existingUniqueNamesAndNormalizedInputRemainSupported() { + assertSame(PacketType.Login.Server.LOGIN_SUCCESS, PacketTypeRegistry.getPacket("login success", true)); + assertSame(PacketType.Play.Server.KEEP_ALIVE, parser.parse(" CLIENTBOUND KEEP ALIVE PACKET ", ParseContext.DEFAULT)); + assertSame(PacketType.Configuration.Client.KEEP_ALIVE, parser.parse("serverbound configuration keep alive packet", ParseContext.DEFAULT)); + assertSame(PacketType.Play.Server.KEEP_ALIVE, PacketTypeRegistry.getPacket("keep alive")); + assertNull(parser.parse("keep alive", ParseContext.DEFAULT)); + assertNull(parser.parse("clientbound unknown keep alive", ParseContext.DEFAULT)); + assertNull(PacketTypeRegistry.getPacket("configuration not a real packet", true)); + } + + private void assertState(String state, PacketTypeCommon[] types) { + for (PacketTypeCommon type : types) { + String name = state + " " + type.getName().replace('_', ' '); + assertSame(type, PacketTypeRegistry.getPacket(name, type.getSide() == PacketSide.SERVER), name); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java b/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java new file mode 100644 index 0000000..084cf0b --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java @@ -0,0 +1,97 @@ +package org.crebs.skpackets.api.simple; + +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.event.UserDisconnectEvent; +import com.github.retrooper.packetevents.protocol.chat.RemoteChatSession; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoUpdate; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoUpdate.PlayerInfo; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ChatSessionListenerTest { + @BeforeAll + static void initializePacketEvents() { + PacketEventsTestBootstrap.initialize(); + } + + @Test + void concurrentProfileUpdatesRetainEverySessionAndDisconnectRemovesThem() throws Exception { + int workers = 4; + int profilesPerWorker = 250; + List> owners = new ArrayList<>(); + for (int worker = 0; worker < workers; worker++) { + List ids = new ArrayList<>(); + for (int profile = 0; profile < profilesPerWorker; profile++) { + ids.add(UUID.randomUUID()); + } + owners.add(ids); + } + RemoteChatSession session = mock(RemoteChatSession.class); + var barrier = new CyclicBarrier(workers); + try (var pool = Executors.newFixedThreadPool(workers)) { + List> updates = new ArrayList<>(); + for (List ids : owners) { + updates.add(pool.submit(() -> { + PacketSendEvent event = mock(PacketSendEvent.class); + when(event.getPacketType()).thenReturn(PacketType.Play.Server.PLAYER_INFO_UPDATE); + PlayerInfo entry = mock(PlayerInfo.class); + AtomicReference owner = new AtomicReference<>(); + when(entry.getGameProfile()).thenAnswer(call -> new UserProfile(owner.get(), "player1", List.of())); + when(entry.getChatSession()).thenReturn(session); + try (var ignored = mockConstruction(WrapperPlayServerPlayerInfoUpdate.class, (packet, context) -> when(packet.getEntries()).thenReturn(List.of(entry)))) { + barrier.await(10, TimeUnit.SECONDS); + ChatSessionListener listener = new ChatSessionListener(); + for (UUID id : ids) { + owner.set(id); + listener.onPacketSend(event); + } + } + return null; + })); + } + for (Future update : updates) { + update.get(30, TimeUnit.SECONDS); + } + for (List ids : owners) { + for (UUID id : ids) { + assertSame(session, ChatSessionListener.getChatSession(id)); + } + } + } finally { + UserDisconnectEvent disconnect = mock(UserDisconnectEvent.class, RETURNS_DEEP_STUBS); + AtomicReference owner = new AtomicReference<>(); + when(disconnect.getUser().getUUID()).thenAnswer(call -> owner.get()); + ChatSessionListener listener = new ChatSessionListener(); + for (List ids : owners) { + for (UUID id : ids) { + owner.set(id); + listener.onUserDisconnect(disconnect); + assertNull(ChatSessionListener.getChatSession(id)); + } + } + } + } + + @Test + void disconnectWithoutCachedSessionDoesNotThrow() { + UserDisconnectEvent disconnect = mock(UserDisconnectEvent.class, RETURNS_DEEP_STUBS); + when(disconnect.getUser().getUUID()).thenReturn(UUID.randomUUID()); + assertDoesNotThrow(() -> new ChatSessionListener().onUserDisconnect(disconnect)); + assertNull(ChatSessionListener.getChatSession(null)); + } +} diff --git a/src/test/java/org/crebs/skpackets/api/simple/GlowingEntityListenerTest.java b/src/test/java/org/crebs/skpackets/api/simple/GlowingEntityListenerTest.java new file mode 100644 index 0000000..5703da3 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/simple/GlowingEntityListenerTest.java @@ -0,0 +1,81 @@ +package org.crebs.skpackets.api.simple; + +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GlowingEntityListenerTest { + @BeforeAll + static void initializePacketEvents() { + PacketEventsTestBootstrap.initialize(); + } + + @Test + void sparseMetadataDoesNotInjectOrResetEntityFlags() { + List> metadata = List.of(new EntityData<>(1, EntityDataTypes.INT, 300)); + var result = intercept(metadata); + assertSame(metadata, result.metadata()); + assertFalse(result.reencoded()); + } + + @Test + void addingGlowPreservesOtherFlagsAndUnrelatedMetadata() { + EntityData other = new EntityData<>(1, EntityDataTypes.INT, 300); + List> metadata = List.of(new EntityData<>(0, EntityDataTypes.BYTE, (byte) 0x21), other); + var result = intercept(metadata); + assertEquals((byte) 0x61, result.metadata().getFirst().getValue()); + assertSame(other, result.metadata().get(1)); + assertEquals((byte) 0x21, metadata.getFirst().getValue()); + assertTrue(result.reencoded()); + } + + @Test + void packetsAlreadyContainingGlowAreNotRewritten() { + List> metadata = List.of(new EntityData<>(0, EntityDataTypes.BYTE, (byte) 0x61)); + var result = intercept(metadata); + assertSame(metadata, result.metadata()); + assertFalse(result.reencoded()); + } + + private record Result(List> metadata, boolean reencoded) {} + + private Result intercept(List> original) { + PacketSendEvent event = mock(PacketSendEvent.class, RETURNS_DEEP_STUBS); + UUID player1Id = UUID.randomUUID(); + when(event.getPacketType()).thenReturn(PacketType.Play.Server.ENTITY_METADATA); + when(event.getUser().getUUID()).thenReturn(player1Id); + AtomicReference>> metadata = new AtomicReference<>(original); + List reencoded = new ArrayList<>(); + doAnswer(call -> { + reencoded.add(call.getArgument(0)); + return null; + }).when(event).markForReEncode(anyBoolean()); + try (var glowing = mockStatic(GlowingEntityManager.class); + var ignored = mockConstruction(WrapperPlayServerEntityMetadata.class, (packet, context) -> { + when(packet.getEntityId()).thenReturn(42); + when(packet.getEntityMetadata()).thenAnswer(call -> metadata.get()); + doAnswer(call -> { + metadata.set(call.getArgument(0)); + return null; + }).when(packet).setEntityMetadata(anyList()); + })) { + glowing.when(GlowingEntityManager::hasGlowingEntities).thenReturn(true); + glowing.when(() -> GlowingEntityManager.isGlowingFor(42, player1Id)).thenReturn(true); + new GlowingEntityListener().onPacketSend(event); + } + return new Result(metadata.get(), reencoded.contains(true)); + } +} diff --git a/src/test/java/org/crebs/skpackets/api/simple/GlowingEntityManagerTest.java b/src/test/java/org/crebs/skpackets/api/simple/GlowingEntityManagerTest.java new file mode 100644 index 0000000..b0dd527 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/simple/GlowingEntityManagerTest.java @@ -0,0 +1,233 @@ +package org.crebs.skpackets.api.simple; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import me.tofaa.entitylib.EntityLib; +import me.tofaa.entitylib.meta.OwnershipCheck; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GlowingEntityManagerTest { + private record SentGlow(UUID viewer, boolean glowing) {} + + @BeforeAll + static void initializePacketEvents() throws ClassNotFoundException { + PacketEventsTestBootstrap.initialize(); + Class.forName("me.tofaa.entitylib.meta.MetaFields$Entity"); + } + + @AfterEach + void clearViewers() { + GlowingEntityManager.clear(); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void switchingViewersThenClearingRestoresEachPlayersGlow(boolean naturallyGlowing) { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(naturallyGlowing, sent, player1Id, player2Id, () -> { + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + assertEquals(List.of(new SentGlow(player1Id, true)), sent); + + sent.clear(); + GlowingEntityManager.setGlowingReceivers(42, List.of(player2Id)); + assertEquals(List.of( + new SentGlow(player1Id, naturallyGlowing), + new SentGlow(player2Id, true)), sent); + assertFalse(GlowingEntityManager.isGlowingFor(42, player1Id)); + assertTrue(GlowingEntityManager.isGlowingFor(42, player2Id)); + + sent.clear(); + GlowingEntityManager.clearGlowingReceivers(42); + assertEquals(List.of(new SentGlow(player2Id, naturallyGlowing)), sent); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + }); + } + + @Test + void settingEmptyViewersRemovesGlowBeforeOutgoingListenersRun() { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(false, sent, player1Id, player2Id, () -> { + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + sent.clear(); + GlowingEntityManager.setGlowingReceivers(42, List.of()); + assertEquals(List.of(new SentGlow(player1Id, false)), sent); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + }); + } + + @Test + void removingViewersAfterTheEntityDisappearsStillClearsState() { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(false, sent, player1Id, player2Id, () -> { + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + sent.clear(); + when(Bukkit.getWorlds()).thenReturn(List.of()); + GlowingEntityManager.removeGlowingReceivers(42, List.of(player1Id)); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + assertTrue(sent.isEmpty()); + }); + } + + @Test + void settingViewersAfterTheEntityDisappearsDiscardsStaleState() { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(false, sent, player1Id, player2Id, () -> { + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + when(Bukkit.getWorlds()).thenReturn(List.of()); + GlowingEntityManager.setGlowingReceivers(42, List.of()); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + }); + } + + @Test + void disconnectingViewersArePrunedWithoutRemovingOtherViewers() { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(false, sent, player1Id, player2Id, () -> { + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id, player2Id)); + GlowingEntityManager.forgetViewer(player1Id); + assertEquals(List.of(player2Id), GlowingEntityManager.getGlowingReceivers(42)); + GlowingEntityManager.forgetViewer(player2Id); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + }); + } + + @Test + void nonLivingEntityRemovalClearsGlowRegistrations() { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(false, sent, player1Id, player2Id, () -> { + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(42); + var removed = new org.bukkit.event.entity.EntityRemoveEvent(target, org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); + new GlowingEntityListener().onEntityRemove(removed); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + }); + } + + @Test + void entityDeathAndUnloadClearRegistrations() { + List sent = new ArrayList<>(); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + withPlayers(false, sent, player1Id, player2Id, () -> { + var listener = new GlowingEntityListener(); + var death = mock(org.bukkit.event.entity.EntityDeathEvent.class, RETURNS_DEEP_STUBS); + when(death.getEntity().getEntityId()).thenReturn(42); + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + listener.onEntityDeath(death); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + var unload = mock(org.bukkit.event.world.EntitiesUnloadEvent.class); + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(42); + when(unload.getEntities()).thenReturn(List.of(target)); + GlowingEntityManager.setGlowingReceivers(42, List.of(player1Id)); + listener.onEntitiesUnload(unload); + assertFalse(GlowingEntityManager.hasGlowingEntities()); + }); + } + + private void withPlayers(boolean naturallyGlowing, List sent, UUID player1Id, UUID player2Id, Runnable scenario) { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(42); + when(target.getType()).thenReturn(EntityType.ARMOR_STAND); + when(target.isGlowing()).thenReturn(naturallyGlowing); + World world = mock(World.class); + when(world.getEntities()).thenReturn(List.of(target)); + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + when(player1.getUniqueId()).thenReturn(player1Id); + when(player2.getUniqueId()).thenReturn(player2Id); + when(player1.isOnline()).thenReturn(true); + when(player2.isOnline()).thenReturn(true); + PacketEventsAPI api = mock(PacketEventsAPI.class, RETURNS_DEEP_STUBS); + var settings = PacketEvents.getAPI().getSettings(); + var version = PacketEvents.getAPI().getServerManager().getVersion(); + when(api.getSettings()).thenReturn(settings); + when(api.getServerManager().getVersion()).thenReturn(version); + EntityLib entityLib = mock(EntityLib.class, RETURNS_DEEP_STUBS); + when(entityLib.getConfig().getOwnershipCheck()).thenReturn(OwnershipCheck.THROW); + var playerManager = api.getPlayerManager(); + doAnswer(call -> { + Player viewer = call.getArgument(0); + WrapperPlayServerEntityMetadata packet = call.getArgument(1); + sent.add(new SentGlow(viewer.getUniqueId(), + outgoingGlowAfterListener(viewer, packet))); + return null; + }).when(playerManager).sendPacket(any(Player.class), any(WrapperPlayServerEntityMetadata.class)); + + try (var bukkit = mockStatic(Bukkit.class); + var packets = mockStatic(PacketEvents.class); + var entities = mockStatic(EntityLib.class); + var conversion = mockStatic(SpigotConversionUtil.class)) { + bukkit.when(Bukkit::getWorlds).thenReturn(List.of(world)); + bukkit.when(() -> Bukkit.getPlayer(player1Id)).thenReturn(player1); + bukkit.when(() -> Bukkit.getPlayer(player2Id)).thenReturn(player2); + packets.when(PacketEvents::getAPI).thenReturn(api); + entities.when(EntityLib::get).thenReturn(entityLib); + conversion.when(() -> SpigotConversionUtil.fromBukkitEntityType(EntityType.ARMOR_STAND)) + .thenReturn(EntityTypes.ARMOR_STAND); + conversion.when(() -> SpigotConversionUtil.getEntityMetadata(target)).thenReturn( + List.of(new EntityData<>(0, EntityDataTypes.BYTE, + (byte) (naturallyGlowing ? 0x40 : 0)))); + scenario.run(); + } + } + + private boolean outgoingGlowAfterListener(Player viewer, WrapperPlayServerEntityMetadata packet) { + PacketSendEvent event = mock(PacketSendEvent.class, RETURNS_DEEP_STUBS); + UUID viewerId = viewer.getUniqueId(); + when(event.getPacketType()).thenReturn(PacketType.Play.Server.ENTITY_METADATA); + when(event.getUser().getUUID()).thenReturn(viewerId); + AtomicReference>> metadata = new AtomicReference<>(packet.getEntityMetadata()); + try (var ignored = mockConstruction(WrapperPlayServerEntityMetadata.class, (wrapper, context) -> { + when(wrapper.getEntityId()).thenReturn(packet.getEntityId()); + when(wrapper.getEntityMetadata()).thenAnswer(call -> metadata.get()); + doAnswer(call -> { + metadata.set(call.getArgument(0)); + return null; + }).when(wrapper).setEntityMetadata(anyList()); + })) { + new GlowingEntityListener().onPacketSend(event); + } + return metadata.get().stream() + .filter(data -> data.getIndex() == 0) + .anyMatch(data -> (((Byte) data.getValue()) & 0x40) != 0); + } +} diff --git a/src/test/java/org/crebs/skpackets/api/util/field/FieldValuesTest.java b/src/test/java/org/crebs/skpackets/api/util/field/FieldValuesTest.java new file mode 100644 index 0000000..ea1160a --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/util/field/FieldValuesTest.java @@ -0,0 +1,27 @@ +package org.crebs.skpackets.api.util.field; + +import ch.njol.skript.Skript; +import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicReference; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class FieldValuesTest { + @Test + void invalidArrayValuesReportAnErrorAndPreserveTheField() { + Object source = new Object(); + Number[] original = {1, 2}; + AtomicReference stored = new AtomicReference<>(original); + FieldAccessor accessor = new FieldAccessor<>( + "numbers", new String[0], Number[].class, false, + ignored -> stored.get(), (ignored, value) -> stored.set(value)); + try (var skript = mockStatic(Skript.class)) { + assertDoesNotThrow(() -> FieldValues.set(new Object[]{source}, ignored -> accessor, + new Object[]{"invalid"}, "packet")); + assertSame(original, stored.get()); + skript.verify(() -> Skript.error(anyString())); + FieldValues.set(new Object[]{source}, ignored -> accessor, new Object[]{3, 4}, "packet"); + assertArrayEquals(new Number[]{3, 4}, stored.get()); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java b/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java new file mode 100644 index 0000000..149591d --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java @@ -0,0 +1,127 @@ +package org.crebs.skpackets.elements; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.registrations.Classes; +import com.github.shanebeee.skr.Registration; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import org.crebs.skpackets.api.entity.metadata.EntityMeta; +import org.crebs.skpackets.api.util.field.BaseFieldRegistry; +import org.crebs.skpackets.elements.general.field.PacketFieldRegistry; +import org.crebs.skpackets.elements.general.section.SecExprNewPacket; +import org.crebs.skpackets.elements.entity.section.SecExprNewFakeEntity; +import org.crebs.skpackets.elements.entity.section.SecExprNewMeta; +import org.skriptlang.skript.Skript; +import org.bukkit.plugin.PluginManager; +import org.crebs.skpackets.elements.general.GeneralModule; +import org.crebs.skpackets.elements.entity.EntityModule; +import org.crebs.skpackets.elements.entity.field.entity.FakeEntityFieldRegistry; +import org.crebs.skpackets.elements.entity.field.meta.MetaFieldRegistry; +import org.crebs.skpackets.elements.simple.SimpleModule; +import org.crebs.skpackets.elements.team.TeamModule; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.Test; +import org.skriptlang.skript.bukkit.lang.eventvalue.EventValueRegistry; +import org.skriptlang.skript.lang.comparator.Comparators; +import org.skriptlang.skript.lang.converter.Converters; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class AddonRegistrationTest { + @Test + void registersAllModulesWithoutDuplicateTypes() throws Exception { + PacketEventsTestBootstrap.initialize(); + // Skript requires the host name to differ from the addon name. + Skript skript = Skript.of(AddonRegistrationTest.class, "skPackets-test"); + skript.storeRegistry(EventValueRegistry.class, EventValueRegistry.empty(skript)); + try (var snapshots = new RegistrySnapshots( + Classes.class, Comparators.class, Converters.class, + SecExprNewPacket.class, SecExprNewFakeEntity.class, SecExprNewMeta.class); + var skriptPlugin = mockStatic(ch.njol.skript.Skript.class); + var bukkit = mockStatic(Bukkit.class)) { + skriptPlugin.when(ch.njol.skript.Skript::instance).thenReturn(skript); + bukkit.when(Bukkit::getPluginManager).thenReturn(mock(PluginManager.class)); + var server = mock(Server.class, RETURNS_DEEP_STUBS); + when(server.getServerConfig().isProxyOnlineMode()).thenReturn(true); + bukkit.when(Bukkit::getServer).thenReturn(server); + Field packetRegistry = BaseFieldRegistry.class.getDeclaredField("REGISTRY"); + packetRegistry.setAccessible(true); + Map packetSchemas = (Map) packetRegistry.get(PacketFieldRegistry.INSTANCE); + snapshots.capture(packetSchemas); + packetSchemas.clear(); + snapshots.captureRegistry(FakeEntityFieldRegistry.INSTANCE); + snapshots.captureRegistry(MetaFieldRegistry.INSTANCE); + Registration registration = new Registration("skPackets", true); + registration.getAddon().loadModules( + new GeneralModule(registration), + new SimpleModule(registration), + new EntityModule(registration), + new TeamModule(registration)); + Field infoField = Registration.TypeRegistrar.class.getDeclaredField("classInfo"); + infoField.setAccessible(true); + List> infos = new ArrayList<>(); + Set> classes = new HashSet<>(); + Set codes = new HashSet<>(); + for (var queued : registration.getTypes()) { + ClassInfo info = (ClassInfo) infoField.get(queued); + assertTrue(classes.add(info.getC()), () -> "Duplicate queued class: " + info.getC()); + assertTrue(codes.add(info.getCodeName()), () -> "Duplicate queued codename: " + info.getCodeName()); + infos.add(info); + } + assertFalse(infos.isEmpty()); + assertDoesNotThrow(registration::finalizeRegistration); + for (ClassInfo info : infos) { + assertSame(info, Classes.getExactClassInfo(info.getC())); + assertSame(info, Classes.getClassInfoNoError(info.getCodeName())); + } + assertNotNull(Classes.getExactClassInfo(PacketTypeCommon.class)); + assertNotNull(Classes.getExactClassInfo(EntityMeta.class)); + assertFalse(registration.getEffects().isEmpty()); + assertFalse(registration.getExpressions().isEmpty()); + } + } + + private static final class RegistrySnapshots implements AutoCloseable { + private final Map snapshots = new IdentityHashMap<>(); + RegistrySnapshots(Class... owners) throws IllegalAccessException { + for (Class owner : owners) { + for (Field field : owner.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers())) continue; + field.setAccessible(true); + Object value = field.get(null); + if (value instanceof Map map) snapshots.put(value, new HashMap<>(map)); + else if (value instanceof Collection collection) snapshots.put(value, new ArrayList<>(collection)); + } + } + } + void captureRegistry(Object registry) throws IllegalAccessException { + for (Class owner = registry.getClass(); owner != Object.class; owner = owner.getSuperclass()) { + for (Field field : owner.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + field.setAccessible(true); + if (field.get(registry) instanceof Map map) capture(map); + } + } + } + void capture(Map map) { snapshots.put(map, new HashMap<>(map)); } + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override public void close() { + snapshots.forEach((target, saved) -> { + if (target instanceof Map map) { + map.clear(); + map.putAll((Map) saved); + } else if (target instanceof Collection collection) { + collection.clear(); + collection.addAll((Collection) saved); + } + }); + } + } +} + diff --git a/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java new file mode 100644 index 0000000..bdcdf6a --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java @@ -0,0 +1,229 @@ +package org.crebs.skpackets.elements.entity.effects; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.crebs.skpackets.SkPackets; +import org.crebs.skpackets.api.entity.PacketEntityManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +class EntityEffectsTest { + private final Event event = mock(Event.class); + + @org.junit.jupiter.api.BeforeAll + static void initializePacketEvents() { + org.crebs.skpackets.support.PacketEventsTestBootstrap.initialize(); + } + + @SuppressWarnings("unchecked") + private Expression input(T... values) { + Expression expression = mock(Expression.class); + when(expression.getArray(event)).thenReturn(values); + when(expression.getAll(event)).thenReturn(values); + when(expression.getSingle(event)).thenReturn(values.length == 0 ? null : values[0]); + return expression; + } + + @Test + void killingRemovesEveryEntityFromPersistenceManager() { + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + var effect = new EffKillPacketEntity(); + Expression[] inputs = {input(first, second)}; + assertTrue(effect.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + PacketEntityManager manager = mock(PacketEntityManager.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(plugin.getPacketEntities()).thenReturn(manager); + try (var staticPlugin = mockStatic(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + effect.execute(event); + verify(manager).remove(first); + verify(manager).remove(second); + verifyNoMoreInteractions(manager); + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void viewerPatternsUpdateEveryEntity(int pattern) { + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + Player viewer = mock(Player.class); + UUID id = UUID.randomUUID(); + when(viewer.getUniqueId()).thenReturn(id); + Expression[] inputs = {input(viewer), input(first, second)}; + var effect = new EffPacketEntityViewers(); + assertTrue(effect.init(inputs, pattern, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + PacketEntityManager manager = mock(PacketEntityManager.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(plugin.getPacketEntities()).thenReturn(manager); + try (var staticPlugin = mockStatic(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + effect.execute(event); + for (WrapperEntity entity : new WrapperEntity[]{first, second}) { + if (pattern == 0) verify(manager).addViewer(entity, viewer); + else verify(manager).removeViewer(entity, id); + } + verifyNoMoreInteractions(manager); + } + } + + @Test + void removingViewerSupportsOfflinePlayers() { + OfflinePlayer viewer = mock(OfflinePlayer.class); + UUID id = UUID.randomUUID(); + when(viewer.getUniqueId()).thenReturn(id); + WrapperEntity entity = mock(WrapperEntity.class); + Expression[] inputs = {input(viewer), input(entity)}; + var effect = new EffPacketEntityViewers(); + assertTrue(effect.init(inputs, 1, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + PacketEntityManager manager = mock(PacketEntityManager.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(plugin.getPacketEntities()).thenReturn(manager); + try (var staticPlugin = mockStatic(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + effect.execute(event); + verify(manager).removeViewer(entity, id); + } + } + + @Test + void teleportMovesEveryEntityToTheEvaluatedLocation() { + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + Location target = new Location(null, 1, 20, -3); + Expression[] inputs = {input(first, second), input(target)}; + var effect = new EffTeleportPacketEntity(); + assertTrue(effect.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + PacketEntityManager manager = mock(PacketEntityManager.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(plugin.getPacketEntities()).thenReturn(manager); + try (var staticPlugin = mockStatic(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + effect.execute(event); + verify(manager).teleport(first, target); + verify(manager).teleport(second, target); + } + } + + @Test + void missingTeleportLocationDoesNotAccessPlugin() { + Expression[] inputs = {input(mock(WrapperEntity.class)), input(new Location[0])}; + var effect = new EffTeleportPacketEntity(); + assertTrue(effect.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + try (var staticPlugin = mockStatic(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + effect.execute(event); + staticPlugin.verifyNoInteractions(); + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void ridingFakeVehicleUpdatesEachPassenger(int pattern) { + WrapperEntity vehicle = mock(WrapperEntity.class); + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + Expression[] inputs = {input(first, second), input(vehicle)}; + var effect = new EffRideFakeEntity(); + assertTrue(effect.init(inputs, pattern, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class); + PacketEntityManager manager = mock(PacketEntityManager.class); + when(plugin.getPacketEntities()).thenReturn(manager); + try (var staticPlugin = mockStatic(SkPackets.class)) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + effect.execute(event); + } + for (WrapperEntity passenger : new WrapperEntity[]{first, second}) { + if (pattern == 0) { + verify(manager).removeRealPassenger(passenger); + verify(vehicle).addPassenger(passenger); + } + else verify(vehicle).removePassenger(passenger); + } + verifyNoMoreInteractions(vehicle); + } + + @Test + void missingVehicleDoesNotChangePassengers() { + WrapperEntity passenger = mock(WrapperEntity.class); + Expression[] inputs = {input(passenger), input(new Object[0])}; + var effect = new EffRideFakeEntity(); + assertTrue(effect.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + effect.execute(event); + verifyNoInteractions(passenger); + } + + @Test + void spawnWithMissingLocationDoesNothing() { + WrapperEntity entity = mock(WrapperEntity.class); + Expression[] inputs = {input(entity), input(new Location[0])}; + var effect = new EffSpawnFakeEntity(); + assertTrue(effect.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + effect.execute(event); + verifyNoInteractions(entity); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void spawningTeleportsManagedEntitiesAndRestoresOnlyOnlineViewers(boolean managed) { + WrapperEntity entity = mock(WrapperEntity.class); + when(entity.getEntityId()).thenReturn(15); + UUID online = UUID.randomUUID(), offline = UUID.randomUUID(); + when(entity.getViewers()).thenReturn(Set.of(online, offline)); + Player viewer = mock(Player.class); + Location target = new Location(null, 4, 50, 6); + Expression[] inputs = {input(entity), input(target)}; + var effect = new EffSpawnFakeEntity(); + assertTrue(effect.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + PacketEntityManager manager = mock(PacketEntityManager.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(plugin.getPacketEntities()).thenReturn(manager); + when(manager.getEntity(15)).thenReturn(managed ? Optional.of(entity) : Optional.empty()); + try (var staticPlugin = mockStatic(SkPackets.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + var conversion = mockStatic(SpigotConversionUtil.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + var bukkit = mockStatic(Bukkit.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + bukkit.when(() -> Bukkit.getPlayer(online)).thenReturn(viewer); + effect.execute(event); + verify(manager).getEntity(15); + if (managed) verify(manager).teleport(entity, target); + else verify(manager).spawn(entity, target, List.of(viewer), true); + verifyNoMoreInteractions(manager); + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void ridingRealVehicleDelegatesViewerSpecificPassengersToManager(int pattern) { + Entity vehicle = mock(Entity.class); + WrapperEntity fakePassenger = mock(WrapperEntity.class); + Expression[] inputs = {input(fakePassenger), input(vehicle)}; + var effect = new EffRideFakeEntity(); + assertTrue(effect.init(inputs, pattern, Kleenean.FALSE, new ParseResult("", inputs))); + SkPackets plugin = mock(SkPackets.class); + PacketEntityManager manager = mock(PacketEntityManager.class); + when(plugin.getPacketEntities()).thenReturn(manager); + try (var staticPlugin = mockStatic(SkPackets.class)) { + staticPlugin.when(SkPackets::getInstance).thenReturn(plugin); + effect.execute(event); + verify(manager).updateRealPassengers(vehicle, List.of(fakePassenger), pattern == 0); + } + verifyNoInteractions(vehicle); + } +} + + diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/EntityLookupExpressionsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/EntityLookupExpressionsTest.java new file mode 100644 index 0000000..274b1e8 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/EntityLookupExpressionsTest.java @@ -0,0 +1,110 @@ +package org.crebs.skpackets.elements.entity.expressions; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import me.tofaa.entitylib.EntityLib; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.junit.jupiter.api.Test; +import org.mockito.MockMakers; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class EntityLookupExpressionsTest { + private final Event event = mock(Event.class); + + @SuppressWarnings("unchecked") + private Expression input(T value) { + Expression expression = mock(Expression.class); + when(expression.getSingle(event)).thenReturn(value); + return expression; + } + + @Test + void entityIdReturnsBukkitIdAndHandlesMissingInput() { + Entity entity = mock(Entity.class); + when(entity.getEntityId()).thenReturn(123); + var expression = new ExprEntityId(); + Expression[] inputs = {input(entity)}; + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + assertArrayEquals(new Number[]{123}, expression.get(event)); + inputs[0] = input(null); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + assertArrayEquals(new Number[0], expression.get(event)); + assertTrue(expression.isSingle()); + assertEquals(Number.class, expression.getReturnType()); + } + + @Test + void lookupByIdReturnsExactEntityAndMissingIdsAreUnset() { + EntityLib lib = mock(EntityLib.class, withSettings().mockMaker(MockMakers.INLINE)); + WrapperEntity entity = mock(WrapperEntity.class); + when(lib.getEntity(42)).thenReturn(entity); + var expression = new ExprFakeEntityFromId(); + Expression[] inputs = {input(42)}; + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + try (var library = mockStatic(EntityLib.class, withSettings().mockMaker(MockMakers.INLINE))) { + library.when(EntityLib::get).thenReturn(lib); + assertArrayEquals(new WrapperEntity[]{entity}, expression.get(event)); + inputs[0] = input(99); + expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs)); + assertNull(expression.get(event)); + inputs[0] = input(null); + expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs)); + assertNull(expression.get(event)); + verify(lib).getEntity(42); + verify(lib).getEntity(99); + verifyNoMoreInteractions(lib); + } + assertTrue(expression.isSingle()); + assertEquals(WrapperEntity.class, expression.getReturnType()); + } + + @Test + void lookupByUuidSearchesTheWholeCollection() { + EntityLib lib = mock(EntityLib.class, withSettings().mockMaker(MockMakers.INLINE)); + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + UUID wanted = UUID.randomUUID(); + when(first.getUuid()).thenReturn(UUID.randomUUID()); + when(second.getUuid()).thenReturn(wanted); + when(lib.getEntities()).thenReturn(List.of(first, second)); + var expression = new ExprFakeEntityFromUuid(); + Expression[] inputs = {input(wanted)}; + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + try (var library = mockStatic(EntityLib.class, withSettings().mockMaker(MockMakers.INLINE))) { + library.when(EntityLib::get).thenReturn(lib); + assertArrayEquals(new WrapperEntity[]{second}, expression.get(event)); + inputs[0] = input(UUID.randomUUID()); + expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs)); + assertNull(expression.get(event)); + inputs[0] = input(null); + expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs)); + clearInvocations(lib); + assertNull(expression.get(event)); + verifyNoInteractions(lib); + } + assertTrue(expression.isSingle()); + } + + @Test + void allEntitiesReflectsCurrentLibraryContents() { + EntityLib lib = mock(EntityLib.class, withSettings().mockMaker(MockMakers.INLINE)); + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + when(lib.getEntities()).thenReturn(List.of(first, second), List.of()); + var expression = new ExprFakeEntitiesAll(); + assertTrue(expression.init(new Expression[0], 0, Kleenean.FALSE, new ParseResult("", new Expression[0]))); + try (var library = mockStatic(EntityLib.class, withSettings().mockMaker(MockMakers.INLINE))) { + library.when(EntityLib::get).thenReturn(lib); + assertArrayEquals(new WrapperEntity[]{first, second}, expression.get(event)); + assertArrayEquals(new WrapperEntity[0], expression.get(event)); + } + assertFalse(expression.isSingle()); + assertEquals(WrapperEntity.class, expression.getReturnType()); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/FakeEquipmentExpressionTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/FakeEquipmentExpressionTest.java new file mode 100644 index 0000000..28bfd67 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/FakeEquipmentExpressionTest.java @@ -0,0 +1,79 @@ +package org.crebs.skpackets.elements.entity.expressions; + +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import me.tofaa.entitylib.wrapper.WrapperEntityEquipment; +import me.tofaa.entitylib.wrapper.WrapperLivingEntity; +import org.bukkit.event.Event; +import org.bukkit.inventory.EquipmentSlot; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class FakeEquipmentExpressionTest { + private final Event event = mock(Event.class); + + @SuppressWarnings("unchecked") + private ExprFakeEquipment expression(EquipmentSlot slot, WrapperEntity... entities) { + Expression slotInput = mock(Expression.class); + when(slotInput.getSingle(event)).thenReturn(slot); + Expression entityInput = mock(Expression.class); + when(entityInput.getArray(event)).thenReturn(entities); + when(entityInput.isSingle()).thenReturn(entities.length == 1); + Expression[] inputs = {slotInput, entityInput}; + var expression = new ExprFakeEquipment(); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + return expression; + } + + @ParameterizedTest + @EnumSource(ChangeMode.class) + void advertisesOnlySupportedChanges(ChangeMode mode) { + var expression = new ExprFakeEquipment(); + if (mode == ChangeMode.SET || mode == ChangeMode.RESET || mode == ChangeMode.DELETE) + assertArrayEquals(new Class[]{ItemType.class}, expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode)); + } + + @ParameterizedTest + @EnumSource(value = ChangeMode.class, names = {"RESET", "DELETE"}) + void deletingAndResettingClearTheSelectedSlotForEveryEntity(ChangeMode mode) { + var first = mock(WrapperLivingEntity.class); + var second = mock(WrapperLivingEntity.class); + var firstEquipment = mock(WrapperEntityEquipment.class); + var secondEquipment = mock(WrapperEntityEquipment.class); + when(first.getEquipment()).thenReturn(firstEquipment); + when(second.getEquipment()).thenReturn(secondEquipment); + expression(EquipmentSlot.HEAD, first, second).change(event, null, mode); + verify(firstEquipment).clearSlot(com.github.retrooper.packetevents.protocol.player.EquipmentSlot.HELMET); + verify(secondEquipment).clearSlot(com.github.retrooper.packetevents.protocol.player.EquipmentSlot.HELMET); + } + + @Test + void absentSlotReturnsNoItemsAndDoesNotMutateEntities() { + var entity = mock(WrapperLivingEntity.class); + var expression = expression(null, entity); + assertArrayEquals(new ItemType[0], expression.get(event)); + expression.change(event, null, ChangeMode.RESET); + verifyNoInteractions(entity); + } + + @Test + void emptySetDoesNotClearExistingEquipment() { + var entity = mock(WrapperLivingEntity.class); + var expression = expression(EquipmentSlot.HAND, entity); + expression.change(event, null, ChangeMode.SET); + expression.change(event, new Object[0], ChangeMode.SET); + verifyNoInteractions(entity); + assertTrue(expression.isSingle()); + assertEquals(ItemType.class, expression.getReturnType()); + assertFalse(expression(EquipmentSlot.HAND, entity, entity).isSingle()); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/EntityMetaDataPropertyTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/EntityMetaDataPropertyTest.java new file mode 100644 index 0000000..ff8b0a2 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/EntityMetaDataPropertyTest.java @@ -0,0 +1,62 @@ +package org.crebs.skpackets.elements.entity.expressions.entity; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.entity.Entity; +import org.crebs.skpackets.api.entity.metadata.EntityMeta; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EntityMetaDataPropertyTest { + @Test + void realEntitiesReturnMetadataSnapshotsWithTheirOwnIds() { + org.crebs.skpackets.support.PacketEventsTestBootstrap.initialize(); + var entityType = EntityTypes.ZOMBIE; + Entity entity = mock(Entity.class); + when(entity.getEntityId()).thenReturn(71); + when(entity.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE); + EntityMeta snapshot = mock(EntityMeta.class, withSettings().mockMaker(INLINE)); + var expression = new ExprFakeEntityEntityMetaData(); + Expression[] inputs = {mock(Expression.class)}; + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + try (var meta = mockStatic(EntityMeta.class, withSettings().mockMaker(INLINE)); + var conversion = mockStatic(SpigotConversionUtil.class, withSettings().mockMaker(INLINE))) { + conversion.when(() -> SpigotConversionUtil.fromBukkitEntityType(org.bukkit.entity.EntityType.ZOMBIE)).thenReturn(entityType); + conversion.when(() -> SpigotConversionUtil.getEntityMetadata(entity)).thenReturn(List.of()); + meta.when(() -> EntityMeta.createMeta(71, entityType)).thenReturn(snapshot); + assertArrayEquals(new EntityMeta[]{snapshot}, expression.get(null, new Object[]{entity})); + var packet = org.mockito.ArgumentCaptor.forClass(WrapperPlayServerEntityMetadata.class); + verify(snapshot).readPacket(packet.capture()); + assertEquals(71, packet.getValue().getEntityId()); + assertTrue(packet.getValue().getEntityMetadata().isEmpty()); + verifyNoMoreInteractions(snapshot); + } + } + + @Test + void returnsLiveMetadataForEachFakeEntityAndIgnoresUnsupportedValues() { + WrapperEntity first = mock(WrapperEntity.class), second = mock(WrapperEntity.class); + EntityMeta firstMeta = mock(EntityMeta.class, withSettings().mockMaker(INLINE)); + EntityMeta secondMeta = mock(EntityMeta.class, withSettings().mockMaker(INLINE)); + var expression = new ExprFakeEntityEntityMetaData(); + Expression[] inputs = {mock(Expression.class)}; + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + try (var meta = mockStatic(EntityMeta.class, withSettings().mockMaker(INLINE))) { + meta.when(() -> EntityMeta.of(first)).thenReturn(firstMeta); + meta.when(() -> EntityMeta.of(second)).thenReturn(secondMeta); + assertArrayEquals(new EntityMeta[]{firstMeta, secondMeta}, expression.get(null, new Object[]{first, "ignored", null, second})); + assertArrayEquals(new EntityMeta[0], expression.get(null, new Object[0])); + } + assertEquals(EntityMeta.class, expression.getReturnType()); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/FakeEntityPropertiesTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/FakeEntityPropertiesTest.java new file mode 100644 index 0000000..3617338 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/FakeEntityPropertiesTest.java @@ -0,0 +1,96 @@ +package org.crebs.skpackets.elements.entity.expressions.entity; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.crebs.skpackets.api.util.field.BaseFieldRegistry; +import org.crebs.skpackets.api.util.field.InheritingFieldRegistry; +import org.crebs.skpackets.elements.entity.expressions.prop.ExprFakeEntityField; +import org.crebs.skpackets.elements.entity.field.entity.FakeEntityFieldRegistry; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class FakeEntityPropertiesTest { + static Stream properties() { + return Stream.of( + arguments(ExprFakeEntityEntityId.class, "entity id", false), + arguments(ExprFakeEntityEntityLocation.class, "entity location", true), + arguments(ExprFakeEntityEntityType.class, "entity type", false), + arguments(ExprFakeEntityEntityUuid.class, "entity uuid", false), + arguments(ExprFakeEntityEntityViewers.class, "entity viewers", true), + arguments(ExprFakeEntityPlayerSkin.class, "player skin", true), + arguments(ExprFakeEntityPlayerUsername.class, "player username", false)); + } + + @ParameterizedTest(name = "{0}: named field read and write dispatch") + @MethodSource("properties") + @SuppressWarnings({"unchecked", "rawtypes"}) + void dispatchesItsOwnFieldAndRespectsSupportedChangeModes( + Class type, String name, boolean writable) throws Exception { + var registryField = BaseFieldRegistry.class.getDeclaredField("REGISTRY"); + registryField.setAccessible(true); + Map registry = (Map) registryField.get(FakeEntityFieldRegistry.INSTANCE); + Map original = new HashMap(registry); + var classesField = InheritingFieldRegistry.class.getDeclaredField("classToTypeMap"); + classesField.setAccessible(true); + Map classes = (Map) classesField.get(FakeEntityFieldRegistry.INSTANCE); + Map originalClasses = new HashMap(classes); + registry.clear(); + try (var logging = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + EntityType entityType = mock(EntityType.class); + WrapperEntity entity = mock(WrapperEntity.class); + when(entity.getEntityType()).thenReturn(entityType); + AtomicReference value = new AtomicReference<>("before"); + FakeEntityFieldRegistry.INSTANCE.builder(entityType, WrapperEntity.class) + .optionalField(String.class, ignored -> value.get(), + writable ? (ignored, changed) -> value.set(changed) : null, name) + .build(); + Expression source = mock(Expression.class); + when(source.getArray(null)).thenReturn(new WrapperEntity[]{entity}); + when(source.isSingle()).thenReturn(true); + Expression[] inputs = {source}; + var expression = type.getConstructor().newInstance(); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + assertArrayEquals(new String[]{"before"}, expression.getArray(null)); + assertEquals(String.class, expression.getReturnType()); + assertTrue(expression.isSingle()); + assertTrue(expression.toString(null, false).contains(name)); + for (ChangeMode mode : ChangeMode.values()) { + if (writable && mode == ChangeMode.SET) + assertArrayEquals(new Class[]{String.class}, expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode)); + } + expression.change(null, new Object[]{"after"}, ChangeMode.SET); + assertEquals(writable ? "after" : "before", value.get()); + expression.change(null, null, ChangeMode.SET); + expression.change(null, new Object[0], ChangeMode.SET); + expression.change(null, new Object[]{"ignored"}, ChangeMode.DELETE); + expression.change(null, new Object[]{123}, ChangeMode.SET); + assertEquals(writable ? "after" : "before", value.get()); + when(source.getArray(null)).thenReturn(new WrapperEntity[0]); + assertArrayEquals(new Object[0], expression.getArray(null)); + when(source.isSingle()).thenReturn(false); + assertFalse(expression.isSingle()); + } finally { + registry.clear(); + registry.putAll(original); + classes.clear(); + classes.putAll(originalClasses); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/meta/EntityMetaExpressionsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/meta/EntityMetaExpressionsTest.java new file mode 100644 index 0000000..d6790c4 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/meta/EntityMetaExpressionsTest.java @@ -0,0 +1,134 @@ +package org.crebs.skpackets.elements.entity.expressions.meta; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import org.crebs.skpackets.api.entity.metadata.EntityMeta; +import org.crebs.skpackets.api.util.field.BaseFieldRegistry; +import org.crebs.skpackets.elements.entity.expressions.prop.ExprMetaField; +import org.crebs.skpackets.elements.entity.field.meta.MetaFieldRegistry; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EntityMetaExpressionsTest { + static Stream expressions() { + return Stream.of( + arguments(ExprEntityMetaCustomName.class, "custom name"), + arguments(ExprEntityMetaCustomNameVisibleState.class, "custom name visible state"), + arguments(ExprEntityMetaDisplayBackgroundColor.class, "display background color"), + arguments(ExprEntityMetaDisplayBillboardConstraints.class, "display billboard constraints"), + arguments(ExprEntityMetaDisplayBlockData.class, "display block data"), + arguments(ExprEntityMetaDisplayBrightnessOverride.class, "display brightness override"), + arguments(ExprEntityMetaDisplayGlowColorOverride.class, "display glow color override"), + arguments(ExprEntityMetaDisplayHeight.class, "display height"), + arguments(ExprEntityMetaDisplayInterpolationDelay.class, "display interpolation delay"), + arguments(ExprEntityMetaDisplayItem.class, "display item"), + arguments(ExprEntityMetaDisplayItemTransform.class, "display item transform"), + arguments(ExprEntityMetaDisplayLeftRotation.class, "display left rotation"), + arguments(ExprEntityMetaDisplayLineWidth.class, "display line width"), + arguments(ExprEntityMetaDisplayRightRotation.class, "display right rotation"), + arguments(ExprEntityMetaDisplayScale.class, "display scale"), + arguments(ExprEntityMetaDisplaySeeThroughState.class, "display see through state"), + arguments(ExprEntityMetaDisplayShadowRadius.class, "display shadow radius"), + arguments(ExprEntityMetaDisplayShadowStrength.class, "display shadow strength"), + arguments(ExprEntityMetaDisplayTeleportInterpolationDuration.class, "display teleport interpolation duration"), + arguments(ExprEntityMetaDisplayText.class, "display text"), + arguments(ExprEntityMetaDisplayTextAlignment.class, "display text alignment"), + arguments(ExprEntityMetaDisplayTextShadowedState.class, "display text shadowed state"), + arguments(ExprEntityMetaDisplayTransformInterpolationDuration.class, "display transform interpolation duration"), + arguments(ExprEntityMetaDisplayTranslation.class, "display translation"), + arguments(ExprEntityMetaDisplayUseDefaultBackgroundState.class, "display use default background state"), + arguments(ExprEntityMetaDisplayViewRange.class, "display view range"), + arguments(ExprEntityMetaDisplayWidth.class, "display width"), + arguments(ExprEntityMetaElytraFlyingState.class, "elytra flying state"), + arguments(ExprEntityMetaEntityId.class, "entity id"), + arguments(ExprEntityMetaFrozenTime.class, "frozen time"), + arguments(ExprEntityMetaGlowingState.class, "glowing state"), + arguments(ExprEntityMetaGravityState.class, "gravity state"), + arguments(ExprEntityMetaHealth.class, "health"), + arguments(ExprEntityMetaInteractionHeight.class, "interaction height"), + arguments(ExprEntityMetaInteractionResponsiveState.class, "interaction responsive state"), + arguments(ExprEntityMetaInteractionWidth.class, "interaction width"), + arguments(ExprEntityMetaInvisibleState.class, "invisible state"), + arguments(ExprEntityMetaItem.class, "item"), + arguments(ExprEntityMetaOnFireState.class, "on fire state"), + arguments(ExprEntityMetaPose.class, "pose"), + arguments(ExprEntityMetaSilentState.class, "silent state"), + arguments(ExprEntityMetaSneakingState.class, "sneaking state"), + arguments(ExprEntityMetaSprintingState.class, "sprinting state"), + arguments(ExprEntityMetaSwimmingState.class, "swimming state") + ); + } + + @ParameterizedTest(name = "{0}: named field access and change modes") + @MethodSource("expressions") + @SuppressWarnings({"unchecked", "rawtypes"}) + void resolvesNamedFieldAndFiltersNullUnsupportedAndIncompatibleValues( + Class type, String name) throws Exception { + Field registryField = BaseFieldRegistry.class.getDeclaredField("REGISTRY"); + registryField.setAccessible(true); + Map registry = (Map) registryField.get(MetaFieldRegistry.INSTANCE); + Map original = new HashMap(registry); + registry.clear(); + try (var logging = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + EntityType entityType = mock(EntityType.class); + EntityMeta meta = mock(EntityMeta.class, withSettings().mockMaker(INLINE)); + EntityMeta unsupported = mock(EntityMeta.class, withSettings().mockMaker(INLINE)); + when(meta.getEntityType()).thenReturn(entityType); + when(unsupported.getEntityType()).thenReturn(mock(EntityType.class)); + AtomicReference value = new AtomicReference<>("initial"); + boolean writable = !name.equals("entity id"); + MetaFieldRegistry.INSTANCE.builder(entityType, EntityMeta.class) + .optionalField(String.class, ignored -> value.get(), + writable ? (ignored, changed) -> value.set(changed) : null, name) + .build(); + Expression source = mock(Expression.class); + when(source.getArray(null)).thenReturn(new EntityMeta[]{null, unsupported, meta}); + when(source.isSingle()).thenReturn(false); + ExprMetaField expression = type.getConstructor().newInstance(); + assertTrue(expression.init(new Expression[]{source}, 0, Kleenean.FALSE, + new ParseResult("", new Expression[]{source}))); + assertSame(String.class, expression.getReturnType()); + assertFalse(expression.isSingle()); + assertArrayEquals(new String[]{"initial"}, expression.getArray(null)); + assertTrue(expression.toString(null, false).contains(name)); + for (ChangeMode mode : ChangeMode.values()) { + if (mode == ChangeMode.SET && writable) + assertArrayEquals(new Class[]{String.class}, expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode)); + } + expression.change(null, new Object[]{"changed"}, ChangeMode.SET); + assertEquals(writable ? "changed" : "initial", value.get()); + String unchanged = value.get(); + expression.change(null, null, ChangeMode.SET); + expression.change(null, new Object[0], ChangeMode.SET); + expression.change(null, new Object[]{"ignored"}, ChangeMode.DELETE); + expression.change(null, new Object[]{"ignored"}, ChangeMode.RESET); + expression.change(null, new Object[]{123}, ChangeMode.SET); + assertEquals(unchanged, value.get()); + when(source.getArray(null)).thenReturn(new EntityMeta[0]); + assertArrayEquals(new Object[0], expression.getArray(null)); + when(source.isSingle()).thenReturn(true); + assertTrue(expression.isSingle()); + } finally { + registry.clear(); + registry.putAll(original); + } + } +} + diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataExpressionsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataExpressionsTest.java new file mode 100644 index 0000000..e9f834d --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataExpressionsTest.java @@ -0,0 +1,132 @@ +package org.crebs.skpackets.elements.entity.expressions.metadata; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.crebs.skpackets.api.entity.metadata.PacketEntityField; +import org.crebs.skpackets.elements.entity.expressions.ExprPacketEntityField; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Field; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class PacketEntityMetadataExpressionsTest { + static Stream expressions() { + org.crebs.skpackets.support.PacketEventsTestBootstrap.initialize(); + return Stream.of( + arguments(ExprPacketEntityCustomName.class, PacketEntityField.CUSTOM_NAME), + arguments(ExprPacketEntityCustomNameVisibleState.class, PacketEntityField.CUSTOM_NAME_VISIBLE), + arguments(ExprPacketEntityDisplayBackgroundColor.class, PacketEntityField.BACKGROUND_COLOR), + arguments(ExprPacketEntityDisplayBillboardConstraints.class, PacketEntityField.BILLBOARD), + arguments(ExprPacketEntityDisplayBlockData.class, PacketEntityField.BLOCK_DATA), + arguments(ExprPacketEntityDisplayBrightnessOverride.class, PacketEntityField.BRIGHTNESS), + arguments(ExprPacketEntityDisplayGlowColorOverride.class, PacketEntityField.GLOW_COLOR), + arguments(ExprPacketEntityDisplayHeight.class, PacketEntityField.DISPLAY_HEIGHT), + arguments(ExprPacketEntityDisplayInterpolationDelay.class, PacketEntityField.INTERPOLATION_DELAY), + arguments(ExprPacketEntityDisplayItem.class, PacketEntityField.DISPLAY_ITEM), + arguments(ExprPacketEntityDisplayItemTransform.class, PacketEntityField.ITEM_TRANSFORM), + arguments(ExprPacketEntityDisplayLeftRotation.class, PacketEntityField.LEFT_ROTATION), + arguments(ExprPacketEntityDisplayLineWidth.class, PacketEntityField.LINE_WIDTH), + arguments(ExprPacketEntityDisplayRightRotation.class, PacketEntityField.RIGHT_ROTATION), + arguments(ExprPacketEntityDisplayScale.class, PacketEntityField.SCALE), + arguments(ExprPacketEntityDisplaySeeThroughState.class, PacketEntityField.SEE_THROUGH), + arguments(ExprPacketEntityDisplayShadowRadius.class, PacketEntityField.SHADOW_RADIUS), + arguments(ExprPacketEntityDisplayShadowStrength.class, PacketEntityField.SHADOW_STRENGTH), + arguments(ExprPacketEntityDisplayTeleportInterpolationDuration.class, PacketEntityField.TELEPORT_DURATION), + arguments(ExprPacketEntityDisplayText.class, PacketEntityField.TEXT), + arguments(ExprPacketEntityDisplayTextAlignment.class, PacketEntityField.ALIGNMENT), + arguments(ExprPacketEntityDisplayTextShadowedState.class, PacketEntityField.TEXT_SHADOW), + arguments(ExprPacketEntityDisplayTransformInterpolationDuration.class, PacketEntityField.TRANSFORM_DURATION), + arguments(ExprPacketEntityDisplayTranslation.class, PacketEntityField.TRANSLATION), + arguments(ExprPacketEntityDisplayUseDefaultBackgroundState.class, PacketEntityField.DEFAULT_BACKGROUND), + arguments(ExprPacketEntityDisplayViewRange.class, PacketEntityField.VIEW_RANGE), + arguments(ExprPacketEntityDisplayWidth.class, PacketEntityField.DISPLAY_WIDTH), + arguments(ExprPacketEntityElytraFlyingState.class, PacketEntityField.ELYTRA_FLYING), + arguments(ExprPacketEntityEntityId.class, PacketEntityField.ENTITY_ID), + arguments(ExprPacketEntityFrozenTime.class, PacketEntityField.FROZEN_TIME), + arguments(ExprPacketEntityGlowingState.class, PacketEntityField.GLOWING), + arguments(ExprPacketEntityGravityState.class, PacketEntityField.GRAVITY), + arguments(ExprPacketEntityHealth.class, PacketEntityField.HEALTH), + arguments(ExprPacketEntityInteractionHeight.class, PacketEntityField.INTERACTION_HEIGHT), + arguments(ExprPacketEntityInteractionResponsiveState.class, PacketEntityField.INTERACTION_RESPONSIVE), + arguments(ExprPacketEntityInteractionWidth.class, PacketEntityField.INTERACTION_WIDTH), + arguments(ExprPacketEntityInvisibleState.class, PacketEntityField.INVISIBLE), + arguments(ExprPacketEntityItem.class, PacketEntityField.ITEM), + arguments(ExprPacketEntityOnFireState.class, PacketEntityField.ON_FIRE), + arguments(ExprPacketEntityPose.class, PacketEntityField.POSE), + arguments(ExprPacketEntitySilentState.class, PacketEntityField.SILENT), + arguments(ExprPacketEntitySneakingState.class, PacketEntityField.SNEAKING), + arguments(ExprPacketEntitySprintingState.class, PacketEntityField.SPRINTING), + arguments(ExprPacketEntitySwimmingState.class, PacketEntityField.SWIMMING) + ); + } + + @ParameterizedTest(name = "{0}: field and change contract") + @MethodSource("expressions") + void exposesCorrectFieldAndChangeContract(Class type, + PacketEntityField expected) throws Exception { + ExprPacketEntityField expression = type.getConstructor().newInstance(); + Field field = ExprPacketEntityField.class.getDeclaredField("field"); + field.setAccessible(true); + assertSame(expected, field.get(expression)); + Class returnType = expected.valueType() == org.bukkit.inventory.ItemStack.class + ? ch.njol.skript.aliases.ItemType.class : expected.valueType(); + assertSame(returnType, expression.getReturnType()); + for (ChangeMode mode : ChangeMode.values()) { + boolean accepted = expected.isWritable() + && (mode == ChangeMode.SET || mode == ChangeMode.DELETE || mode == ChangeMode.RESET); + if (accepted) assertArrayEquals(new Class[]{returnType}, expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode), mode.name()); + } + } + + @ParameterizedTest(name = "{0}: dispatch") + @MethodSource("expressions") + @SuppressWarnings({"unchecked", "rawtypes"}) + void dispatchesReadsWritesAndResets(Class type, + PacketEntityField expected) throws Exception { + ExprPacketEntityField expression = type.getConstructor().newInstance(); + PacketEntityField field = mock(PacketEntityField.class, withSettings().mockMaker(INLINE)); + when(field.valueType()).thenReturn((Class) String.class); + Field dependency = ExprPacketEntityField.class.getDeclaredField("field"); + dependency.setAccessible(true); + dependency.set(expression, field); + Expression source = mock(Expression.class); + WrapperEntity first = mock(WrapperEntity.class); + WrapperEntity second = mock(WrapperEntity.class); + when(source.getArray(null)).thenReturn(new WrapperEntity[]{first, second}); + when(source.isSingle()).thenReturn(false); + when(field.get(first)).thenReturn("first"); + when(field.get(second)).thenReturn("second"); + assertTrue(expression.init(new Expression[]{source}, 0, Kleenean.FALSE, + new ParseResult("", new Expression[]{source}))); + assertFalse(expression.isSingle()); + assertArrayEquals(new String[]{"first", "second"}, expression.getArray(null)); + expression.change(null, new Object[]{"updated", "ignored"}, ChangeMode.SET); + verify(field).set(first, "updated"); + verify(field).set(second, "updated"); + expression.change(null, null, ChangeMode.RESET); + expression.change(null, null, ChangeMode.DELETE); + verify(field, times(2)).reset(first); + verify(field, times(2)).reset(second); + clearInvocations(field); + expression.change(null, null, ChangeMode.SET); + expression.change(null, new Object[0], ChangeMode.SET); + expression.change(null, new Object[]{"ignored"}, ChangeMode.ADD); + verifyNoInteractions(field); + when(source.getArray(null)).thenReturn(new WrapperEntity[0]); + assertArrayEquals(new String[0], expression.getArray(null)); + when(source.isSingle()).thenReturn(true); + assertTrue(expression.isSingle()); + } +} + diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataValuesTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataValuesTest.java new file mode 100644 index 0000000..a3746f4 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataValuesTest.java @@ -0,0 +1,108 @@ +package org.crebs.skpackets.elements.entity.expressions.metadata; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.util.Vector3f; +import me.tofaa.entitylib.meta.MetaFields; +import me.tofaa.entitylib.meta.Metadata; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import net.kyori.adventure.text.Component; +import org.bukkit.util.Vector; +import org.crebs.skpackets.api.entity.metadata.PacketEntityField; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class PacketEntityMetadataValuesTest { + private WrapperEntity entity; + private Metadata metadata; + + @BeforeEach + void createEntity() { + entity = mock(WrapperEntity.class); + metadata = mock(Metadata.class, withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(entity.getMetadata()).thenReturn(metadata); + } + + @Test + void glowUsesTheGlowingBitRatherThanReplacingEntityFlags() { + when(entity.getEntityType()).thenReturn(EntityTypes.ZOMBIE); + when(metadata.getFlag(MetaFields.Entity.GLOWING)).thenReturn(true); + assertEquals(true, PacketEntityField.GLOWING.get(entity)); + PacketEntityField.GLOWING.set(entity, false); + verify(entity).setFlag(MetaFields.Entity.GLOWING, false); + verify(entity, never()).setMetadata(any(), any()); + } + + @Test + void gravityInvertsTheWireNoGravityValue() { + when(entity.getEntityType()).thenReturn(EntityTypes.ZOMBIE); + when(metadata.get(MetaFields.Entity.NO_GRAVITY)).thenReturn(true); + assertEquals(false, PacketEntityField.GRAVITY.get(entity)); + PacketEntityField.GRAVITY.set(entity, true); + verify(entity).setMetadata(MetaFields.Entity.NO_GRAVITY, false); + PacketEntityField.GRAVITY.reset(entity); + verify(entity, times(2)).setMetadata(MetaFields.Entity.NO_GRAVITY, false); + } + + @Test + void customNameConvertsTextAndEmptyStringToOptionalComponent() { + when(entity.getEntityType()).thenReturn(EntityTypes.ZOMBIE); + when(metadata.get(MetaFields.Entity.CUSTOM_NAME)) + .thenReturn(Optional.of(Component.text("Crebs"))); + assertEquals("Crebs", PacketEntityField.CUSTOM_NAME.get(entity)); + PacketEntityField.CUSTOM_NAME.set(entity, "Cat"); + verify(entity).setMetadata(MetaFields.Entity.CUSTOM_NAME, Optional.of(Component.text("Cat"))); + PacketEntityField.CUSTOM_NAME.set(entity, ""); + verify(entity).setMetadata(MetaFields.Entity.CUSTOM_NAME, Optional.empty()); + } + + @Test + void healthConvertsNumbersAndRejectsNonFiniteOrUnsupportedValues() { + when(entity.getEntityType()).thenReturn(EntityTypes.ZOMBIE); + when(metadata.get(MetaFields.LivingEntity.HEALTH)).thenReturn(7.5f); + assertEquals(7.5f, PacketEntityField.HEALTH.get(entity)); + PacketEntityField.HEALTH.set(entity, 12); + verify(entity).setMetadata(MetaFields.LivingEntity.HEALTH, 12f); + assertThrows(IllegalArgumentException.class, () -> PacketEntityField.HEALTH.set(entity, Double.NaN)); + assertThrows(IllegalArgumentException.class, () -> PacketEntityField.HEALTH.set(entity, "12")); + when(entity.getEntityType()).thenReturn(EntityTypes.TEXT_DISPLAY); + assertThrows(IllegalArgumentException.class, () -> PacketEntityField.HEALTH.get(entity)); + assertThrows(IllegalArgumentException.class, () -> PacketEntityField.HEALTH.set(entity, 12)); + } + + @Test + void displayVectorsConvertBothDirectionsAndDefaultsAreIndependent() { + when(entity.getEntityType()).thenReturn(EntityTypes.TEXT_DISPLAY); + when(metadata.get(MetaFields.Display.TRANSLATION)).thenReturn(new Vector3f(1, 2, 3)); + assertEquals(new Vector(1, 2, 3), PacketEntityField.TRANSLATION.get(entity)); + PacketEntityField.TRANSLATION.set(entity, new Vector(4, 5, 6)); + verify(entity).setMetadata(MetaFields.Display.TRANSLATION, new Vector3f(4, 5, 6)); + Vector first = (Vector) PacketEntityField.SCALE.get(entity); + first.setX(99); + assertEquals(new Vector(1, 1, 1), PacketEntityField.SCALE.get(entity)); + } + + @Test + void teleportInterpolationRejectsOutOfRangeOrFractionalTicks() { + when(entity.getEntityType()).thenReturn(EntityTypes.TEXT_DISPLAY); + PacketEntityField.TELEPORT_DURATION.set(entity, 59); + verify(entity).setMetadata(MetaFields.Display.POS_ROT_INTERPOLATION_DURATION, 59); + for (Number value : new Number[]{-1, 60, 1.5, Double.POSITIVE_INFINITY}) + assertThrows(IllegalArgumentException.class, + () -> PacketEntityField.TELEPORT_DURATION.set(entity, value)); + } + + @Test + void entityIdIsReadableButCannotBeChanged() { + when(entity.getEntityType()).thenReturn(EntityTypes.ZOMBIE); + when(entity.getEntityId()).thenReturn(42); + assertEquals(42, PacketEntityField.ENTITY_ID.get(entity)); + assertThrows(IllegalArgumentException.class, () -> PacketEntityField.ENTITY_ID.set(entity, 7)); + verify(entity, never()).setMetadata(any(), any()); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/VisibleFakeEntitiesTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/VisibleFakeEntitiesTest.java new file mode 100644 index 0000000..817a1a1 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/VisibleFakeEntitiesTest.java @@ -0,0 +1,46 @@ +package org.crebs.skpackets.elements.entity.expressions.prop; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import me.tofaa.entitylib.EntityLib; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class VisibleFakeEntitiesTest { + @Test + @SuppressWarnings("unchecked") + void returnsUnionOfEntitiesVisibleToAnySourcePlayerWithoutDuplicates() { + Player first = mock(Player.class), second = mock(Player.class); + UUID firstId = UUID.randomUUID(), secondId = UUID.randomUUID(); + when(first.getUniqueId()).thenReturn(firstId); + when(second.getUniqueId()).thenReturn(secondId); + WrapperEntity shared = mock(WrapperEntity.class), privateEntity = mock(WrapperEntity.class), hidden = mock(WrapperEntity.class); + when(shared.getViewers()).thenReturn(Set.of(firstId, secondId)); + when(privateEntity.getViewers()).thenReturn(Set.of(secondId)); + when(hidden.getViewers()).thenReturn(Set.of(UUID.randomUUID())); + EntityLib lib = mock(EntityLib.class, withSettings().mockMaker(INLINE)); + when(lib.getEntities()).thenReturn(List.of(shared, privateEntity, hidden)); + Expression source = mock(Expression.class); + Expression[] inputs = {source}; + var expression = new ExprVisibleFakeEntities(); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs))); + try (var library = mockStatic(EntityLib.class, withSettings().mockMaker(INLINE))) { + library.when(EntityLib::get).thenReturn(lib); + assertArrayEquals(new WrapperEntity[]{shared, privateEntity}, expression.get(null, new Player[]{first, second})); + assertArrayEquals(new WrapperEntity[]{shared}, expression.get(null, new Player[]{first})); + assertArrayEquals(new WrapperEntity[0], expression.get(null, new Player[0])); + } + assertFalse(expression.isSingle()); + assertEquals(WrapperEntity.class, expression.getReturnType()); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/living/ExprFakeLivingEntityAttributeTest.java b/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/living/ExprFakeLivingEntityAttributeTest.java new file mode 100644 index 0000000..c89040b --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/living/ExprFakeLivingEntityAttributeTest.java @@ -0,0 +1,147 @@ +package org.crebs.skpackets.elements.entity.expressions.prop.living; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.attribute.Attributes; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes.Property; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import me.tofaa.entitylib.wrapper.WrapperLivingEntity; +import org.bukkit.NamespacedKey; +import org.bukkit.attribute.Attribute; +import org.bukkit.event.Event; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ExprFakeLivingEntityAttributeTest { + @org.junit.jupiter.api.BeforeAll + static void initializePackets() { org.crebs.skpackets.support.PacketEventsTestBootstrap.initialize(); } + private final Event event = mock(Event.class); + + @ParameterizedTest + @ValueSource(ints={0,1}) + void readsTheRequestedAttributeInBothPropertyOrders(int pattern) { + var attribute = mock(Attribute.class, withSettings().mockMaker(org.mockito.MockMakers.PROXY)); + when(attribute.getKey()).thenReturn(NamespacedKey.minecraft("scale")); + var protocol = mock(com.github.retrooper.packetevents.protocol.attribute.Attribute.class); + when(protocol.getDefaultValue()).thenReturn(1.0); + var other = mock(com.github.retrooper.packetevents.protocol.attribute.Attribute.class); + var property = mock(Property.class); + when(property.getAttribute()).thenReturn(protocol); + when(property.getValue()).thenReturn(2.0); + var unrelated = mock(Property.class); + when(unrelated.getAttribute()).thenReturn(other); + var living = living(); + when(living.getAttributes().getProperties()).thenReturn(List.of(unrelated,property)); + var defaults = living(); + when(defaults.getAttributes().getProperties()).thenReturn(List.of()); + Probe expression = expression(attribute,pattern,living,defaults,mock(WrapperEntity.class),null); + try (var attributes = mockStatic(Attributes.class,withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + attributes.when(() -> Attributes.getByName("minecraft:scale")).thenReturn(protocol); + assertArrayEquals(new Number[]{2.0,1.0},expression.read(event,new WrapperEntity[]{living,defaults,mock(WrapperEntity.class),null})); + } + } + + @ParameterizedTest + @CsvSource({"SET,3.0", "ADD,5.0", "REMOVE,-1.0", "RESET,1.0"}) + void changesAttributeUsingTheCurrentValue(ChangeMode mode,double expected) { + var attribute = mock(Attribute.class, withSettings().mockMaker(org.mockito.MockMakers.PROXY)); + when(attribute.getKey()).thenReturn(NamespacedKey.minecraft("scale")); + var protocol = mock(com.github.retrooper.packetevents.protocol.attribute.Attribute.class); + when(protocol.getDefaultValue()).thenReturn(1.0); + var property = mock(Property.class); + when(property.getAttribute()).thenReturn(protocol); + when(property.getValue()).thenReturn(2.0); + var living = living(); + when(living.getAttributes().getProperties()).thenReturn(List.of(property)); + var nonliving = mock(WrapperEntity.class); + Probe expression = expression(attribute,0,living,nonliving,null); + try (var attributes = mockStatic(Attributes.class,withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + attributes.when(() -> Attributes.getByName("minecraft:scale")).thenReturn(protocol); + expression.change(event,mode==ChangeMode.RESET?null:new Object[]{3},mode); + verify(living.getAttributes()).setAttribute(protocol,expected); + verifyNoInteractions(nonliving); + } + } + + @Test + void missingAttributeAndInvalidDeltasDoNothing() { + var living = mock(WrapperLivingEntity.class); + Probe missing = expression(null,0,living); + assertEquals(0,missing.read(event,new WrapperEntity[]{living}).length); + missing.change(event,new Object[]{3},ChangeMode.SET); + var attribute = mock(Attribute.class, withSettings().mockMaker(org.mockito.MockMakers.PROXY)); + Probe expression = expression(attribute,0,living); + expression.change(event,null,ChangeMode.SET); + expression.change(event,new Object[0],ChangeMode.ADD); + expression.change(event,new Object[]{"wrong"},ChangeMode.REMOVE); + verifyNoInteractions(living,attribute); + } + + @Test + void exposesOnlySupportedChangeModes() { + Probe expression = new Probe(); + for (ChangeMode mode:ChangeMode.values()) { + boolean supported=mode==ChangeMode.SET||mode==ChangeMode.ADD||mode==ChangeMode.REMOVE||mode==ChangeMode.RESET; + if(supported) assertArrayEquals(new Class[]{Number.class},expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode)); + } + assertSame(Number.class,expression.getReturnType()); + } + + @Test + void addingToAnUnsetAttributeStartsAtItsProtocolDefault() { + var attribute=mock(Attribute.class, withSettings().mockMaker(org.mockito.MockMakers.PROXY)); + when(attribute.getKey()).thenReturn(NamespacedKey.minecraft("scale")); + var protocol=mock(com.github.retrooper.packetevents.protocol.attribute.Attribute.class); + when(protocol.getDefaultValue()).thenReturn(1.0); + var living=living(); + when(living.getAttributes().getProperties()).thenReturn(List.of()); + Probe expression=expression(attribute,1,living); + try(var attributes=mockStatic(Attributes.class,withSettings().mockMaker(org.mockito.MockMakers.INLINE))) { + attributes.when(() -> Attributes.getByName("minecraft:scale")).thenReturn(protocol); + expression.change(event,new Object[]{3},ChangeMode.ADD); + verify(living.getAttributes()).setAttribute(protocol,4.0); + } + } + + @Test + void emptyEntityListsDoNotResolveProtocolAttributes() { + var attribute=mock(Attribute.class, withSettings().mockMaker(org.mockito.MockMakers.PROXY)); + Probe expression=expression(attribute,0); + expression.change(event,null,ChangeMode.RESET); + verifyNoInteractions(attribute); + } + private WrapperLivingEntity living() { + var living=mock(WrapperLivingEntity.class); + var attributes=mock(me.tofaa.entitylib.wrapper.WrapperEntityAttributes.class,withSettings().mockMaker(org.mockito.MockMakers.INLINE)); + when(living.getAttributes()).thenReturn(attributes); + return living; + } + + private Probe expression(Attribute attribute,int pattern,WrapperEntity... entities) { + Expression attributes = mock(Expression.class); + when(attributes.getSingle(event)).thenReturn(attribute); + Expression source = mock(Expression.class); + when(source.getAll(event)).thenReturn(entities); + Probe expression = new Probe(); + Expression[] expressions=pattern==0?new Expression[]{attributes,source}:new Expression[]{source,attributes}; + assertTrue(expression.init(expressions,pattern,Kleenean.FALSE,new ParseResult("",expressions))); + return expression; + } + + static class Probe extends ExprFakeLivingEntityAttribute { + Number[] read(Event event,WrapperEntity[] source) { return get(event,source); } + } +} + + + + + diff --git a/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java new file mode 100644 index 0000000..f5e63c4 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java @@ -0,0 +1,132 @@ +package org.crebs.skpackets.elements.entity.section; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.crebs.skpackets.api.entity.metadata.EntityMeta; +import org.crebs.skpackets.api.util.field.BaseFieldRegistry; +import org.crebs.skpackets.api.util.field.FieldAccessor; +import org.crebs.skpackets.api.util.field.FieldSchema; +import org.crebs.skpackets.api.util.field.FieldSections; +import org.crebs.skpackets.elements.entity.field.entity.FakeEntityFieldRegistry; +import org.crebs.skpackets.elements.entity.field.meta.MetaFieldRegistry; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EntityCreationSectionsTest { + @BeforeAll + static void bootstrap() { PacketEventsTestBootstrap.initialize(); } + + @SuppressWarnings("rawtypes") + private Map registry(Object owner) throws Exception { + Field field = BaseFieldRegistry.class.getDeclaredField("REGISTRY"); + field.setAccessible(true); + return (Map) field.get(owner); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @SuppressWarnings({"unchecked", "rawtypes"}) + void typedEntityCreationPassesPersistenceAndReturnsConstructedEntity(boolean persistent) throws Exception { + Map registry = registry(FakeEntityFieldRegistry.INSTANCE); + Map original = new HashMap(registry); + WrapperEntity created = mock(WrapperEntity.class); + EntityType type = EntityTypes.ZOMBIE; + FieldAccessor flag = new FieldAccessor<>("persistent", new String[0], Boolean.class, true, ignored -> false, null); + FieldSchema schema = new FieldSchema<>(type, List.of(flag), context -> { + assertSame(type, context.getKey()); + assertEquals(persistent, context.getOptional("persistent", Boolean.class)); + return created; + }); + registry.put(type, schema); + Expression[] inputs = {mock(Expression.class)}; + ParseResult result = new ParseResult("", inputs); + if (persistent) result.tags.add("persistent"); + try (var fields = mockStatic(FieldSections.class, withSettings().mockMaker(INLINE).defaultAnswer(CALLS_REAL_METHODS)); + var conversion = mockStatic(SpigotConversionUtil.class, withSettings().mockMaker(INLINE))) { + fields.when(() -> FieldSections.entityType(inputs[0])).thenReturn(org.bukkit.entity.EntityType.ZOMBIE); + conversion.when(() -> SpigotConversionUtil.fromBukkitEntityType(org.bukkit.entity.EntityType.ZOMBIE)).thenReturn(type); + var expression = new SecExprNewFakeEntity(); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, result, null, null)); + assertArrayEquals(new WrapperEntity[]{created}, expression.get(null)); + assertTrue(expression.isSingle()); + assertEquals(WrapperEntity.class, expression.getReturnType()); + } finally { + registry.clear(); registry.putAll(original); + } + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + void metaCreationUsesResolvedSchemaAndHandlesNullConstructorResults() throws Exception { + Map registry = registry(MetaFieldRegistry.INSTANCE); + Map original = new HashMap(registry); + EntityType type = EntityTypes.ZOMBIE; + EntityMeta meta = mock(EntityMeta.class, withSettings().mockMaker(INLINE)); + var results = new java.util.concurrent.atomic.AtomicReference<>(meta); + FieldSchema schema = new FieldSchema<>(type, List.of(), ignored -> results.get()); + registry.put(type, schema); + Expression[] inputs = {mock(Expression.class)}; + try (var fields = mockStatic(FieldSections.class, withSettings().mockMaker(INLINE).defaultAnswer(CALLS_REAL_METHODS)); + var conversion = mockStatic(SpigotConversionUtil.class, withSettings().mockMaker(INLINE))) { + fields.when(() -> FieldSections.entityType(inputs[0])).thenReturn(org.bukkit.entity.EntityType.ZOMBIE); + conversion.when(() -> SpigotConversionUtil.fromBukkitEntityType(org.bukkit.entity.EntityType.ZOMBIE)).thenReturn(type); + var expression = new SecExprNewMeta(); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new ParseResult("", inputs), null, null)); + assertArrayEquals(new EntityMeta[]{meta}, expression.get(null)); + results.set(null); + assertArrayEquals(new EntityMeta[0], expression.get(null)); + assertTrue(expression.isSingle()); + assertEquals(EntityMeta.class, expression.getReturnType()); + } finally { + registry.clear(); registry.putAll(original); + } + } + + @Test + void genericEntityCreationRequiresSection() { + try (var logging = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + var expression = new SecExprNewFakeEntity(); + Expression[] inputs = new Expression[0]; + assertFalse(expression.init(inputs, 1, Kleenean.FALSE, new ParseResult("", inputs), null, null)); + logging.verify(() -> Skript.error(contains("requires a section"))); + } + } + + @Test + void genericEntityDiagnosticDoesNotRequireTypedEntity() throws Exception { + var expression = new SecExprNewFakeEntity(); + Field generic = SecExprNewFakeEntity.class.getDeclaredField("generic"); + generic.setAccessible(true); + generic.setBoolean(expression, true); + assertEquals("a new fake entity", expression.toString(null, false)); + assertEquals("a new fake entity", expression.toString(null, true)); + } + + @Test + void invalidEntityTypeRejectsBothSections() { + try (var logging = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + Expression[] inputs = {mock(Expression.class)}; + var result = new ParseResult("", inputs); + assertFalse(new SecExprNewFakeEntity().init(inputs, 0, Kleenean.FALSE, result, null, null)); + assertFalse(new SecExprNewMeta().init(inputs, 0, Kleenean.FALSE, result, null, null)); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java b/src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java new file mode 100644 index 0000000..490843b --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java @@ -0,0 +1,40 @@ +package org.crebs.skpackets.elements.general.condition; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.PacketSide; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CondPacketTypeIsBoundTest { + @ParameterizedTest + @CsvSource({"client, SERVER, true", "client, CLIENT, false", "server, CLIENT, true", "server, SERVER, false"}) + void checksDestinationRatherThanSendingSide(String tag, PacketSide side, boolean expected) { + CondPacketTypeIsBound condition = condition(tag, 0); + PacketTypeCommon packetType = mock(PacketTypeCommon.class); + when(packetType.getSide()).thenReturn(side); + assertEquals(expected, condition.check(packetType)); + } + + @ParameterizedTest + @CsvSource({"client, 0, false", "client, 1, true", "server, 0, false", "server, 1, true"}) + void preservesNegatedPattern(String tag, int pattern, boolean negated) { + assertEquals(negated, condition(tag, pattern).isNegated()); + } + + private CondPacketTypeIsBound condition(String tag, int pattern) { + Expression[] expressions = new Expression[]{mock(Expression.class)}; + SkriptParser.ParseResult result = new SkriptParser.ParseResult("", expressions); + result.tags.add(tag); + CondPacketTypeIsBound condition = new CondPacketTypeIsBound(); + assertTrue(condition.init(expressions, pattern, Kleenean.FALSE, result)); + return condition; + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/effects/EffCancelPacketTest.java b/src/test/java/org/crebs/skpackets/elements/general/effects/EffCancelPacketTest.java new file mode 100644 index 0000000..42ee3d5 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/effects/EffCancelPacketTest.java @@ -0,0 +1,52 @@ +package org.crebs.skpackets.elements.general.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.general.packet.PacketSendOrReceiveEvent; +import org.crebs.skpackets.elements.general.event.EvtPacketSendOrReceive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EffCancelPacketTest { + @ParameterizedTest + @CsvSource({"true,NETTY,NORMAL,true", "false,NETTY,NORMAL,false", "true,SYNC,MONITOR,false", "true,NETTY,MONITOR,false"}) + void onlyInitializesInMutableNettyPacketEvents(boolean packetEvent, EvtPacketSendOrReceive.ProcessType process, PacketListenerPriority priority, boolean expected) { + ParserInstance parser = mock(ParserInstance.class, withSettings().mockMaker(INLINE)); + EvtPacketSendOrReceive.PacketSendOrReceiveParserData data = mock(EvtPacketSendOrReceive.PacketSendOrReceiveParserData.class); + when(parser.isCurrentEvent(PacketSendOrReceiveEvent.class)).thenReturn(packetEvent); + when(parser.getData(EvtPacketSendOrReceive.PacketSendOrReceiveParserData.class)).thenReturn(data); + when(data.getProcessType()).thenReturn(process); + when(data.getPriority()).thenReturn(priority); + try (MockedStatic instances = mockStatic(ParserInstance.class, withSettings().mockMaker(INLINE)); + MockedStatic skript = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + instances.when(ParserInstance::get).thenReturn(parser); + Expression[] expressions = new Expression[0]; + assertEquals(expected, new EffCancelPacket().init(expressions, 0, Kleenean.FALSE, new SkriptParser.ParseResult("", expressions))); + } + } + + @Test + void cancelsOnlyNettyEvents() { + EffCancelPacket effect = new EffCancelPacket(); + PacketSendOrReceiveEvent.NettyPacketEvent event = mock(PacketSendOrReceiveEvent.NettyPacketEvent.class); + effect.execute(event); + verify(event).setCancelled(true); + Event unrelated = mock(Event.class); + effect.execute(unrelated); + verifyNoInteractions(unrelated); + PacketSendOrReceiveEvent.SyncPacketEvent sync = mock(PacketSendOrReceiveEvent.SyncPacketEvent.class); + effect.execute(sync); + verifyNoInteractions(sync); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/effects/EffFetchSkinTest.java b/src/test/java/org/crebs/skpackets/elements/general/effects/EffFetchSkinTest.java new file mode 100644 index 0000000..323fa7d --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/effects/EffFetchSkinTest.java @@ -0,0 +1,55 @@ +package org.crebs.skpackets.elements.general.effects; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.entity.Skin; +import org.crebs.skpackets.api.entity.SkinManager; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; + +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EffFetchSkinTest { + @Test + void writesFetchedSkinToTheDestination() throws Exception { + Event event = mock(Event.class); + Expression name = mock(Expression.class); + Expression destination = mock(Expression.class); + when(name.getSingle(event)).thenReturn("player1"); + Skin skin = new Skin("value", "signature"); + EffFetchSkin effect = effect(name, destination); + try (MockedStatic manager = mockStatic(SkinManager.class, withSettings().mockMaker(INLINE))) { + manager.when(() -> SkinManager.getOfflinePlayer("player1")).thenReturn(skin); + effect.execute(event); + verify(destination).change(event, new Skin[]{skin}, ChangeMode.SET); + } + } + + @Test + void failedFetchLeavesDestinationUnchanged() throws Exception { + Event event = mock(Event.class); + Expression name = mock(Expression.class); + Expression destination = mock(Expression.class); + when(name.getSingle(event)).thenReturn("player2"); + try (MockedStatic manager = mockStatic(SkinManager.class, withSettings().mockMaker(INLINE))) { + manager.when(() -> SkinManager.getOfflinePlayer("player2")).thenThrow(new IllegalStateException("offline")); + effect(name, destination).execute(event); + verifyNoInteractions(destination); + } + } + + private EffFetchSkin effect(Expression name, Expression destination) throws Exception { + EffFetchSkin effect = new EffFetchSkin(); + Field nameField = EffFetchSkin.class.getDeclaredField("nameExpr"); + nameField.setAccessible(true); + nameField.set(effect, name); + Field destinationField = EffFetchSkin.class.getDeclaredField("expression"); + destinationField.setAccessible(true); + destinationField.set(effect, destination); + return effect; + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/effects/EffSendOrReceivePacketTest.java b/src/test/java/org/crebs/skpackets/elements/general/effects/EffSendOrReceivePacketTest.java new file mode 100644 index 0000000..9901eb2 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/effects/EffSendOrReceivePacketTest.java @@ -0,0 +1,84 @@ +package org.crebs.skpackets.elements.general.effects; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.player.User; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EffSendOrReceivePacketTest { + @ParameterizedTest + @CsvSource({"send,false", "send,true", "receive,false", "receive,true"}) + void dispatchesEveryPacketToEveryConnectedPlayer(String direction, boolean silent) { + Event event = mock(Event.class); + Player player1 = mock(Player.class); + Player disconnectedPlayer = mock(Player.class); + Player player2 = mock(Player.class); + User player1User = mock(User.class); + User player2User = mock(User.class); + PacketWrapper a = mock(PacketWrapper.class); + PacketWrapper b = mock(PacketWrapper.class); + Expression> packets = mock(Expression.class); + Expression players = mock(Expression.class); + when(packets.getAll(event)).thenReturn(new PacketWrapper[]{a, b}); + when(players.getAll(event)).thenReturn(new Player[]{player1, disconnectedPlayer, player2}); + PacketEventsAPI api = mock(PacketEventsAPI.class, RETURNS_DEEP_STUBS); + when(api.getPlayerManager().getUser(player1)).thenReturn(player1User); + when(api.getPlayerManager().getUser(player2)).thenReturn(player2User); + try (MockedStatic packetEvents = mockStatic(PacketEvents.class, withSettings().mockMaker(INLINE))) { + packetEvents.when(PacketEvents::getAPI).thenReturn(api); + EffSendOrReceivePacket effect = new EffSendOrReceivePacket(); + SkriptParser.ParseResult result = new SkriptParser.ParseResult("", new Expression[0]); + result.tags.add(direction); + if (silent) { + result.tags.add("silently"); + } + + assertTrue(effect.init(new Expression[]{packets, players}, 0, Kleenean.FALSE, result)); + effect.execute(event); + + for (User user : new User[]{player1User, player2User}) { + for (PacketWrapper packet : new PacketWrapper[]{a, b}) { + if (direction.equals("send")) { + if (silent) { + verify(user).sendPacketSilently(packet); + } else { + verify(user).sendPacket(packet); + } + } else if (silent) { + verify(user).receivePacketSilently(packet); + } else { + verify(user).receivePacket(packet); + } + } + + verifyNoMoreInteractions(user); + } + } + } + + @Test + void emptyPacketsNeverAccessPacketEvents() { + Event event = mock(Event.class); + Expression packets = mock(Expression.class); + Expression players = mock(Expression.class); + EffSendOrReceivePacket effect = new EffSendOrReceivePacket(); + assertTrue(effect.init(new Expression[]{packets, players}, 0, Kleenean.FALSE, new SkriptParser.ParseResult("", new Expression[0]))); + try (MockedStatic api = mockStatic(PacketEvents.class, withSettings().mockMaker(INLINE))) { + effect.execute(event); + api.verifyNoInteractions(); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java b/src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java new file mode 100644 index 0000000..c4183ff --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java @@ -0,0 +1,97 @@ +package org.crebs.skpackets.elements.general.event; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.event.ProtocolPacketEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.general.packet.PacketSendOrReceiveEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class EvtPacketSendOrReceiveTest { + @ParameterizedTest + @CsvSource({"netty,NORMAL", "sync,MONITOR", "async,MONITOR"}) + void defaultsPriorityForEachProcessingThread(String processing, PacketListenerPriority priority) { + EvtPacketSendOrReceive syntax = syntax(null, processing); + PacketSendOrReceiveEvent event = event(processing, priority); + assertTrue(syntax.check(event)); + assertTrue(syntax.canExecuteAsynchronously()); + assertFalse(syntax.check(mock(Event.class))); + assertFalse(syntax.check(event(processing, PacketListenerPriority.LOWEST))); + assertTrue(syntax.toString(null, false).contains(priority.name().toLowerCase())); + } + + @Test + void filtersPacketTypeAndThread() { + PacketTypeCommon expected = mock(PacketTypeCommon.class); + EvtPacketSendOrReceive syntax = syntax(expected, "sync"); + PacketSendOrReceiveEvent matching = event("sync", PacketListenerPriority.MONITOR); + when(matching.getEvent().getPacketType()).thenReturn(expected); + assertTrue(syntax.check(matching)); + when(matching.getEvent().getPacketType()).thenReturn(mock(PacketTypeCommon.class)); + assertFalse(syntax.check(matching)); + assertFalse(syntax.check(event("async", PacketListenerPriority.MONITOR))); + } + + @ParameterizedTest + @CsvSource({"sync,low", "async,normal", "sync,highest"}) + void rejectsMutablePriorityAfterProcessing(String thread, String priority) { + try (MockedStatic skript = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + EvtPacketSendOrReceive syntax = new EvtPacketSendOrReceive(); + assertFalse(syntax.init(new Literal[0], 0, result(thread, priority))); + skript.verify(() -> Skript.error(anyString())); + } + } + + @Test + void nettyAliasUsesSameFiltering() { + EvtNettyPacket syntax = new EvtNettyPacket(); + assertTrue(syntax.init(new Literal[0], 0, result("high"))); + assertTrue(syntax.check(event("netty", PacketListenerPriority.HIGH))); + assertFalse(syntax.check(event("sync", PacketListenerPriority.HIGH))); + } + + private EvtPacketSendOrReceive syntax(PacketTypeCommon type, String... tags) { + EvtPacketSendOrReceive syntax = new EvtPacketSendOrReceive(); + Literal[] literals = new Literal[0]; + if (type != null) { + Literal literal = mock(Literal.class); + when(literal.getSingle()).thenReturn(type); + literals = new Literal[]{literal}; + } + + assertTrue(syntax.init(literals, 0, result(tags))); + return syntax; + } + + private SkriptParser.ParseResult result(String... tags) { + SkriptParser.ParseResult result = new SkriptParser.ParseResult("", new Expression[0]); + result.tags.addAll(List.of(tags)); + return result; + } + + private PacketSendOrReceiveEvent event(String thread, PacketListenerPriority priority) { + Class type = switch (thread) { + case "sync" -> PacketSendOrReceiveEvent.SyncPacketEvent.class; + case "async" -> PacketSendOrReceiveEvent.AsyncPacketEvent.class; + default -> PacketSendOrReceiveEvent.NettyPacketEvent.class; + }; + PacketSendOrReceiveEvent event = mock(type); + when(event.getPriority()).thenReturn(priority); + when(event.getEvent()).thenReturn(mock(ProtocolPacketEvent.class)); + return event; + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java b/src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java new file mode 100644 index 0000000..c98535b --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java @@ -0,0 +1,39 @@ +package org.crebs.skpackets.elements.general.expressions; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.entity.Skin; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ExprSkinFromValueTest { + @Test + void createsSignedAndUnsignedSkins() { + Event event = mock(Event.class); + Expression value = mock(Expression.class); + Expression signature = mock(Expression.class); + when(value.getSingle(event)).thenReturn("texture"); + when(signature.getSingle(event)).thenReturn("signature"); + ExprSkinFromValue expression = expression(value, signature); + assertArrayEquals(new Skin[]{new Skin("texture", "signature")}, expression.get(event)); + assertArrayEquals(new Skin[]{new Skin("texture", null)}, expression(value, null).get(event)); + assertTrue(expression.isSingle()); + assertSame(Skin.class, expression.getReturnType()); + } + + @Test + void missingValueReturnsNoSkin() { + assertEquals(0, expression(mock(Expression.class), null).get(mock(Event.class)).length); + } + + private ExprSkinFromValue expression(Expression value, Expression signature) { + ExprSkinFromValue expression = new ExprSkinFromValue(); + assertTrue(expression.init(new Expression[]{value, signature}, 0, Kleenean.FALSE, new SkriptParser.ParseResult("", new Expression[0]))); + return expression; + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityIdTest.java b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityIdTest.java new file mode 100644 index 0000000..b509059 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityIdTest.java @@ -0,0 +1,79 @@ +package org.crebs.skpackets.elements.general.expressions.prop; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ExprEntityIdTest { + @ParameterizedTest + @ValueSource(ints = {0, 42, Integer.MAX_VALUE}) + void returnsPlayerEntityId(int id) { + Player player = mock(Player.class); + when(player.getEntityId()).thenReturn(id); + assertEquals(id, new ExprEntityId().convert(player)); + } + + @Test + void supportsNonPlayerEntities() { + Entity entity = mock(Entity.class); + when(entity.getEntityId()).thenReturn(73); + assertEquals(73, new ExprEntityId().convert(entity)); + } + + @Test + void sharedPacketEntityIdSupportsFakeEntitiesAndRealPlayers() { + var fake = mock(me.tofaa.entitylib.wrapper.WrapperEntity.class); + Player player1 = mock(Player.class); + when(fake.getEntityId()).thenReturn(50); + when(player1.getEntityId()).thenReturn(32); + ExprEntityId expression = new ExprEntityId(); + assertEquals(32, expression.convert(player1)); + assertEquals(50, expression.convert(fake)); + assertNull(expression.convert("player1")); + } + + @Test + void packetEntityIdHasOneRegistrationForBothSourceTypes() { + org.crebs.skpackets.support.PacketEventsTestBootstrap.initialize(); + org.crebs.skpackets.elements.entity.field.entity.FakeEntityFieldRegistry.INSTANCE.registerAll(); + java.util.List patterns = new java.util.ArrayList<>(); + java.util.List sourceTypes = new java.util.ArrayList<>(); + var registration = mock(com.github.shanebeee.skr.Registration.class, call -> { + if (!call.getMethod().getName().equals("newPropertyExpression")) { + return RETURNS_DEFAULTS.answer(call); + } + Object[] arguments = call.getRawArguments(); + sourceTypes.add((String) arguments[3]); + patterns.addAll(java.util.List.of(ch.njol.skript.expressions.base.SimplePropertyExpression.getPatterns((String) arguments[2], (String) arguments[3]))); + return mock(call.getMethod().getReturnType(), RETURNS_SELF); + }); + ExprEntityId.register(registration); + org.crebs.skpackets.elements.entity.expressions.entity.ExprFakeEntityEntityId.register(registration); + assertEquals("entity/packetentity", sourceTypes.getFirst()); + long sharedMatches = patterns.stream() + .map(pattern -> ch.njol.skript.patterns.PatternCompiler.compile(pattern.replaceAll("%[^%]+%", ""))) + .filter(pattern -> pattern.match("packet entity id of value") != null) + .count(); + assertEquals(1, sharedMatches); + assertTrue(patterns.stream() + .map(pattern -> ch.njol.skript.patterns.PatternCompiler.compile(pattern.replaceAll("%[^%]+%", ""))) + .anyMatch(pattern -> pattern.match("packet entity field id of value") != null)); + } + + @Test + void readsEachPlayersOwnId() { + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + when(player1.getEntityId()).thenReturn(12); + when(player2.getEntityId()).thenReturn(34); + ExprEntityId expression = new ExprEntityId(); + assertEquals(12, expression.convert(player1)); + assertEquals(34, expression.convert(player2)); + assertEquals(12, expression.convert(player1)); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketFieldTest.java b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketFieldTest.java new file mode 100644 index 0000000..b43060a --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketFieldTest.java @@ -0,0 +1,159 @@ +package org.crebs.skpackets.elements.general.expressions.prop; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEffect; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityVelocity; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import org.bukkit.Bukkit; +import org.bukkit.block.data.BlockData; +import org.bukkit.event.Event; +import org.bukkit.plugin.PluginManager; +import org.crebs.skpackets.api.general.packet.PacketSendOrReceiveEvent; +import org.crebs.skpackets.api.util.field.FieldAccessor; +import org.crebs.skpackets.elements.general.event.EvtPacketSendOrReceive.PacketSendOrReceiveParserData; +import org.crebs.skpackets.elements.general.field.PacketFieldRegistry; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ExprPacketFieldTest { + @BeforeAll + static void initializeFields() { + PacketEventsTestBootstrap.initialize(); + PluginManager plugins = mock(PluginManager.class); + try (var bukkit = mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::getPluginManager).thenReturn(plugins); + PacketFieldRegistry.INSTANCE.registerAll(); + } + } + + @Test + void readsAndSetsScalarPacketFields() { + Event event = mock(Event.class); + PacketWrapper packet = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); + AtomicReference value = new AtomicReference<>("before"); + FieldAccessor, String> accessor = new FieldAccessor<>("review text", new String[0], String.class, false, ignored -> value.get(), (ignored, next) -> value.set(next)); + Probe expression = expression(accessor, packet, event); + assertArrayEquals(new Object[]{"before"}, expression.read(event, packet)); + expression.change(event, new Object[]{"after"}, ChangeMode.SET); + assertEquals("after", value.get()); + expression.change(event, new Object[]{"ignored"}, ChangeMode.ADD); + expression.change(event, null, ChangeMode.SET); + expression.change(event, new Object[0], ChangeMode.SET); + assertEquals("after", value.get()); + assertSame(String.class, expression.getReturnType()); + assertTrue(expression.isSingle()); + } + + @Test + void flattensArrayFieldsAndReportsPlurality() { + Event event = mock(Event.class); + PacketWrapper packet = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); + FieldAccessor, String[]> accessor = new FieldAccessor<>("review names", new String[0], String[].class, false, ignored -> new String[]{"player1", "player2"}, null); + Probe expression = expression(accessor, packet, event); + assertArrayEquals(new Object[]{"player1", "player2"}, expression.read(event, packet)); + assertSame(String.class, expression.getReturnType()); + assertFalse(expression.isSingle()); + try (var skript = mockStatic(Skript.class)) { + assertNull(expression.acceptChange(ChangeMode.DELETE)); + skript.verify(() -> Skript.error("Cannot set review names because it is a read-only field.")); + } + } + + @Test + void sharedDataAliasReadsBothScalarTypesWithoutArrayStoreFailure() { + Event event = mock(Event.class); + WrapperPlayServerEffect effect = mock(WrapperPlayServerEffect.class, RETURNS_DEEP_STUBS); + WrapperPlayServerBlockChange block = mock(WrapperPlayServerBlockChange.class, RETURNS_DEEP_STUBS); + when(effect.getPacketTypeData().getPacketType()).thenReturn(PacketType.Play.Server.EFFECT); + when(block.getPacketTypeData().getPacketType()).thenReturn(PacketType.Play.Server.BLOCK_CHANGE); + when(effect.getData()).thenReturn(1); + BlockData data = mock(BlockData.class); + try (var conversion = mockStatic(SpigotConversionUtil.class)) { + conversion.when(() -> SpigotConversionUtil.toBukkitBlockData(block.getBlockState())).thenReturn(data); + Probe expression = initialized("data", effect, event, null); + assertSame(Object.class, expression.getReturnType()); + assertArrayEquals(new Object[]{1}, expression.read(event, effect)); + assertArrayEquals(new Object[]{data}, expression.read(event, block)); + assertTrue(expression.isSingle()); + } + } + + @Test + void typedMetadataEventStillResolvesTheAccessedVelocityPacketsSchema() { + Event event = mock(Event.class); + WrapperPlayServerEntityVelocity velocity = mock(WrapperPlayServerEntityVelocity.class, RETURNS_DEEP_STUBS); + when(velocity.getPacketTypeData().getPacketType()).thenReturn(PacketType.Play.Server.ENTITY_VELOCITY); + when(velocity.getEntityId()).thenReturn(123); + Probe expression = initialized("entity id", velocity, event, PacketType.Play.Server.ENTITY_METADATA); + assertArrayEquals(new Object[]{123}, expression.read(event, velocity)); + } + + @Test + void writableSharedAliasAcceptsBothRuntimeValueTypes() { + Event event = mock(Event.class); + PacketWrapper packet = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); + Probe expression = initialized("data", packet, event, null); + ParserInstance parser = mock(ParserInstance.class); + try (var parsers = mockStatic(ParserInstance.class)) { + parsers.when(ParserInstance::get).thenReturn(parser); + assertTrue(java.util.Set.of(expression.acceptChange(ChangeMode.SET)).containsAll(java.util.Set.of(Number.class, BlockData.class))); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Probe expression(FieldAccessor, ?> accessor, PacketWrapper packet, Event event) { + PacketTypeCommon type = mock(PacketTypeCommon.class); + when(packet.getPacketTypeData().getPacketType()).thenReturn(type); + PacketFieldRegistry.INSTANCE.builder(type, (Class) PacketWrapper.class) + .requiredField((Class) accessor.expectedType(), (Function) accessor.getter(), (BiConsumer) accessor.setter(), accessor.name()) + .build(); + return initialized(accessor.name(), packet, event, null); + } + + @SuppressWarnings("unchecked") + private Probe initialized(String field, PacketWrapper packet, Event event, PacketTypeCommon eventType) { + Expression> source = mock(Expression.class); + when(source.getArray(event)).thenReturn(new PacketWrapper[]{packet}); + when(source.isSingle()).thenReturn(true); + ParserInstance parser = mock(ParserInstance.class); + PacketSendOrReceiveParserData data = mock(PacketSendOrReceiveParserData.class); + when(parser.isCurrentEvent(PacketSendOrReceiveEvent.class)).thenReturn(eventType != null); + when(parser.getData(PacketSendOrReceiveParserData.class)).thenReturn(data); + when(data.getPacketType()).thenReturn(eventType); + Expression[] inputs = {source}; + ParseResult result = new ParseResult("", inputs); + var matcher = Pattern.compile(Pattern.quote(field)).matcher(field); + assertTrue(matcher.find()); + result.regexes.add(matcher); + Probe expression = new Probe(); + try (var parsers = mockStatic(ParserInstance.class)) { + parsers.when(ParserInstance::get).thenReturn(parser); + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, result)); + } + return expression; + } + + static class Probe extends ExprPacketField { + Object[] read(Event event, PacketWrapper packet) { + return get(event, new PacketWrapper[]{packet}); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketPacketTypeTest.java b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketPacketTypeTest.java new file mode 100644 index 0000000..df71097 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketPacketTypeTest.java @@ -0,0 +1,32 @@ +package org.crebs.skpackets.elements.general.expressions.prop; + +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.*; + +class ExprPacketPacketTypeTest { + @Test + void returnsThePacketsType() { + PacketWrapper packet = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); + PacketTypeCommon type = mock(PacketTypeCommon.class); + when(packet.getPacketTypeData().getPacketType()).thenReturn(type); + assertSame(type, new ExprPacketPacketType().convert(packet)); + } + + @Test + void keepsDifferentPacketTypesSeparate() { + PacketWrapper first = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); + PacketWrapper second = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); + PacketTypeCommon firstType = mock(PacketTypeCommon.class); + PacketTypeCommon secondType = mock(PacketTypeCommon.class); + when(first.getPacketTypeData().getPacketType()).thenReturn(firstType); + when(second.getPacketTypeData().getPacketType()).thenReturn(secondType); + ExprPacketPacketType expression = new ExprPacketPacketType(); + assertSame(firstType, expression.convert(first)); + assertSame(secondType, expression.convert(second)); + assertSame(firstType, expression.convert(first)); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPlayerSkinTest.java b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPlayerSkinTest.java new file mode 100644 index 0000000..d878ae5 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPlayerSkinTest.java @@ -0,0 +1,36 @@ +package org.crebs.skpackets.elements.general.expressions.prop; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.crebs.skpackets.api.entity.Skin; +import org.crebs.skpackets.api.general.PlayerSkinRegistry; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class ExprPlayerSkinTest { + @Test + void readsOriginalSkinByPlayerUuid() { + Server server = mock(Server.class, RETURNS_DEEP_STUBS); + when(server.getServerConfig().isProxyOnlineMode()).thenReturn(true); + Player player = mock(Player.class); + UUID uuid = UUID.randomUUID(); + when(player.getUniqueId()).thenReturn(uuid); + Skin skin = new Skin("texture", "signature"); + try (MockedStatic bukkit = mockStatic(Bukkit.class, withSettings().mockMaker(INLINE)); + MockedStatic registry = mockStatic(PlayerSkinRegistry.class, withSettings().mockMaker(INLINE))) { + bukkit.when(Bukkit::getServer).thenReturn(server); + registry.when(() -> PlayerSkinRegistry.getSkin(uuid)).thenReturn(skin); + ExprPlayerSkin expression = new ExprPlayerSkin(); + assertSame(skin, expression.convert(player)); + assertSame(Skin.class, expression.getReturnType()); + registry.verify(() -> PlayerSkinRegistry.getSkin(uuid)); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java b/src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java new file mode 100644 index 0000000..ac18cab --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java @@ -0,0 +1,47 @@ +package org.crebs.skpackets.elements.general.field; + +import com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import org.bukkit.Bukkit; +import org.bukkit.plugin.PluginManager; +import org.crebs.skpackets.api.util.field.FieldAccessor; +import org.crebs.skpackets.api.util.field.FieldSchema; +import org.crebs.skpackets.support.PacketEventsTestBootstrap; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +class ServerBoundPacketFieldRegistrarTest { + @BeforeAll + static void registerFields() { + PacketEventsTestBootstrap.initialize(); + PluginManager plugins = mock(PluginManager.class); + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::getPluginManager).thenReturn(plugins); + PacketFieldRegistry.INSTANCE.registerAll(); + } + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @CsvSource( + value = {"true,true", "false,false", "NULL,false"}, + nullValues = {"NULL"} + ) + void sneakingSetterHandlesNullableBoolean(Boolean value, boolean expected) { + FieldSchema> schema = PacketFieldRegistry.INSTANCE.getSchema(Client.INTERACT_ENTITY); + FieldAccessor, Boolean> field = (FieldAccessor, Boolean>) schema.getAccessor("sneaking state"); + assertNotNull(field); + assertNotNull(field.setter()); + WrapperPlayClientInteractEntity packet = mock(WrapperPlayClientInteractEntity.class); + assertDoesNotThrow(() -> field.setter().accept(packet, value)); + verify(packet).setSneaking(expected); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java b/src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java new file mode 100644 index 0000000..ff6497f --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java @@ -0,0 +1,61 @@ +package org.crebs.skpackets.elements.general.section; + +import ch.njol.skript.Skript; +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.util.field.FieldSchema; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class SecExprNewPacketTest { + @Test + void constructsPacketUsingTheSelectedSchema() throws Exception { + PacketTypeCommon type = mock(PacketTypeCommon.class); + PacketWrapper packet = mock(PacketWrapper.class); + SecExprNewPacket expression = expression(type, new FieldSchema<>(type, List.of(), context -> { + assertSame(type, context.getKey()); + return packet; + })); + assertArrayEquals(new PacketWrapper[]{packet}, expression.get(mock(Event.class))); + assertTrue(expression.isSingle()); + assertSame(PacketWrapper.class, expression.getReturnType()); + } + + @Test + void constructorReturningNullProducesNoPacket() throws Exception { + PacketTypeCommon type = mock(PacketTypeCommon.class); + SecExprNewPacket expression = expression(type, new FieldSchema<>(type, List.of(), context -> null)); + assertArrayEquals(new PacketWrapper[0], expression.get(mock(Event.class))); + } + + @Test + void invalidConstructionProducesErrorAndNoPacket() throws Exception { + PacketTypeCommon type = mock(PacketTypeCommon.class); + SecExprNewPacket expression = expression(type, new FieldSchema<>(type, List.of(), context -> { + throw new IllegalArgumentException("missing entity id"); + })); + try (MockedStatic skript = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + assertArrayEquals(new PacketWrapper[0], expression.get(mock(Event.class))); + skript.verify(() -> Skript.error("missing entity id")); + } + } + + private SecExprNewPacket expression(PacketTypeCommon type, FieldSchema> schema) throws Exception { + SecExprNewPacket expression = new SecExprNewPacket(); + Field typeField = SecExprNewPacket.class.getDeclaredField("type"); + typeField.setAccessible(true); + typeField.set(expression, type); + Field schemaField = SecExprNewPacket.class.getDeclaredField("schema"); + schemaField.setAccessible(true); + schemaField.set(expression, schema); + return expression; + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java b/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java new file mode 100644 index 0000000..a12f421 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java @@ -0,0 +1,311 @@ +package org.crebs.skpackets.elements.simple.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.util.Kleenean; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.crebs.skpackets.SkPackets; +import org.crebs.skpackets.api.entity.Skin; +import org.crebs.skpackets.api.simple.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class SimpleEffectsTest { + @BeforeAll + static void initializeItemTypesWithoutServerVersionLookup() throws ClassNotFoundException { + try (MockedStatic ignored = statics(Skript.class)) { + Class.forName(ItemType.class.getName()); + } + } + + private static MockedStatic statics(Class type) { + return mockStatic(type, withSettings().mockMaker(INLINE)); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + private static Expression values(T... values) { + Expression expression = mock(Expression.class); + when(expression.getArray(null)).thenReturn(values); + when(expression.getAll(null)).thenReturn(values); + when(expression.getSingle(null)).thenReturn(values.length == 0 ? null : values[0]); + return expression; + } + + private static void init(SyntaxElement element, int pattern, boolean persistent, Expression... values) { + SkriptParser.ParseResult result = new SkriptParser.ParseResult("", values); + if (persistent) { + result.tags.add("persistent"); + } + + assertTrue(element.init(values, pattern, Kleenean.FALSE, result)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void fakeBlocksDispatchEveryLocationAndViewer(boolean persistent) { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeBlockManager manager = mock(FakeBlockManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeBlocks()).thenReturn(manager); + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + Location a = new Location(null, 1.0, 2.0, 3.0); + Location b = new Location(null, 4.0, 5.0, 6.0); + BlockData data = mock(BlockData.class); + try (MockedStatic ignored = statics(SkPackets.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeBlock effect = new EffFakeBlock(); + init(effect, 0, persistent, values(a, b), values(data), values(player1, player2)); + effect.execute(null); + + for (Player viewer : new Player[]{player1, player2}) { + verify(manager).show(viewer, a, data, persistent); + verify(manager).show(viewer, b, data, persistent); + } + + verifyNoMoreInteractions(manager); + } + } + + @Test + void fakeBlocksResetClearAndMissingData() { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeBlockManager manager = mock(FakeBlockManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeBlocks()).thenReturn(manager); + Player player = mock(Player.class); + Location location = new Location(null, 1.0, 2.0, 3.0); + try (MockedStatic ignored = statics(SkPackets.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeBlock reset = new EffFakeBlock(); + init(reset, 1, false, values(location), values(player)); + reset.execute(null); + verify(manager).reset(player, location); + EffFakeBlock clear = new EffFakeBlock(); + init(clear, 2, false, values(player)); + clear.execute(null); + verify(manager).clear(player); + EffFakeBlock missing = new EffFakeBlock(); + init(missing, 0, false, values(location), values(), values(player)); + missing.execute(null); + verifyNoMoreInteractions(manager); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void fakeInventoryShowsAllSelectedSlots(boolean persistent) { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeInventoryManager manager = mock(FakeInventoryManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeInventory()).thenReturn(manager); + Player player = mock(Player.class); + ItemStack stack = mock(ItemStack.class); + ItemType type = mock(ItemType.class); + when(type.getRandom()).thenReturn(stack); + try (MockedStatic ignored = statics(SkPackets.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeInventorySlot effect = new EffFakeInventorySlot(); + init(effect, 0, persistent, values(0, 1, 45), values(type), values(player)); + effect.execute(null); + + for (int slot : new int[]{0, 1, 45}) { + verify(manager).show(player, slot, stack, persistent); + } + + verifyNoMoreInteractions(manager); + } + } + + @Test + void fakeInventoryRejectsInvalidSlotsButContinuesWithValidSlots() { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeInventoryManager manager = mock(FakeInventoryManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeInventory()).thenReturn(manager); + Player player = mock(Player.class); + try (MockedStatic ignored = statics(SkPackets.class); + MockedStatic skript = statics(Skript.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeInventorySlot effect = new EffFakeInventorySlot(); + init(effect, 1, false, values(-1, 0.5, Double.NaN, Double.POSITIVE_INFINITY, (long) Integer.MAX_VALUE + 1, 4), values(player)); + effect.execute(null); + verify(manager).reset(player, 4); + verifyNoMoreInteractions(manager); + skript.verify(() -> Skript.error("Fake inventory slots require nonnegative whole numbers."), times(5)); + EffFakeInventorySlot clear = new EffFakeInventorySlot(); + init(clear, 2, false, values(player)); + clear.execute(null); + verify(manager).clear(player); + } + } + + @Test + void fakeInventoryMissingItemDoesNotSendPackets() { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeInventoryManager manager = mock(FakeInventoryManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeInventory()).thenReturn(manager); + try (MockedStatic ignored = statics(SkPackets.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeInventorySlot effect = new EffFakeInventorySlot(); + init(effect, 0, false, values(1), values(), values(mock(Player.class))); + effect.execute(null); + verifyNoInteractions(manager); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void equipmentShowResetAndClear(boolean persistent) { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeEquipmentManager manager = mock(FakeEquipmentManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeEquipment()).thenReturn(manager); + LivingEntity target = mock(LivingEntity.class); + when(target.getEquipment()).thenReturn(mock(EntityEquipment.class)); + Player viewer = mock(Player.class); + ItemType type = mock(ItemType.class); + ItemStack stack = mock(ItemStack.class); + when(type.getRandom()).thenReturn(stack); + try (MockedStatic ignored = statics(SkPackets.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeEquipment show = new EffFakeEquipment(); + init(show, 0, persistent, values(EquipmentSlot.HAND, EquipmentSlot.HEAD), values(target), values(type), values(viewer)); + show.execute(null); + verify(manager).show(viewer, target, EquipmentSlot.HAND, stack, persistent); + verify(manager).show(viewer, target, EquipmentSlot.HEAD, stack, persistent); + EffFakeEquipment reset = new EffFakeEquipment(); + init(reset, 1, false, values(EquipmentSlot.HEAD), values(target), values(viewer)); + reset.execute(null); + verify(manager).reset(viewer, target, EquipmentSlot.HEAD); + EffFakeEquipment clear = new EffFakeEquipment(); + init(clear, 2, false, values(target), values(viewer)); + clear.execute(null); + verify(manager).reset(viewer, target); + verifyNoMoreInteractions(manager); + } + } + + @Test + void equipmentRejectsEntitiesWithoutEquipment() { + SkPackets plugin = mock(SkPackets.class, withSettings().mockMaker(INLINE)); + FakeEquipmentManager manager = mock(FakeEquipmentManager.class, withSettings().mockMaker(INLINE)); + when(plugin.getFakeEquipment()).thenReturn(manager); + try (MockedStatic ignored = statics(SkPackets.class); + MockedStatic skript = statics(Skript.class)) { + ignored.when(SkPackets::getInstance).thenReturn(plugin); + EffFakeEquipment effect = new EffFakeEquipment(); + init(effect, 2, false, values(mock(Entity.class), mock(LivingEntity.class)), values(mock(Player.class))); + effect.execute(null); + verifyNoInteractions(manager); + skript.verify(() -> Skript.error("Displayed equipment requires a living entity with equipment."), times(2)); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void displayedSkinUsesGlobalOrSpecificViewers(boolean persistent) { + Player target = mock(Player.class); + Player viewer = mock(Player.class); + Skin skin = new Skin("texture", "signature"); + try (MockedStatic skins = statics(PlayerSkinManager.class)) { + EffDisplayedSkin global = new EffDisplayedSkin(); + init(global, 0, persistent, values(target), values(skin), null); + global.execute(null); + skins.verify(() -> PlayerSkinManager.setGlobalSkin(target, skin, persistent)); + EffDisplayedSkin specific = new EffDisplayedSkin(); + init(specific, 0, persistent, values(target), values(skin), values(viewer)); + specific.execute(null); + skins.verify(() -> PlayerSkinManager.setSkinForViewers(target, List.of(viewer), skin, persistent)); + skins.verifyNoMoreInteractions(); + } + } + + @Test + void displayedSkinMissingSkinOrTargetsIsNoOp() { + try (MockedStatic skins = statics(PlayerSkinManager.class)) { + EffDisplayedSkin effect = new EffDisplayedSkin(); + init(effect, 0, false, values(mock(Player.class)), values(), null); + effect.execute(null); + init(effect, 0, false, values(new Player[0]), values(new Skin("texture", "signature")), null); + effect.execute(null); + skins.verifyNoInteractions(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void glowDispatchesStateToViewerIds(boolean state) { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(37); + Player viewer = mock(Player.class); + UUID uuid = UUID.randomUUID(); + when(viewer.getUniqueId()).thenReturn(uuid); + try (MockedStatic glow = statics(GlowingEntityManager.class)) { + EffGlow effect = new EffGlow(); + init(effect, 0, false, values(target), values(state), values(viewer, viewer)); + effect.execute(null); + if (state) { + glow.verify(() -> GlowingEntityManager.addGlowingReceivers(37, Set.of(uuid))); + } else { + glow.verify(() -> GlowingEntityManager.removeGlowingReceivers(37, Set.of(uuid))); + } + + glow.verifyNoMoreInteractions(); + } + } + + @Test + void glowOmittedViewersUsesOnlinePlayersAndMissingStateIsNoOp() { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(37); + Player viewer = mock(Player.class); + UUID uuid = UUID.randomUUID(); + when(viewer.getUniqueId()).thenReturn(uuid); + try (MockedStatic glow = statics(GlowingEntityManager.class); + MockedStatic bukkit = statics(Bukkit.class)) { + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(viewer)); + EffGlow effect = new EffGlow(); + init(effect, 0, false, values(target), values(true), null); + effect.execute(null); + glow.verify(() -> GlowingEntityManager.addGlowingReceivers(37, Set.of(uuid))); + init(effect, 0, false, values(target), values(), null); + effect.execute(null); + glow.verifyNoMoreInteractions(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void glowExplicitEmptyViewersDoesNotAffectOnlinePlayers(boolean state) { + Entity target = mock(Entity.class); + Player player1 = mock(Player.class); + try (MockedStatic glow = statics(GlowingEntityManager.class); + MockedStatic bukkit = statics(Bukkit.class)) { + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(player1)); + EffGlow effect = new EffGlow(); + init(effect, 0, false, values(target), values(state), values(new Player[0])); + effect.execute(null); + glow.verifyNoInteractions(); + bukkit.verifyNoInteractions(); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java b/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java new file mode 100644 index 0000000..aed4812 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java @@ -0,0 +1,128 @@ +package org.crebs.skpackets.elements.simple.expressions; + +import ch.njol.skript.classes.Changer; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.crebs.skpackets.api.simple.GlowingEntityManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class ExprGlowReceiversTest { + private ExprGlowReceivers expression(Entity... entities) { + Expression source = mock(Expression.class); + when(source.getAll(null)).thenReturn(entities); + ExprGlowReceivers expression = new ExprGlowReceivers(); + Expression[] expressions = new Expression[]{source}; + assertTrue(expression.init(expressions, 0, Kleenean.FALSE, new SkriptParser.ParseResult("", expressions))); + return expression; + } + + @Test + void readsOnlineViewersAndSkipsNullEntitiesAndOfflinePlayers() { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(16); + Player player = mock(Player.class); + UUID online = UUID.randomUUID(); + UUID offline = UUID.randomUUID(); + try (MockedStatic glow = mockStatic(GlowingEntityManager.class, withSettings().mockMaker(INLINE)); + MockedStatic bukkit = mockStatic(Bukkit.class, withSettings().mockMaker(INLINE))) { + glow.when(() -> GlowingEntityManager.getGlowingReceivers(16)).thenReturn(List.of(online, offline)); + bukkit.when(() -> Bukkit.getPlayer(online)).thenReturn(player); + assertArrayEquals(new Player[]{player}, expression().get(null, new Entity[]{null, target})); + } + } + + @ParameterizedTest + @EnumSource(Changer.ChangeMode.class) + void declaresSupportedChangeModes(Changer.ChangeMode mode) { + Class[] accepted = expression().acceptChange(mode); + if (Set.of(ChangeMode.SET, ChangeMode.ADD, ChangeMode.REMOVE, ChangeMode.RESET, ChangeMode.REMOVE_ALL).contains(mode)) { + assertArrayEquals(new Class[]{Player[].class}, accepted); + } else { + assertNull(accepted); + } + + assertEquals(Player.class, expression().getReturnType()); + } + + @ParameterizedTest + @EnumSource( + value = Changer.ChangeMode.class, + names = {"SET", "ADD", "REMOVE", "RESET", "REMOVE_ALL"} + ) + void changesOnlyNonNullTargetsUsingPlayerDelta(Changer.ChangeMode mode) { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(16); + Player player = mock(Player.class); + UUID uuid = UUID.randomUUID(); + when(player.getUniqueId()).thenReturn(uuid); + try (MockedStatic glow = mockStatic(GlowingEntityManager.class, withSettings().mockMaker(INLINE))) { + expression(null, target).change(null, new Object[]{player, "ignored", null}, mode); + switch (mode) { + case SET: + glow.verify(() -> GlowingEntityManager.setGlowingReceivers(16, List.of(uuid))); + break; + case ADD: + glow.verify(() -> GlowingEntityManager.addGlowingReceivers(16, List.of(uuid))); + break; + case REMOVE: + glow.verify(() -> GlowingEntityManager.removeGlowingReceivers(16, List.of(uuid))); + break; + case RESET: + case REMOVE_ALL: + glow.verify(() -> GlowingEntityManager.clearGlowingReceivers(16)); + } + + glow.verifyNoMoreInteractions(); + } + } + + @Test + void emptySourcesDoNotChangeGlowAndNullSetClearsReceivers() { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(16); + try (MockedStatic glow = mockStatic(GlowingEntityManager.class, withSettings().mockMaker(INLINE))) { + expression().change(null, null, ChangeMode.RESET); + glow.verifyNoInteractions(); + expression(target).change(null, null, ChangeMode.SET); + glow.verify(() -> GlowingEntityManager.setGlowingReceivers(16, List.of())); + } + } + @Test + void aSingleEntityCanReturnMultipleViewers() { + Entity target = mock(Entity.class); + when(target.getEntityId()).thenReturn(42); + Expression source = mock(Expression.class); + when(source.isSingle()).thenReturn(true); + ExprGlowReceivers expression = new ExprGlowReceivers(); + Expression[] inputs = {source}; + assertTrue(expression.init(inputs, 0, Kleenean.FALSE, new SkriptParser.ParseResult("", inputs))); + Player player1 = mock(Player.class); + Player player2 = mock(Player.class); + UUID player1Id = UUID.randomUUID(); + UUID player2Id = UUID.randomUUID(); + try (MockedStatic glow = mockStatic(GlowingEntityManager.class, withSettings().mockMaker(INLINE)); + MockedStatic bukkit = mockStatic(Bukkit.class, withSettings().mockMaker(INLINE))) { + glow.when(() -> GlowingEntityManager.getGlowingReceivers(42)).thenReturn(List.of(player1Id, player2Id)); + bukkit.when(() -> Bukkit.getPlayer(player1Id)).thenReturn(player1); + bukkit.when(() -> Bukkit.getPlayer(player2Id)).thenReturn(player2); + assertFalse(expression.isSingle()); + assertArrayEquals(new Player[]{player1, player2}, expression.get(null, new Entity[]{target})); + } + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/team/effects/EffTeamMembersTest.java b/src/test/java/org/crebs/skpackets/elements/team/effects/EffTeamMembersTest.java new file mode 100644 index 0000000..90d0c58 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/team/effects/EffTeamMembersTest.java @@ -0,0 +1,82 @@ +package org.crebs.skpackets.elements.team.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import org.bukkit.entity.Player; +import org.crebs.skpackets.SkPackets; +import org.crebs.skpackets.api.team.PacketTeamManager; +import org.crebs.skpackets.elements.team.PacketTeam; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.mockito.MockMakers; + +class EffTeamMembersTest { + @Test + void addAndRemoveResolvePlayersAndStrings() { + var plugin = mock(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE)); + var manager = mock(PacketTeamManager.class, withSettings().mockMaker(MockMakers.INLINE)); + var team = mock(PacketTeam.class, withSettings().mockMaker(MockMakers.INLINE)); + var player = mock(Player.class); + when(player.getName()).thenReturn("player1"); + when(plugin.getPacketTeams()).thenReturn(manager); + try (var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE))) { + plugins.when(SkPackets::getInstance).thenReturn(plugin); + effect(0, team, player, "player2").execute(null); + verify(manager).assign("player1", team); + verify(manager).assign("player2", team); + effect(1, team, player, "player2").execute(null); + verify(manager).remove("player1", team); + verify(manager).remove("player2", team); + } + } + + @Test + void missingTeamDoesNothing() { + try (var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE))) { + effect(0, null, "player1").execute(null); + plugins.verifyNoInteractions(); + } + } + + @Test + void rejectsNonTeamPackets() { + try (var skript = mockStatic(Skript.class, withSettings().mockMaker(MockMakers.INLINE)); var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE))) { + effect(0, mock(PacketWrapper.class), "player1").execute(null); + skript.verify(() -> Skript.error("Use a team made with 'a new fake team' or 'a new packet team'.")); + plugins.verifyNoInteractions(); + } + } + + @Test + void invalidEntriesDoNotPreventLaterValidEntries() { + var plugin = mock(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE)); + var manager = mock(PacketTeamManager.class, withSettings().mockMaker(MockMakers.INLINE)); + var team = mock(PacketTeam.class, withSettings().mockMaker(MockMakers.INLINE)); + when(plugin.getPacketTeams()).thenReturn(manager); + try (var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE)); var skript = mockStatic(Skript.class, withSettings().mockMaker(MockMakers.INLINE))) { + plugins.when(SkPackets::getInstance).thenReturn(plugin); + effect(0, team, "", "x".repeat(41), "player1").execute(null); + verify(manager).assign("player1", team); + verifyNoMoreInteractions(manager); + skript.verify(() -> Skript.error("Team entries must contain 1 to 40 characters."), times(2)); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static EffTeamMembers effect(int pattern, PacketWrapper packet, Object... entries) { + Expression members = mock(Expression.class); + Expression team = mock(Expression.class); + when(members.getArray(null)).thenReturn(entries); + when(team.getSingle(null)).thenReturn(packet); + var effect = new EffTeamMembers(); + var expressions = new Expression[]{members, team}; + assertTrue(effect.init(expressions, pattern, Kleenean.FALSE, new ParseResult("", expressions))); + return effect; + } +} + diff --git a/src/test/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeamTest.java b/src/test/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeamTest.java new file mode 100644 index 0000000..2552634 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeamTest.java @@ -0,0 +1,77 @@ +package org.crebs.skpackets.elements.team.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.entity.Player; +import org.crebs.skpackets.SkPackets; +import org.crebs.skpackets.api.team.PacketTeamManager; +import org.crebs.skpackets.elements.team.PacketTeam; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.mockito.MockMakers; + +class ExprPacketTeamTest { + @Test + void looksUpTeamsAndOmitsUnassignedEntities() { + var plugin = mock(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE)); + var manager = mock(PacketTeamManager.class, withSettings().mockMaker(MockMakers.INLINE)); + var team = mock(PacketTeam.class, withSettings().mockMaker(MockMakers.INLINE)); + var player1 = player("player1"); + var player2 = player("player2"); + when(plugin.getPacketTeams()).thenReturn(manager); + when(manager.get("player1")).thenReturn(team); + try (var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE))) { + plugins.when(SkPackets::getInstance).thenReturn(plugin); + assertArrayEquals(new PacketTeam[]{team}, expression(player1, player2).get(null, new Object[]{player1, player2})); + } + } + + @Test + void deleteAndResetClearAssignmentsButSetIsUnsupported() { + var plugin = mock(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE)); + var manager = mock(PacketTeamManager.class, withSettings().mockMaker(MockMakers.INLINE)); + when(plugin.getPacketTeams()).thenReturn(manager); + try (var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE))) { + plugins.when(SkPackets::getInstance).thenReturn(plugin); + var expression = expression(player("player1")); + expression.change(null, null, ChangeMode.DELETE); + expression.change(null, null, ChangeMode.RESET); + expression.change(null, new Object[]{mock(PacketTeam.class, withSettings().mockMaker(MockMakers.INLINE))}, ChangeMode.SET); + verify(manager, times(2)).assign("player1", null); + verifyNoMoreInteractions(manager); + } + } + + @Test + void advertisesOnlyDeletionAndReset() { + var expression = new ExprPacketTeam(); + for (var mode : ChangeMode.values()) { + if (mode == ChangeMode.DELETE || mode == ChangeMode.RESET) assertArrayEquals(new Class[]{PacketTeam.class}, expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode)); + } + assertEquals(PacketTeam.class, expression.getReturnType()); + } + + private static Player player(String name) { + var player = mock(Player.class); + when(player.getName()).thenReturn(name); + return player; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static ExprPacketTeam expression(Object... entities) { + Expression source = mock(Expression.class); + when(source.getArray(null)).thenReturn(entities); + when(source.isSingle()).thenReturn(entities.length == 1); + var expression = new ExprPacketTeam(); + var expressions = new Expression[]{source}; + assertTrue(expression.init(expressions, 0, Kleenean.FALSE, new ParseResult("", expressions))); + assertEquals(entities.length == 1, expression.isSingle()); + return expression; + } +} + diff --git a/src/test/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpressionTest.java b/src/test/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpressionTest.java new file mode 100644 index 0000000..e4004f9 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpressionTest.java @@ -0,0 +1,181 @@ +package org.crebs.skpackets.elements.team.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.util.SkriptColor; +import ch.njol.util.Kleenean; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams; +import org.crebs.skpackets.elements.team.TeamField; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.mockito.MockMakers; + +class TeamPropertyExpressionTest { + record Property(TeamPropertyExpression expression, TeamField field, Object value) {} + + static Stream properties() { + return Stream.of( + new Property(new ExprTeamName(), TeamField.NAME, "red"), + new Property(new ExprTeamEntries(), TeamField.ENTRIES, new String[]{"player1", "player2"}), + new Property(new ExprTeamDisplayName(), TeamField.DISPLAY_NAME, "Red Team"), + new Property(new ExprTeamPrefix(), TeamField.PREFIX, "[Red] "), + new Property(new ExprTeamSuffix(), TeamField.SUFFIX, "!"), + new Property(new ExprTeamColor(), TeamField.COLOR, SkriptColor.LIGHT_RED), + new Property(new ExprTeamCollisionRule(), TeamField.COLLISION_RULE, "never"), + new Property(new ExprTeamNameTagVisibility(), TeamField.NAME_TAG_VISIBILITY, "never"), + new Property(new ExprTeamFriendlyFire(), TeamField.FRIENDLY_FIRE, true), + new Property(new ExprTeamSeeInvisible(), TeamField.SEE_INVISIBLE, true)); + } + + @ParameterizedTest + @MethodSource("properties") + void readsAndWritesTheCorrectField(Property property) { + WrapperPlayServerTeams packet = packet(); + var expression = initialize(property.expression(), packet); + Object[] delta = property.value() instanceof String[] entries ? entries : new Object[]{property.value()}; + expression.change(null, delta, ChangeMode.SET); + + Object[] expected = property.value() instanceof String[] entries ? entries : new Object[]{property.value()}; + assertArrayEquals(expected, expression.get(null, new PacketWrapper[]{packet})); + assertEquals(property.field().elementType(), expression.getReturnType()); + assertEquals(property.field() != TeamField.ENTRIES, expression.isSingle()); + assertTrue(expression.toString(null, false).contains(property.field().key())); + } + + @ParameterizedTest + @MethodSource("properties") + void advertisesOnlySupportedChanges(Property property) { + var expression = property.expression(); + for (ChangeMode mode : ChangeMode.values()) { + boolean entriesChange = property.field() == TeamField.ENTRIES + && (mode == ChangeMode.ADD || mode == ChangeMode.REMOVE || mode == ChangeMode.DELETE || mode == ChangeMode.RESET); + if (mode == ChangeMode.SET) assertArrayEquals(new Class[]{property.field().type()}, expression.acceptChange(mode)); + else if (entriesChange) assertArrayEquals(new Class[]{String[].class}, expression.acceptChange(mode)); + else assertNull(expression.acceptChange(mode)); + } + } + + @ParameterizedTest + @MethodSource("properties") + void ignoresOtherPacketTypesAndHandlesEmptySources(Property property) { + PacketWrapper other = mock(PacketWrapper.class); + var expression = initialize(property.expression(), other); + assertEquals(0, expression.get(null, new PacketWrapper[]{other}).length); + assertEquals(0, expression.get(null, new PacketWrapper[0]).length); + expression.change(null, new Object[]{property.value()}, ChangeMode.SET); + verifyNoInteractions(other); + } + + @Test + void entriesChangesPreserveOrderAndRemoveDuplicates() { + var packet = packet(); + var expression = initialize(new ExprTeamEntries(), packet); + expression.change(null, new Object[]{"player1", "player2", "player1"}, ChangeMode.SET); + expression.change(null, new Object[]{"player2", "player3"}, ChangeMode.ADD); + assertArrayEquals(new String[]{"player1", "player2", "player3"}, expression.get(null, new PacketWrapper[]{packet})); + expression.change(null, new Object[]{"player2", "player4"}, ChangeMode.REMOVE); + assertArrayEquals(new String[]{"player1", "player3"}, expression.get(null, new PacketWrapper[]{packet})); + expression.change(null, null, ChangeMode.RESET); + assertEquals(0, expression.get(null, new PacketWrapper[]{packet}).length); + expression.change(null, new Object[]{"player1"}, ChangeMode.ADD); + expression.change(null, null, ChangeMode.DELETE); + assertEquals(0, expression.get(null, new PacketWrapper[]{packet}).length); + } + + @Test + void optionFlagsDoNotOverwriteEachOther() { + var packet = packet(); + var friendly = initialize(new ExprTeamFriendlyFire(), packet); + var invisible = initialize(new ExprTeamSeeInvisible(), packet); + friendly.change(null, new Object[]{true}, ChangeMode.SET); + invisible.change(null, new Object[]{true}, ChangeMode.SET); + friendly.change(null, new Object[]{false}, ChangeMode.SET); + assertArrayEquals(new Boolean[]{false}, friendly.get(null, new PacketWrapper[]{packet})); + assertArrayEquals(new Boolean[]{true}, invisible.get(null, new PacketWrapper[]{packet})); + } + + @Test + void missingTeamInfoProducesNoValuesAndCanBeInitializedByAChange() { + var packet = packet(); + when(packet.getTeamInfo()).thenReturn(Optional.empty()); + var expression = initialize(new ExprTeamPrefix(), packet); + assertEquals(0, expression.get(null, new PacketWrapper[]{packet}).length); + var info = teamInfo(); + try (var defaults = mockStatic(TeamField.class)) { + defaults.when(() -> TeamField.defaults("original")).thenReturn(info); + expression.change(null, new Object[]{"prefix"}, ChangeMode.SET); + defaults.verify(() -> TeamField.defaults("original")); + } + assertArrayEquals(new String[]{"prefix"}, expression.get(null, new PacketWrapper[]{packet})); + } + + @Test + void scalarPropertiesIgnoreMissingDeltaAndUnsupportedModes() { + var packet = packet(); + var expression = initialize(new ExprTeamName(), packet); + expression.change(null, null, ChangeMode.SET); + expression.change(null, new Object[0], ChangeMode.SET); + expression.change(null, new Object[]{"new"}, ChangeMode.ADD); + assertArrayEquals(new String[]{"original"}, expression.get(null, new PacketWrapper[]{packet})); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static > T initialize(T expression, PacketWrapper packet) { + Expression source = mock(Expression.class); + when(source.getArray(null)).thenReturn(new PacketWrapper[]{packet}); + when(source.isSingle()).thenReturn(true); + when(source.toString(null, false)).thenReturn("source"); + assertTrue(expression.init(new Expression[]{source}, 0, Kleenean.FALSE, new ParseResult("", new Expression[]{source}))); + return expression; + } + + private static WrapperPlayServerTeams packet() { + var packet = mock(WrapperPlayServerTeams.class); + when(packet.getTeamName()).thenReturn("original"); + when(packet.getPlayers()).thenReturn(List.of()); + var info = teamInfo(); + when(packet.getTeamInfo()).thenReturn(Optional.of(info)); + doAnswer(call -> { when(packet.getTeamName()).thenReturn(call.getArgument(0)); return null; }).when(packet).setTeamName(anyString()); + doAnswer(call -> { when(packet.getPlayers()).thenReturn(call.getArgument(0)); return null; }).when(packet).setPlayers(any()); + doAnswer(call -> { when(packet.getTeamInfo()).thenReturn(Optional.of(call.getArgument(0))); return null; }).when(packet).setTeamInfo(any()); + return packet; + } + + private static WrapperPlayServerTeams.ScoreBoardTeamInfo teamInfo() { + Map state = new HashMap<>(); + state.put("DisplayName", Component.text("original")); + state.put("Prefix", Component.empty()); + state.put("Suffix", Component.empty()); + state.put("Color", NamedTextColor.WHITE); + state.put("TagVisibility", WrapperPlayServerTeams.NameTagVisibility.ALWAYS); + state.put("CollisionRule", WrapperPlayServerTeams.CollisionRule.ALWAYS); + state.put("OptionData", WrapperPlayServerTeams.OptionData.NONE); + return mock(WrapperPlayServerTeams.ScoreBoardTeamInfo.class, invocation -> { + String method = invocation.getMethod().getName(); + if (method.startsWith("get") && invocation.getArguments().length == 0) { + return state.get(method.substring(3)); + } + if (method.startsWith("set") && invocation.getArguments().length == 1) { + state.put(method.substring(3), invocation.getArgument(0)); + return null; + } + return RETURNS_DEFAULTS.answer(invocation); + }); + } +} + + diff --git a/src/test/java/org/crebs/skpackets/elements/team/section/SecNewPacketTeamTest.java b/src/test/java/org/crebs/skpackets/elements/team/section/SecNewPacketTeamTest.java new file mode 100644 index 0000000..e8bbff6 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/team/section/SecNewPacketTeamTest.java @@ -0,0 +1,80 @@ +package org.crebs.skpackets.elements.team.section; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.crebs.skpackets.elements.team.TeamField; +import org.crebs.skpackets.elements.team.PacketTeam; +import org.crebs.skpackets.SkPackets; +import org.crebs.skpackets.api.team.PacketTeamManager; +import org.junit.jupiter.api.Test; +import org.mockito.MockMakers; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SecNewPacketTeamTest { + @Test + void createsAndTracksATeamFromEvaluatedEntries() throws Exception { + var section = new SecNewPacketTeam(); + fields(section).put(TeamField.NAME, value("red")); + var team = mock(PacketTeam.class, withSettings().mockMaker(MockMakers.INLINE)); + var plugin = mock(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE)); + var manager = mock(PacketTeamManager.class, withSettings().mockMaker(MockMakers.INLINE)); + when(plugin.getPacketTeams()).thenReturn(manager); + try (var teams = mockStatic(TeamField.class, withSettings().mockMaker(MockMakers.INLINE)); + var plugins = mockStatic(SkPackets.class, withSettings().mockMaker(MockMakers.INLINE))) { + teams.when(() -> TeamField.create("red")).thenReturn(team); + plugins.when(SkPackets::getInstance).thenReturn(plugin); + assertArrayEquals(new PacketTeam[]{team}, section.get(null)); + verify(team).setTeamName("red"); + verify(manager).track(team); + } + } + + @Test + void requiresASection() { + try (var skript = mockStatic(Skript.class, withSettings().mockMaker(MockMakers.INLINE))) { + assertFalse(new SecNewPacketTeam().init(new Expression[0], 0, Kleenean.FALSE, + new ParseResult("", new Expression[0]), null, List.of())); + skript.verify(() -> Skript.error("A new packet team requires a section with a name entry.")); + } + } + + @Test + void missingRuntimeNameProducesNoPacket() throws Exception { + var section = new SecNewPacketTeam(); + fields(section).put(TeamField.NAME, value(null)); + assertEquals(0, section.get(null).length); + assertTrue(section.isSingle()); + assertEquals("new packet team", section.toString(null, false)); + } + + @Test + void invalidRuntimeNameIsReported() throws Exception { + var section = new SecNewPacketTeam(); + fields(section).put(TeamField.NAME, value("x".repeat(17))); + try (var skript = mockStatic(Skript.class, withSettings().mockMaker(MockMakers.INLINE))) { + assertEquals(0, section.get(null).length); + skript.verify(() -> Skript.error("Team name must contain 1 to 16 characters.")); + } + } + + @SuppressWarnings("unchecked") + private static Map> fields(SecNewPacketTeam section) throws Exception { + var field = SecNewPacketTeam.class.getDeclaredField("fields"); + field.setAccessible(true); + return (Map>) field.get(section); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Expression value(Object value) { + Expression expression = mock(Expression.class); + when(expression.getSingle(null)).thenReturn(value); + return expression; + } +} diff --git a/src/test/java/org/crebs/skpackets/support/PacketEventsTestBootstrap.java b/src/test/java/org/crebs/skpackets/support/PacketEventsTestBootstrap.java new file mode 100644 index 0000000..15aeac8 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/support/PacketEventsTestBootstrap.java @@ -0,0 +1,25 @@ +package org.crebs.skpackets.support; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.manager.server.ServerManager; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.settings.PacketEventsSettings; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class PacketEventsTestBootstrap { + private PacketEventsTestBootstrap() {} + + public static synchronized void initialize() { + if (PacketEvents.getAPI() != null && PacketEvents.getAPI().getSettings() != null) return; + PacketEventsAPI api = mock(PacketEventsAPI.class); + PacketEventsSettings settings = new PacketEventsSettings(); + ServerManager server = mock(ServerManager.class); + when(api.getSettings()).thenReturn(settings); + when(api.getServerManager()).thenReturn(server); + when(server.getVersion()).thenReturn(ServerVersion.V_26_3); + PacketEvents.setAPI(api); + } +}