From 12c2269e5bd58352bcf17e06021e9fafb26725b0 Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Thu, 29 Oct 2026 10:41:09 +0800 Subject: [PATCH 1/8] Refactor element registration into modules --- .../java/org/crebs/skpackets/SkPackets.java | 12 +++- .../elements/ElementRegistration.java | 19 ------ .../elements/RegistrationModule.java | 18 ++++++ .../elements/entity/EntityModule.java | 51 +++++++++++++++ .../elements/entity/EntityRegister.java | 58 ----------------- ...ntRegistration.java => GeneralModule.java} | 63 +++++++------------ ...entRegistration.java => SimpleModule.java} | 29 ++++----- .../skpackets/elements/team/TeamModule.java | 34 ++++++++++ .../skpackets/elements/team/TeamRegister.java | 32 ---------- 9 files changed, 150 insertions(+), 166 deletions(-) delete mode 100644 src/main/java/org/crebs/skpackets/elements/ElementRegistration.java create mode 100644 src/main/java/org/crebs/skpackets/elements/RegistrationModule.java create mode 100644 src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java delete mode 100644 src/main/java/org/crebs/skpackets/elements/entity/EntityRegister.java rename src/main/java/org/crebs/skpackets/elements/general/{GeneralElementRegistration.java => GeneralModule.java} (59%) rename src/main/java/org/crebs/skpackets/elements/simple/{SimpleElementRegistration.java => SimpleModule.java} (68%) create mode 100644 src/main/java/org/crebs/skpackets/elements/team/TeamModule.java delete mode 100644 src/main/java/org/crebs/skpackets/elements/team/TeamRegister.java diff --git a/src/main/java/org/crebs/skpackets/SkPackets.java b/src/main/java/org/crebs/skpackets/SkPackets.java index 7e828f7..e50556d 100644 --- a/src/main/java/org/crebs/skpackets/SkPackets.java +++ b/src/main/java/org/crebs/skpackets/SkPackets.java @@ -9,7 +9,10 @@ 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.entity.EntityModule; +import org.crebs.skpackets.elements.general.GeneralModule; +import org.crebs.skpackets.elements.simple.SimpleModule; +import org.crebs.skpackets.elements.team.TeamModule; import org.crebs.skpackets.elements.ElementRuntime; public final class SkPackets extends JavaPlugin { @@ -81,7 +84,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/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/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..94a7116 --- /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.skriptlang.skript.addon.SkriptAddon; +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 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/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..5cdb6f7 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,8 @@ package org.crebs.skpackets.elements.general; import com.github.shanebeee.skr.Registration; +import org.crebs.skpackets.elements.RegistrationModule; +import org.skriptlang.skript.addon.SkriptAddon; import org.crebs.skpackets.elements.general.condition.CondPacketTypeIsBound; import org.crebs.skpackets.elements.general.effects.EffCancelPacket; import org.crebs.skpackets.elements.general.effects.EffFetchSkin; @@ -35,50 +37,29 @@ import org.crebs.skpackets.elements.general.field.PacketFieldRegistry; import org.crebs.skpackets.elements.general.section.SecExprNewPacket; -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/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..1d60830 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.skriptlang.skript.addon.SkriptAddon; import org.crebs.skpackets.elements.simple.effects.*; import org.crebs.skpackets.elements.simple.expressions.ExprGlowReceivers; -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/team/TeamModule.java b/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java new file mode 100644 index 0000000..c5e11f8 --- /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.skriptlang.skript.addon.SkriptAddon; +import org.crebs.skpackets.elements.team.effects.EffTeamMembers; +import org.crebs.skpackets.elements.team.expressions.*; +import org.crebs.skpackets.elements.team.section.SecNewPacketTeam; + +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); - } -} From 185a9dfd2b18a2a28cdf2bf9aba164201562dbbf Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Fri, 30 Oct 2026 18:31:59 +0800 Subject: [PATCH 2/8] Fixed null UUID warnings Fixed possible null Boolean unboxing in the packet sneaking field. Added entity tests and JUnit/Mockito setup. Added a test workflow, updated Gradle to 9.8.2, and changed README navigation/docs links to match the one in main. --- .github/workflows/tests.yml | 28 +++ README.md | 13 +- build.gradle.kts | 21 +- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 48749 bytes gradle/wrapper/gradle-wrapper.properties | 7 +- gradlew | 54 +++-- gradlew.bat | 93 ++++--- .../skpackets/api/general/EntityTracker.java | 4 +- .../api/simple/FakeBlockManager.java | 2 +- .../ServerBoundPacketFieldRegistrar.java | 2 +- .../entity/effects/EntityEffectsTest.java | 226 ++++++++++++++++++ .../EntityLookupExpressionsTest.java | 110 +++++++++ .../FakeEquipmentExpressionTest.java | 79 ++++++ .../entity/EntityMetaDataPropertyTest.java | 60 +++++ .../entity/FakeEntityPropertiesTest.java | 94 ++++++++ .../meta/EntityMetaExpressionsTest.java | 132 ++++++++++ .../PacketEntityMetadataExpressionsTest.java | 130 ++++++++++ .../PacketEntityMetadataValuesTest.java | 107 +++++++++ .../prop/VisibleFakeEntitiesTest.java | 44 ++++ .../ExprFakeLivingEntityAttributeTest.java | 147 ++++++++++++ .../section/EntityCreationSectionsTest.java | 120 ++++++++++ .../support/PacketEventsTestBootstrap.java | 25 ++ 22 files changed, 1436 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/EntityLookupExpressionsTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/FakeEquipmentExpressionTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/EntityMetaDataPropertyTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/FakeEntityPropertiesTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/meta/EntityMetaExpressionsTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataExpressionsTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataValuesTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/VisibleFakeEntitiesTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/living/ExprFakeLivingEntityAttributeTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java create mode 100644 src/test/java/org/crebs/skpackets/support/PacketEventsTestBootstrap.java 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..4a7490a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,8 @@ plugins { group = "org.crebs" version = "1.0" +val mockitoAgent by configurations.creating + repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") @@ -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 249e5832f090a2944b7473328c07c9755baa3196..9fa7f288283f81eec4952784c271abd7e0938dee 100644 GIT binary patch literal 48749 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 /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/api/general/EntityTracker.java b/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java index 9e49ce8..bd60e96 100644 --- a/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java +++ b/src/main/java/org/crebs/skpackets/api/general/EntityTracker.java @@ -78,6 +78,7 @@ public static EntityType getType(int entityId) { @Override 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) { @@ -111,6 +112,7 @@ private void handleDestroy(PacketSendEvent event) { @Override 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/simple/FakeBlockManager.java b/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java index 0be6710..72a649a 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java +++ b/src/main/java/org/crebs/skpackets/api/simple/FakeBlockManager.java @@ -157,7 +157,7 @@ private void send(Player viewer, Position pos, BlockData data) { 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/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/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..c3c4034 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java @@ -0,0 +1,226 @@ +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 me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +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.UUID; +import java.util.Optional; +import java.util.Set; +import java.util.List; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers; +import org.mockito.ArgumentCaptor; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +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))); + effect.execute(event); + for (WrapperEntity passenger : new WrapperEntity[]{first, second}) { + if (pattern == 0) 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 ridingRealVehiclePreservesItsServerPassengers(int pattern) { + Entity vehicle = mock(Entity.class), realPassenger = mock(Entity.class); + WrapperEntity fakePassenger = mock(WrapperEntity.class); + when(vehicle.getEntityId()).thenReturn(1); + when(realPassenger.getEntityId()).thenReturn(2); + when(fakePassenger.getEntityId()).thenReturn(3); + when(vehicle.getPassengers()).thenReturn(List.of(realPassenger)); + Expression[] inputs = {input(fakePassenger), input(vehicle)}; + var effect = new EffRideFakeEntity(); + assertTrue(effect.init(inputs, pattern, Kleenean.FALSE, new ParseResult("", inputs))); + effect.execute(event); + var packet = ArgumentCaptor.forClass(WrapperPlayServerSetPassengers.class); + verify(fakePassenger).sendPacketToViewers(packet.capture()); + assertEquals(1, packet.getValue().getEntityId()); + assertArrayEquals(pattern == 0 ? new int[]{2, 3} : new int[]{2}, packet.getValue().getPassengers()); + verify(vehicle, never()).addPassenger(any()); + verify(vehicle, never()).removePassenger(any()); + } +} + + 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..9530bf7 --- /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.WrapperLivingEntity; +import me.tofaa.entitylib.wrapper.WrapperEntityEquipment; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.event.Event; +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..8c441c4 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/EntityMetaDataPropertyTest.java @@ -0,0 +1,60 @@ +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 me.tofaa.entitylib.wrapper.WrapperEntity; +import org.crebs.skpackets.api.entity.metadata.EntityMeta; +import org.junit.jupiter.api.Test; +import org.bukkit.entity.Entity; +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 java.util.List; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockMakers.INLINE; + +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..7fbf6a0 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/entity/FakeEntityPropertiesTest.java @@ -0,0 +1,94 @@ +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.Mockito.*; +import static org.mockito.MockMakers.INLINE; + +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..f5a248d --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/meta/EntityMetaExpressionsTest.java @@ -0,0 +1,132 @@ +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.Mockito.*; +import static org.mockito.MockMakers.INLINE; + +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..6f82c8f --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataExpressionsTest.java @@ -0,0 +1,130 @@ +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.Mockito.*; +import static org.mockito.MockMakers.INLINE; + +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..2bacec0 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/metadata/PacketEntityMetadataValuesTest.java @@ -0,0 +1,107 @@ +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.*; +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..58facd2 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/prop/VisibleFakeEntitiesTest.java @@ -0,0 +1,44 @@ +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.Mockito.*; +import static org.mockito.MockMakers.INLINE; + +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..990b5c2 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java @@ -0,0 +1,120 @@ +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.Mockito.*; +import static org.mockito.MockMakers.INLINE; + +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 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/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); + } +} From d80b052316a8694131b104af6921b9db2bbcadde Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Sun, 1 Nov 2026 03:12:17 +0800 Subject: [PATCH 3/8] Add general and simple module tests --- .../condition/CondPacketTypeIsBoundTest.java | 38 +++ .../general/effects/EffCancelPacketTest.java | 52 +++ .../general/effects/EffFetchSkinTest.java | 55 ++++ .../effects/EffSendOrReceivePacketTest.java | 84 +++++ .../event/EvtPacketSendOrReceiveTest.java | 95 ++++++ .../expressions/ExprSkinFromValueTest.java | 38 +++ .../expressions/prop/ExprEntityIdTest.java | 39 +++ .../expressions/prop/ExprPacketFieldTest.java | 74 +++++ .../prop/ExprPacketPacketTypeTest.java | 32 ++ .../expressions/prop/ExprPlayerSkinTest.java | 35 ++ .../ServerBoundPacketFieldRegistrarTest.java | 46 +++ .../general/section/SecExprNewPacketTest.java | 60 ++++ .../simple/effects/SimpleEffectsTest.java | 298 ++++++++++++++++++ .../expressions/ExprGlowReceiversTest.java | 105 ++++++ 14 files changed, 1051 insertions(+) create mode 100644 src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/effects/EffCancelPacketTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/effects/EffFetchSkinTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/effects/EffSendOrReceivePacketTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityIdTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketFieldTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketPacketTypeTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPlayerSkinTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java 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..84687cc --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java @@ -0,0 +1,38 @@ +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.*; +import static org.mockito.Mockito.*; + +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..153b6e8 --- /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.*; +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..14151ec --- /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 java.lang.reflect.Field; +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 static org.junit.jupiter.api.Assertions.*; +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..6d39a7b --- /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.*; +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..0435534 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java @@ -0,0 +1,95 @@ +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 java.util.List; +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 static org.junit.jupiter.api.Assertions.*; +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..0ae1609 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java @@ -0,0 +1,38 @@ +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.*; + +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..18a7dfd --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprEntityIdTest.java @@ -0,0 +1,39 @@ +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 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..445da89 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPacketFieldTest.java @@ -0,0 +1,74 @@ +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 com.github.retrooper.packetevents.wrapper.PacketWrapper; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.util.field.FieldAccessor; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; + +class ExprPacketFieldTest { + @Test + void readsAndSetsScalarPacketFields() throws Exception { + Event event = mock(Event.class); + PacketWrapper packet = mock(PacketWrapper.class); + AtomicReference value = new AtomicReference<>("before"); + FieldAccessor, String> accessor = new FieldAccessor<>("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() throws Exception { + Event event = mock(Event.class); + PacketWrapper packet = mock(PacketWrapper.class); + FieldAccessor, String[]> accessor = new FieldAccessor<>("names", new String[0], String[].class, false, ignored -> new String[]{"a", "b"}, null); + Probe expression = expression(accessor, packet, event); + assertArrayEquals(new Object[]{"a", "b"}, expression.read(event, packet)); + assertSame(String.class, expression.getReturnType()); + assertFalse(expression.isSingle()); + try (MockedStatic skript = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + assertNull(expression.acceptChange(ChangeMode.DELETE)); + skript.verify(() -> Skript.error("Cannot set names because it is a read-only field.")); + } + } + + @SuppressWarnings("unchecked") + private Probe expression(FieldAccessor accessor, PacketWrapper packet, Event event) throws Exception { + Probe expression = new Probe(); + Field field = ExprPacketField.class.getDeclaredField("fieldAccessor"); + field.setAccessible(true); + field.set(expression, accessor); + Expression> source = mock(Expression.class); + when(source.getArray(event)).thenReturn(new PacketWrapper[]{packet}); + when(source.isSingle()).thenReturn(true); + expression.source(source); + return expression; + } + + static class Probe extends ExprPacketField { + void source(Expression expression) { + setExpr(expression); + } + + 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..eb179fd --- /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.*; +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..0431553 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/prop/ExprPlayerSkinTest.java @@ -0,0 +1,35 @@ +package org.crebs.skpackets.elements.general.expressions.prop; + +import java.util.UUID; +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 static org.junit.jupiter.api.Assertions.*; +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..7da0de3 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java @@ -0,0 +1,46 @@ +package org.crebs.skpackets.elements.general.field; + +import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; +import com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client; +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.*; +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..a0556aa --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java @@ -0,0 +1,60 @@ +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 java.lang.reflect.Field; +import java.util.List; +import org.bukkit.event.Event; +import org.crebs.skpackets.api.util.field.FieldSchema; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +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..191aaed --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java @@ -0,0 +1,298 @@ +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 java.util.List; +import java.util.Set; +import java.util.UUID; +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.FakeBlockManager; +import org.crebs.skpackets.api.simple.FakeEquipmentManager; +import org.crebs.skpackets.api.simple.FakeInventoryManager; +import org.crebs.skpackets.api.simple.GlowingEntityManager; +import org.crebs.skpackets.api.simple.PlayerSkinManager; +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 static org.junit.jupiter.api.Assertions.*; +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(); + } + } +} 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..b92af50 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java @@ -0,0 +1,105 @@ +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 java.util.List; +import java.util.Set; +import java.util.UUID; +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 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())); + } + } +} From 780c7c28b7e52dc03a5bae0530e56a19a56241a1 Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Sun, 1 Nov 2026 04:24:37 +0800 Subject: [PATCH 4/8] Bug fixes Fix packet syntax collisions, field access, glow, and fake passengers --- .../api/entity/PacketEntityManager.java | 51 +++- .../entity/RealEntityPassengerManager.java | 90 +++++++ .../general/packet/PacketTypeRegistry.java | 92 ++++--- .../api/simple/ChatSessionListener.java | 11 +- .../api/simple/GlowingEntityListener.java | 67 ++++- .../api/simple/GlowingEntityManager.java | 37 ++- .../skpackets/elements/ElementRuntime.java | 5 +- .../entity/effects/EffRideFakeEntity.java | 32 +-- .../entity/ExprFakeEntityEntityId.java | 2 +- .../entity/section/SecExprNewFakeEntity.java | 1 + .../skpackets/elements/general/Types.java | 91 +++---- .../expressions/prop/ExprEntityId.java | 13 +- .../expressions/prop/ExprPacketField.java | 163 ++++-------- .../elements/simple/effects/EffGlow.java | 5 +- .../simple/expressions/ExprGlowReceivers.java | 5 + .../RealEntityPassengerManagerTest.java | 210 ++++++++++++++++ .../packet/PacketTypeRegistryTest.java | 109 ++++++++ .../api/simple/ChatSessionListenerTest.java | 96 ++++++++ .../api/simple/GlowingEntityListenerTest.java | 81 ++++++ .../api/simple/GlowingEntityManagerTest.java | 233 ++++++++++++++++++ .../entity/effects/EntityEffectsTest.java | 41 +-- .../section/EntityCreationSectionsTest.java | 10 + .../expressions/prop/ExprEntityIdTest.java | 40 +++ .../expressions/prop/ExprPacketFieldTest.java | 131 ++++++++-- .../simple/effects/SimpleEffectsTest.java | 16 ++ .../expressions/ExprGlowReceiversTest.java | 22 ++ 26 files changed, 1340 insertions(+), 314 deletions(-) create mode 100644 src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java create mode 100644 src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java create mode 100644 src/test/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistryTest.java create mode 100644 src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java create mode 100644 src/test/java/org/crebs/skpackets/api/simple/GlowingEntityListenerTest.java create mode 100644 src/test/java/org/crebs/skpackets/api/simple/GlowingEntityManagerTest.java 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..97abfa8 100644 --- a/src/main/java/org/crebs/skpackets/api/entity/PacketEntityManager.java +++ b/src/main/java/org/crebs/skpackets/api/entity/PacketEntityManager.java @@ -8,6 +8,7 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.HandlerList; @@ -16,6 +17,9 @@ import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityRemoveEvent; +import org.bukkit.event.world.EntitiesUnloadEvent; import org.bukkit.plugin.java.JavaPlugin; import java.util.*; @@ -26,6 +30,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 +144,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 +173,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 +199,7 @@ public void remove(WrapperEntity entity) { } requireTracked(entity); entities.remove(entity.getEntityId()); + realPassengers.remove(entity); entity.remove(); } @@ -205,7 +228,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 +240,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 +254,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 +263,7 @@ public void onQuit(PlayerQuitEvent event) { && tracked.viewers().remove(id) && tracked.viewers().isEmpty()) { iterator.remove(); + realPassengers.remove(tracked.entity()); tracked.entity().remove(); } } @@ -240,16 +271,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 +310,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..0f03187 --- /dev/null +++ b/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java @@ -0,0 +1,90 @@ +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.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.UUID; + +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/general/packet/PacketTypeRegistry.java b/src/main/java/org/crebs/skpackets/api/general/packet/PacketTypeRegistry.java index f6ca430..c490838 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 @@ -26,44 +26,61 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; 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 +88,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..bad46fa 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) { @@ -56,10 +56,13 @@ public void onPacketSend(PacketSendEvent event) { @Override 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/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..41c2678 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); - glowingReceivers.forEach(toRemove::remove); - if (!toRemove.isEmpty()) { - sendGlowPacket(glowingEntity, toRemove, false); - } + toRemove.addAll(oldReceivers); + toRemove.removeAll(glowingReceivers); } 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/elements/ElementRuntime.java b/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java index 28ab2d0..fa5f1ac 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() 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/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..6fda29c 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 @@ -25,16 +25,17 @@ import com.github.shanebeee.skr.Registration; import org.bukkit.entity.Entity; +import me.tofaa.entitylib.wrapper.WrapperEntity; 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 +52,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..08b41ef 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 @@ -42,6 +42,8 @@ import org.jetbrains.annotations.Nullable; import java.util.Locale; +import java.util.List; +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/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/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..b33e8d7 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java @@ -0,0 +1,210 @@ +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.*; +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/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..8240a2d --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java @@ -0,0 +1,96 @@ +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 disconnectWithoutAProfileUuidDoesNotThrow() { + UserDisconnectEvent disconnect = mock(UserDisconnectEvent.class, RETURNS_DEEP_STUBS); + 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/elements/entity/effects/EntityEffectsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java index c3c4034..edcd41e 100644 --- a/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java @@ -21,12 +21,8 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Entity; import io.github.retrooper.packetevents.util.SpigotConversionUtil; -import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers; -import org.mockito.ArgumentCaptor; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; class EntityEffectsTest { @@ -144,9 +140,18 @@ void ridingFakeVehicleUpdatesEachPassenger(int pattern) { Expression[] inputs = {input(first, second), input(vehicle)}; var effect = new EffRideFakeEntity(); assertTrue(effect.init(inputs, pattern, Kleenean.FALSE, new ParseResult("", inputs))); - effect.execute(event); + 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(vehicle).addPassenger(passenger); + if (pattern == 0) { + verify(manager).removeRealPassenger(passenger); + verify(vehicle).addPassenger(passenger); + } else verify(vehicle).removePassenger(passenger); } verifyNoMoreInteractions(vehicle); @@ -203,23 +208,21 @@ void spawningTeleportsManagedEntitiesAndRestoresOnlyOnlineViewers(boolean manage @ParameterizedTest @ValueSource(ints = {0, 1}) - void ridingRealVehiclePreservesItsServerPassengers(int pattern) { - Entity vehicle = mock(Entity.class), realPassenger = mock(Entity.class); + void ridingRealVehicleDelegatesViewerSpecificPassengersToManager(int pattern) { + Entity vehicle = mock(Entity.class); WrapperEntity fakePassenger = mock(WrapperEntity.class); - when(vehicle.getEntityId()).thenReturn(1); - when(realPassenger.getEntityId()).thenReturn(2); - when(fakePassenger.getEntityId()).thenReturn(3); - when(vehicle.getPassengers()).thenReturn(List.of(realPassenger)); Expression[] inputs = {input(fakePassenger), input(vehicle)}; var effect = new EffRideFakeEntity(); assertTrue(effect.init(inputs, pattern, Kleenean.FALSE, new ParseResult("", inputs))); - effect.execute(event); - var packet = ArgumentCaptor.forClass(WrapperPlayServerSetPassengers.class); - verify(fakePassenger).sendPacketToViewers(packet.capture()); - assertEquals(1, packet.getValue().getEntityId()); - assertArrayEquals(pattern == 0 ? new int[]{2, 3} : new int[]{2}, packet.getValue().getPassengers()); - verify(vehicle, never()).addPassenger(any()); - verify(vehicle, never()).removePassenger(any()); + 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/section/EntityCreationSectionsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java index 990b5c2..3b58d45 100644 --- a/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java @@ -108,6 +108,16 @@ void genericEntityCreationRequiresSection() { } } + @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))) { 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 index 18a7dfd..b509059 100644 --- 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 @@ -25,6 +25,46 @@ void supportsNonPlayerEntities() { 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); 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 index 445da89..b43060a 100644 --- 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 @@ -3,25 +3,53 @@ 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 java.lang.reflect.Field; -import java.util.concurrent.atomic.AtomicReference; +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 org.mockito.MockedStatic; + +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.MockMakers.INLINE; 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() throws Exception { + void readsAndSetsScalarPacketFields() { Event event = mock(Event.class); - PacketWrapper packet = mock(PacketWrapper.class); + PacketWrapper packet = mock(PacketWrapper.class, RETURNS_DEEP_STUBS); AtomicReference value = new AtomicReference<>("before"); - FieldAccessor, String> accessor = new FieldAccessor<>("text", new String[0], String.class, false, ignored -> value.get(), (ignored, next) -> value.set(next)); + 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); @@ -35,38 +63,95 @@ void readsAndSetsScalarPacketFields() throws Exception { } @Test - void flattensArrayFieldsAndReportsPlurality() throws Exception { + void flattensArrayFieldsAndReportsPlurality() { Event event = mock(Event.class); - PacketWrapper packet = mock(PacketWrapper.class); - FieldAccessor, String[]> accessor = new FieldAccessor<>("names", new String[0], String[].class, false, ignored -> new String[]{"a", "b"}, null); + 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[]{"a", "b"}, expression.read(event, packet)); + assertArrayEquals(new Object[]{"player1", "player2"}, expression.read(event, packet)); assertSame(String.class, expression.getReturnType()); assertFalse(expression.isSingle()); - try (MockedStatic skript = mockStatic(Skript.class, withSettings().mockMaker(INLINE))) { + try (var skript = mockStatic(Skript.class)) { assertNull(expression.acceptChange(ChangeMode.DELETE)); - skript.verify(() -> Skript.error("Cannot set names because it is a read-only field.")); + 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 expression(FieldAccessor accessor, PacketWrapper packet, Event event) throws Exception { - Probe expression = new Probe(); - Field field = ExprPacketField.class.getDeclaredField("fieldAccessor"); - field.setAccessible(true); - field.set(expression, accessor); + 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); - expression.source(source); + 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 { - void source(Expression expression) { - setExpr(expression); - } - Object[] read(Event event, PacketWrapper packet) { return get(event, new PacketWrapper[]{packet}); } 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 index 191aaed..30ea246 100644 --- a/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java @@ -295,4 +295,20 @@ void glowOmittedViewersUsesOnlinePlayersAndMissingStateIsNoOp() { 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 index b92af50..29c62ea 100644 --- a/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java +++ b/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java @@ -102,4 +102,26 @@ void emptySourcesDoNotChangeGlowAndNullSetClearsReceivers() { 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})); + } + } } From 4f3a753c40ceb8841438c1fc12d77d78bff5febd Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Sun, 1 Nov 2026 04:38:07 +0800 Subject: [PATCH 5/8] Replace set.removeAll(list) with direct set removals. Add null-UUID regression tests. Optimized Imports --- .../java/org/crebs/skpackets/SkPackets.java | 3 +- .../api/entity/PacketEntityManager.java | 8 +-- .../entity/RealEntityPassengerManager.java | 6 +- .../skpackets/api/general/EntityTracker.java | 2 + .../general/packet/PacketTypeRegistry.java | 7 +- .../api/simple/ChatSessionListener.java | 1 + .../api/simple/FakeBlockManager.java | 1 + .../api/simple/GlowingEntityManager.java | 2 +- .../elements/entity/EntityModule.java | 2 +- .../elements/general/GeneralModule.java | 2 +- .../expressions/prop/ExprEntityId.java | 4 +- .../expressions/prop/ExprPacketField.java | 2 +- .../elements/simple/SimpleModule.java | 2 +- .../skpackets/elements/team/TeamModule.java | 2 +- .../team/expressions/ExprPacketTeam.java | 6 +- .../expressions/TeamPropertyExpression.java | 2 +- .../RealEntityPassengerManagerTest.java | 3 +- .../packet/PacketEventsNullUuidTest.java | 65 +++++++++++++++++++ .../api/simple/ChatSessionListenerTest.java | 3 +- .../entity/effects/EntityEffectsTest.java | 10 +-- .../FakeEquipmentExpressionTest.java | 4 +- .../entity/EntityMetaDataPropertyTest.java | 12 ++-- .../entity/FakeEntityPropertiesTest.java | 4 +- .../meta/EntityMetaExpressionsTest.java | 4 +- .../PacketEntityMetadataExpressionsTest.java | 4 +- .../PacketEntityMetadataValuesTest.java | 3 +- .../prop/VisibleFakeEntitiesTest.java | 4 +- .../section/EntityCreationSectionsTest.java | 4 +- .../condition/CondPacketTypeIsBoundTest.java | 6 +- .../general/effects/EffCancelPacketTest.java | 2 +- .../general/effects/EffFetchSkinTest.java | 4 +- .../effects/EffSendOrReceivePacketTest.java | 2 +- .../event/EvtPacketSendOrReceiveTest.java | 6 +- .../expressions/ExprSkinFromValueTest.java | 3 +- .../prop/ExprPacketPacketTypeTest.java | 2 +- .../expressions/prop/ExprPlayerSkinTest.java | 5 +- .../ServerBoundPacketFieldRegistrarTest.java | 5 +- .../general/section/SecExprNewPacketTest.java | 5 +- .../simple/effects/SimpleEffectsTest.java | 15 ++--- .../expressions/ExprGlowReceiversTest.java | 7 +- 40 files changed, 155 insertions(+), 79 deletions(-) create mode 100644 src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java diff --git a/src/main/java/org/crebs/skpackets/SkPackets.java b/src/main/java/org/crebs/skpackets/SkPackets.java index e50556d..d1682e7 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,11 +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.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; -import org.crebs.skpackets.elements.ElementRuntime; public final class SkPackets extends JavaPlugin { private static SkPackets instance; 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 97abfa8..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,24 +1,22 @@ 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.EntityType; 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.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityRemoveEvent; import org.bukkit.event.world.EntitiesUnloadEvent; import org.bukkit.plugin.java.JavaPlugin; diff --git a/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java b/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java index 0f03187..b44bb37 100644 --- a/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java +++ b/src/main/java/org/crebs/skpackets/api/entity/RealEntityPassengerManager.java @@ -7,11 +7,7 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.UUID; +import java.util.*; final class RealEntityPassengerManager { private record Key(UUID viewer, UUID vehicle) {} 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 bd60e96..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,6 +77,7 @@ 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) { @@ -111,6 +112,7 @@ private void handleDestroy(PacketSendEvent event) { } @Override + @SuppressWarnings("ConstantValue") public void onUserDisconnect(UserDisconnectEvent event) { UUID userUuid = event.getUser().getUUID(); if (userUuid != null) PLAYER_ENTITIES.remove(userUuid); 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 c490838..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,14 +22,9 @@ 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.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; public class PacketTypeRegistry { 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 bad46fa..d7b7aab 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/ChatSessionListener.java +++ b/src/main/java/org/crebs/skpackets/api/simple/ChatSessionListener.java @@ -55,6 +55,7 @@ public void onPacketSend(PacketSendEvent event) { } @Override + @SuppressWarnings("ConstantValue") public void onUserDisconnect(UserDisconnectEvent event) { UUID ownerId = event.getUser().getUUID(); if (ownerId != null) { 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 72a649a..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,6 +154,7 @@ 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(); 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 41c2678..bec5403 100644 --- a/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityManager.java +++ b/src/main/java/org/crebs/skpackets/api/simple/GlowingEntityManager.java @@ -70,7 +70,7 @@ public static void setGlowingReceivers(int glowingEntityId, List glowingRe Set toRemove = new HashSet<>(); if (oldReceivers != null) { toRemove.addAll(oldReceivers); - toRemove.removeAll(glowingReceivers); + glowingReceivers.forEach(toRemove::remove); } Set newReceivers = new HashSet<>(glowingReceivers); diff --git a/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java b/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java index 94a7116..0011d4b 100644 --- a/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java +++ b/src/main/java/org/crebs/skpackets/elements/entity/EntityModule.java @@ -2,7 +2,6 @@ import com.github.shanebeee.skr.Registration; import org.crebs.skpackets.elements.RegistrationModule; -import org.skriptlang.skript.addon.SkriptAddon; import org.crebs.skpackets.elements.entity.effects.*; import org.crebs.skpackets.elements.entity.expressions.*; import org.crebs.skpackets.elements.entity.expressions.entity.FakeEntityExpressions; @@ -15,6 +14,7 @@ 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 { diff --git a/src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java b/src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java index 5cdb6f7..136b43f 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java +++ b/src/main/java/org/crebs/skpackets/elements/general/GeneralModule.java @@ -22,7 +22,6 @@ import com.github.shanebeee.skr.Registration; import org.crebs.skpackets.elements.RegistrationModule; -import org.skriptlang.skript.addon.SkriptAddon; import org.crebs.skpackets.elements.general.condition.CondPacketTypeIsBound; import org.crebs.skpackets.elements.general.effects.EffCancelPacket; import org.crebs.skpackets.elements.general.effects.EffFetchSkin; @@ -36,6 +35,7 @@ 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 record GeneralModule(Registration registration) implements RegistrationModule { 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 6fda29c..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,11 +21,9 @@ package org.crebs.skpackets.elements.general.expressions.prop; import ch.njol.skript.expressions.base.SimplePropertyExpression; - import com.github.shanebeee.skr.Registration; - -import org.bukkit.entity.Entity; import me.tofaa.entitylib.wrapper.WrapperEntity; +import org.bukkit.entity.Entity; import org.jetbrains.annotations.Nullable; public class ExprEntityId extends SimplePropertyExpression { 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 08b41ef..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,8 +41,8 @@ import org.crebs.skpackets.elements.general.field.PacketFieldRegistry; import org.jetbrains.annotations.Nullable; -import java.util.Locale; import java.util.List; +import java.util.Locale; import java.util.Objects; @SuppressWarnings("rawtypes") diff --git a/src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java b/src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java index 1d60830..46cedd8 100644 --- a/src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java +++ b/src/main/java/org/crebs/skpackets/elements/simple/SimpleModule.java @@ -22,9 +22,9 @@ import com.github.shanebeee.skr.Registration; import org.crebs.skpackets.elements.RegistrationModule; -import org.skriptlang.skript.addon.SkriptAddon; import org.crebs.skpackets.elements.simple.effects.*; import org.crebs.skpackets.elements.simple.expressions.ExprGlowReceivers; +import org.skriptlang.skript.addon.SkriptAddon; public record SimpleModule(Registration registration) implements RegistrationModule { diff --git a/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java b/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java index c5e11f8..cae8b47 100644 --- a/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java +++ b/src/main/java/org/crebs/skpackets/elements/team/TeamModule.java @@ -2,10 +2,10 @@ import com.github.shanebeee.skr.Registration; import org.crebs.skpackets.elements.RegistrationModule; -import org.skriptlang.skript.addon.SkriptAddon; 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 { 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 index b33e8d7..a4c9828 100644 --- a/src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java +++ b/src/test/java/org/crebs/skpackets/api/entity/RealEntityPassengerManagerTest.java @@ -19,7 +19,8 @@ import java.util.Set; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; class RealEntityPassengerManagerTest { 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..0971fb1 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java @@ -0,0 +1,65 @@ +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.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 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/simple/ChatSessionListenerTest.java b/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java index 8240a2d..084cf0b 100644 --- a/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java +++ b/src/test/java/org/crebs/skpackets/api/simple/ChatSessionListenerTest.java @@ -88,8 +88,9 @@ void concurrentProfileUpdatesRetainEverySessionAndDisconnectRemovesThem() throws } @Test - void disconnectWithoutAProfileUuidDoesNotThrow() { + 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/elements/entity/effects/EntityEffectsTest.java b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java index edcd41e..bdcdf6a 100644 --- a/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/entity/effects/EntityEffectsTest.java @@ -3,9 +3,12 @@ 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; @@ -14,13 +17,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import java.util.UUID; +import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.List; -import org.bukkit.Bukkit; -import org.bukkit.entity.Entity; -import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; 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 index 9530bf7..28bfd67 100644 --- a/src/test/java/org/crebs/skpackets/elements/entity/expressions/FakeEquipmentExpressionTest.java +++ b/src/test/java/org/crebs/skpackets/elements/entity/expressions/FakeEquipmentExpressionTest.java @@ -6,10 +6,10 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; import me.tofaa.entitylib.wrapper.WrapperEntity; -import me.tofaa.entitylib.wrapper.WrapperLivingEntity; import me.tofaa.entitylib.wrapper.WrapperEntityEquipment; -import org.bukkit.inventory.EquipmentSlot; +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; 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 index 8c441c4..ff8b0a2 100644 --- 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 @@ -3,17 +3,19 @@ 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.EntityMeta; -import org.junit.jupiter.api.Test; -import org.bukkit.entity.Entity; 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.Mockito.*; import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; class EntityMetaDataPropertyTest { @Test 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 index 7fbf6a0..3617338 100644 --- 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 @@ -14,14 +14,16 @@ 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.Mockito.*; import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; class FakeEntityPropertiesTest { static Stream properties() { 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 index f5a248d..d6790c4 100644 --- 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 @@ -13,15 +13,17 @@ 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.Mockito.*; import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; class EntityMetaExpressionsTest { static Stream expressions() { 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 index 6f82c8f..e9f834d 100644 --- 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 @@ -10,12 +10,14 @@ 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.Mockito.*; import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; class PacketEntityMetadataExpressionsTest { static Stream expressions() { 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 index 2bacec0..a3746f4 100644 --- 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 @@ -13,7 +13,8 @@ import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; class PacketEntityMetadataValuesTest { 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 index 58facd2..817a1a1 100644 --- 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 @@ -7,12 +7,14 @@ 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.Mockito.*; import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; class VisibleFakeEntitiesTest { @Test 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 index 3b58d45..f5e63c4 100644 --- a/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/entity/section/EntityCreationSectionsTest.java @@ -20,13 +20,15 @@ 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.Mockito.*; import static org.mockito.MockMakers.INLINE; +import static org.mockito.Mockito.*; class EntityCreationSectionsTest { @BeforeAll 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 index 84687cc..490843b 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/condition/CondPacketTypeIsBoundTest.java @@ -8,8 +8,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +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 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 index 153b6e8..42ee3d5 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/effects/EffCancelPacketTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/effects/EffCancelPacketTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.MockedStatic; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.MockMakers.INLINE; import static org.mockito.Mockito.*; 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 index 14151ec..323fa7d 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/effects/EffFetchSkinTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/effects/EffFetchSkinTest.java @@ -2,14 +2,14 @@ import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.lang.Expression; -import java.lang.reflect.Field; 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 static org.junit.jupiter.api.Assertions.*; +import java.lang.reflect.Field; + import static org.mockito.MockMakers.INLINE; import static org.mockito.Mockito.*; 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 index 6d39a7b..9901eb2 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/effects/EffSendOrReceivePacketTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/effects/EffSendOrReceivePacketTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.MockedStatic; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.MockMakers.INLINE; import static org.mockito.Mockito.*; 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 index 0435534..c4183ff 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/event/EvtPacketSendOrReceiveTest.java @@ -7,7 +7,6 @@ import com.github.retrooper.packetevents.event.PacketListenerPriority; import com.github.retrooper.packetevents.event.ProtocolPacketEvent; import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; -import java.util.List; import org.bukkit.event.Event; import org.crebs.skpackets.api.general.packet.PacketSendOrReceiveEvent; import org.junit.jupiter.api.Test; @@ -15,7 +14,10 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.MockedStatic; -import static org.junit.jupiter.api.Assertions.*; +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.*; 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 index 0ae1609..c98535b 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/expressions/ExprSkinFromValueTest.java @@ -8,7 +8,8 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class ExprSkinFromValueTest { @Test 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 index eb179fd..df71097 100644 --- 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 @@ -4,7 +4,7 @@ import com.github.retrooper.packetevents.wrapper.PacketWrapper; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.*; class ExprPacketPacketTypeTest { 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 index 0431553..d878ae5 100644 --- 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 @@ -1,6 +1,5 @@ package org.crebs.skpackets.elements.general.expressions.prop; -import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.entity.Player; @@ -9,7 +8,9 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import static org.junit.jupiter.api.Assertions.*; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.MockMakers.INLINE; import static org.mockito.Mockito.*; 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 index 7da0de3..ac18cab 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/field/ServerBoundPacketFieldRegistrarTest.java @@ -1,7 +1,7 @@ package org.crebs.skpackets.elements.general.field; -import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; 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; @@ -14,7 +14,8 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.MockedStatic; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.*; class ServerBoundPacketFieldRegistrarTest { 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 index a0556aa..ff6497f 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/section/SecExprNewPacketTest.java @@ -3,13 +3,14 @@ import ch.njol.skript.Skript; import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; import com.github.retrooper.packetevents.wrapper.PacketWrapper; -import java.lang.reflect.Field; -import java.util.List; 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.*; 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 index 30ea246..a12f421 100644 --- a/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/simple/effects/SimpleEffectsTest.java @@ -6,9 +6,6 @@ import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.SyntaxElement; import ch.njol.util.Kleenean; -import java.util.List; -import java.util.Set; -import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.block.data.BlockData; @@ -20,18 +17,18 @@ import org.bukkit.inventory.ItemStack; import org.crebs.skpackets.SkPackets; import org.crebs.skpackets.api.entity.Skin; -import org.crebs.skpackets.api.simple.FakeBlockManager; -import org.crebs.skpackets.api.simple.FakeEquipmentManager; -import org.crebs.skpackets.api.simple.FakeInventoryManager; -import org.crebs.skpackets.api.simple.GlowingEntityManager; -import org.crebs.skpackets.api.simple.PlayerSkinManager; +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 static org.junit.jupiter.api.Assertions.*; +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.*; 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 index 29c62ea..aed4812 100644 --- a/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java +++ b/src/test/java/org/crebs/skpackets/elements/simple/expressions/ExprGlowReceiversTest.java @@ -5,9 +5,6 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.util.Kleenean; -import java.util.List; -import java.util.Set; -import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -17,6 +14,10 @@ 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.*; From e4b67a45190e06959b6303ab9eca887d44ae0265 Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Tue, 3 Nov 2026 08:32:05 +0800 Subject: [PATCH 6/8] Finished teams test --- .../team/effects/EffTeamMembersTest.java | 82 ++++++++ .../team/expressions/ExprPacketTeamTest.java | 77 ++++++++ .../TeamPropertyExpressionTest.java | 181 ++++++++++++++++++ .../team/section/SecNewPacketTeamTest.java | 80 ++++++++ 4 files changed, 420 insertions(+) create mode 100644 src/test/java/org/crebs/skpackets/elements/team/effects/EffTeamMembersTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/team/expressions/ExprPacketTeamTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/team/expressions/TeamPropertyExpressionTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/team/section/SecNewPacketTeamTest.java 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; + } +} From b5d3fe40953bc3ffc93c35c86824ac68a706f9fe Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Thu, 5 Nov 2026 21:48:19 +0800 Subject: [PATCH 7/8] Finished Tests and Bumped to 1.0.1 + Bug fixes Added some guard for some edge case that was probably removed bcuz qodana is feeding me false positives. This should prevent some npe. --- build.gradle.kts | 2 +- .../java/org/crebs/skpackets/SkPackets.java | 2 +- .../skpackets/api/entity/SkinManager.java | 6 +- .../api/general/PlayerSkinRegistry.java | 11 +- .../api/simple/FakeInventoryManager.java | 2 + .../api/simple/PlayerSkinRefresh.java | 2 + .../skpackets/api/util/field/FieldValues.java | 59 ++++---- .../skpackets/elements/ElementRuntime.java | 1 + .../general/field/PacketFieldRegistry.java | 2 +- .../skpackets/api/entity/SkinManagerTest.java | 44 ++++++ .../packet/PacketEventsNullUuidTest.java | 37 +++++ .../api/util/field/FieldValuesTest.java | 27 ++++ .../elements/AddonRegistrationTest.java | 126 ++++++++++++++++++ 13 files changed, 280 insertions(+), 41 deletions(-) create mode 100644 src/test/java/org/crebs/skpackets/api/entity/SkinManagerTest.java create mode 100644 src/test/java/org/crebs/skpackets/api/util/field/FieldValuesTest.java create mode 100644 src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 4a7490a..96bd873 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "org.crebs" -version = "1.0" +version = "1.0.1" val mockitoAgent by configurations.creating diff --git a/src/main/java/org/crebs/skpackets/SkPackets.java b/src/main/java/org/crebs/skpackets/SkPackets.java index d1682e7..51b60a0 100644 --- a/src/main/java/org/crebs/skpackets/SkPackets.java +++ b/src/main/java/org/crebs/skpackets/SkPackets.java @@ -69,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; 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/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/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/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/ElementRuntime.java b/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java index fa5f1ac..c3ddc04 100644 --- a/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java +++ b/src/main/java/org/crebs/skpackets/elements/ElementRuntime.java @@ -77,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/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/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 index 0971fb1..5ffbf31 100644 --- a/src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java +++ b/src/test/java/org/crebs/skpackets/api/general/packet/PacketEventsNullUuidTest.java @@ -3,6 +3,10 @@ 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; @@ -45,6 +49,39 @@ void unassignedPacketEventsProfileCanDisconnectWithoutCrashingCaches() throws Re 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(); 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..e7c4528 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java @@ -0,0 +1,126 @@ +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 skript = Skript.of(AddonRegistrationTest.class, "skPackets"); + 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); + } + }); + } + } +} + From f89558a0651a855bac7646bdebeeba535ea4b8e1 Mon Sep 17 00:00:00 2001 From: CrebsTheCoder Date: Thu, 5 Nov 2026 22:00:37 +0800 Subject: [PATCH 8/8] fix test using the same name for the Skript instance and addon fails the test which ig make sense --- .../org/crebs/skpackets/elements/AddonRegistrationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java b/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java index e7c4528..149591d 100644 --- a/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java +++ b/src/test/java/org/crebs/skpackets/elements/AddonRegistrationTest.java @@ -37,7 +37,8 @@ class AddonRegistrationTest { @Test void registersAllModulesWithoutDuplicateTypes() throws Exception { PacketEventsTestBootstrap.initialize(); - Skript skript = Skript.of(AddonRegistrationTest.class, "skPackets"); + // 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,