From 1c16d2c6a035c5e41d5e1d1e200c5f96c117bcc9 Mon Sep 17 00:00:00 2001 From: derklaro Date: Fri, 17 Jun 2022 17:25:46 +0200 Subject: [PATCH] theoretically implement bukkit platform --- .gitignore | 3 - .idea/codeStyles/Project.xml | 3 + .../com/github/juliarn/npclib/api/Npc.java | 45 +- .../github/juliarn/npclib/api/NpcTracker.java | 12 +- .../github/juliarn/npclib/api/Platform.java | 36 +- .../npclib/api/PlatformVersionAccessor.java | 36 ++ .../juliarn/npclib/api/event/NpcEvent.java | 2 +- .../api/profile/DefaultResolvedProfile.java | 2 +- .../api/profile/MojangProfileResolver.java | 2 +- .../juliarn/npclib/api/profile/Profile.java | 11 +- .../DefaultNpcSpecificOutboundPacket.java | 69 +++ .../protocol/NpcSpecificOutboundPacket.java | 54 ++ .../npclib/api/protocol/OutboundPacket.java | 20 +- .../api/protocol/PlatformPacketAdapter.java | 27 +- .../protocol/{ => enums}/EntityAnimation.java | 2 +- .../npclib/api/protocol/enums/EntityPose.java | 37 ++ .../npclib/api/protocol/enums/ItemSlot.java | 35 ++ .../{ => enums}/PlayerInfoAction.java | 2 +- .../protocol/meta/DefaultEntityMetadata.java | 51 ++ .../meta/DefaultEntityMetadataFactory.java | 154 ++++++ .../DefaultEntityMetadataFactoryBuilder.java | 111 ++++ .../api/protocol/meta/EntityMetadata.java | 39 ++ .../protocol/meta/EntityMetadataFactory.java | 69 +++ .../api/settings/NpcProfileResolver.java | 2 +- .../npclib/api/settings/NpcTrackingRule.java | 2 +- build.gradle.kts | 12 +- bukkit/build.gradle.kts | 46 ++ .../npclib/bukkit/BukkitActionController.java | 219 ++++++++ .../juliarn/npclib/bukkit/BukkitPlatform.java | 101 ++++ .../bukkit/BukkitPlatformTaskManager.java | 64 +++ .../npclib/bukkit/BukkitProfileResolver.java | 125 +++++ .../npclib/bukkit/BukkitVersionAccessor.java | 64 +++ .../npclib/bukkit/BukkitWorldAccessor.java | 88 +++ .../protocol/BukkitProtocolAdapter.java | 58 ++ .../protocol/PacketEventsPacketAdapter.java | 435 +++++++++++++++ .../protocol/ProtocolLibPacketAdapter.java | 508 ++++++++++++++++++ .../bukkit/util/BukkitPlatformUtil.java | 54 ++ .../npclib/common/CommonNpcTracker.java | 22 +- .../npclib/common/event/CommonNpcEvent.java | 8 +- .../common/event/CommonPlayerNpcEvent.java | 2 +- .../common/event/DefaultAttackNpcEvent.java | 4 +- .../common/event/DefaultHideNpcEvent.java | 10 +- .../common/event/DefaultInteractNpcEvent.java | 4 +- .../common/event/DefaultShowNpcEvent.java | 10 +- .../juliarn/npclib/common/npc/CommonNpc.java | 60 ++- .../npclib/common/npc/CommonNpcBuilder.java | 26 +- .../common/platform/CommonPlatform.java | 36 +- .../platform/CommonPlatformBuilder.java | 74 ++- .../npclib/common/util/EventDispatcher.java | 6 +- gradle/libs.versions.toml | 9 +- settings.gradle.kts | 2 +- 51 files changed, 2723 insertions(+), 150 deletions(-) create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/PlatformVersionAccessor.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/DefaultNpcSpecificOutboundPacket.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/NpcSpecificOutboundPacket.java rename api/src/main/java/com/github/juliarn/npclib/api/protocol/{ => enums}/EntityAnimation.java (96%) create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityPose.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/ItemSlot.java rename api/src/main/java/com/github/juliarn/npclib/api/protocol/{ => enums}/PlayerInfoAction.java (95%) create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadata.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactory.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactoryBuilder.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadata.java create mode 100644 api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadataFactory.java create mode 100644 bukkit/build.gradle.kts create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitActionController.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatform.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatformTaskManager.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitProfileResolver.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitVersionAccessor.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitWorldAccessor.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/BukkitProtocolAdapter.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/PacketEventsPacketAdapter.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/ProtocolLibPacketAdapter.java create mode 100644 bukkit/src/main/java/com/github/juliarn/npclib/bukkit/util/BukkitPlatformUtil.java diff --git a/.gitignore b/.gitignore index 1ef4bf3..2451dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,3 @@ atlassian-ide-plugin.xml # delombok */src/main/lombok - -# CloudNet -.launchermeta/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index b583943..d0f4383 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -112,6 +112,9 @@ + + diff --git a/api/src/main/java/com/github/juliarn/npclib/api/Npc.java b/api/src/main/java/com/github/juliarn/npclib/api/Npc.java index 165dd9e..78856f3 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/Npc.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/Npc.java @@ -29,6 +29,8 @@ import com.github.juliarn.npclib.api.flag.NpcFlaggedObject; import com.github.juliarn.npclib.api.profile.Profile; import com.github.juliarn.npclib.api.profile.ProfileResolver; +import com.github.juliarn.npclib.api.protocol.NpcSpecificOutboundPacket; +import com.github.juliarn.npclib.api.protocol.enums.ItemSlot; import com.github.juliarn.npclib.api.settings.NpcSettings; import java.util.Collection; import java.util.concurrent.CompletableFuture; @@ -37,7 +39,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; -public interface Npc extends NpcFlaggedObject { +public interface Npc extends NpcFlaggedObject { NpcFlag LOOK_AT_PLAYER = NpcFlag.flag("imitate_player_look", false); NpcFlag HIT_WHEN_PLAYER_HITS = NpcFlag.flag("imitate_player_hit", false); @@ -53,46 +55,55 @@ public interface Npc extends NpcFlaggedObject { @NotNull NpcSettings

settings(); - @NotNull Platform platform(); + @NotNull Platform platform(); - @NotNull NpcTracker npcTracker(); + @NotNull NpcTracker npcTracker(); boolean shouldIncludePlayer(@NotNull P player); @UnmodifiableView @NotNull Collection

includedPlayers(); - @NotNull Npc addIncludedPlayer(@NotNull P player); + boolean includesPlayer(@NotNull P player); - @NotNull Npc removeIncludedPlayer(@NotNull P player); + @NotNull Npc addIncludedPlayer(@NotNull P player); + + @NotNull Npc removeIncludedPlayer(@NotNull P player); @UnmodifiableView @NotNull Collection

trackedPlayers(); - @NotNull Npc trackPlayer(@NotNull P player); + boolean tracksPlayer(@NotNull P player); + + @NotNull Npc trackPlayer(@NotNull P player); + + @NotNull Npc forceTrackPlayer(@NotNull P player); + + @NotNull Npc stopTrackingPlayer(@NotNull P player); - @NotNull Npc forceTrackPlayer(@NotNull P player); + @NotNull NpcSpecificOutboundPacket lookAt(@NotNull Position position); - @NotNull Npc stopTrackingPlayer(@NotNull P player); + @NotNull NpcSpecificOutboundPacket changeItem(@NotNull ItemSlot slot, @NotNull I item); - interface Builder extends NpcFlaggedBuilder> { + interface Builder extends NpcFlaggedBuilder> { - @NotNull Builder entityId(int id); + @NotNull Builder entityId(int id); - @NotNull Builder position(@NotNull Position position); + @NotNull Builder position(@NotNull Position position); - @NotNull Builder profile(@NotNull Profile.Resolved profile); + @NotNull Builder profile(@NotNull Profile.Resolved profile); - default @NotNull CompletableFuture> profile(@NotNull Profile profile) { + default @NotNull CompletableFuture> profile(@NotNull Profile profile) { return this.profile(null, profile); } - @NotNull CompletableFuture> profile(@Nullable ProfileResolver resolver, @NotNull Profile profile); + @NotNull CompletableFuture> profile(@Nullable ProfileResolver resolver, + @NotNull Profile profile); - @NotNull Builder npcSettings(@NotNull Consumer> decorator); + @NotNull Builder npcSettings(@NotNull Consumer> decorator); - @NotNull Npc build(); + @NotNull Npc build(); - @NotNull Npc buildAndTrack(); + @NotNull Npc buildAndTrack(); } } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/NpcTracker.java b/api/src/main/java/com/github/juliarn/npclib/api/NpcTracker.java index 85aa88c..7e9b906 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/NpcTracker.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/NpcTracker.java @@ -30,16 +30,16 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; -public interface NpcTracker { +public interface NpcTracker { - @Nullable Npc npcById(int entityId); + @Nullable Npc npcById(int entityId); - @Nullable Npc npcByUniqueId(@NotNull UUID uniqueId); + @Nullable Npc npcByUniqueId(@NotNull UUID uniqueId); - void trackNpc(@NotNull Npc npc); + void trackNpc(@NotNull Npc npc); - void stopTrackingNpc(@NotNull Npc npc); + void stopTrackingNpc(@NotNull Npc npc); @UnmodifiableView - @NotNull Collection> trackedNpcs(); + @NotNull Collection> trackedNpcs(); } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/Platform.java b/api/src/main/java/com/github/juliarn/npclib/api/Platform.java index e32839b..e7e8124 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/Platform.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/Platform.java @@ -32,44 +32,52 @@ import net.kyori.event.EventBus; import org.jetbrains.annotations.NotNull; -public interface Platform { +public interface Platform { boolean debug(); + @NotNull E extension(); + @NotNull EventBus eventBus(); - @NotNull NpcTracker npcTracker(); + @NotNull NpcTracker npcTracker(); @NotNull ProfileResolver profileResolver(); @NotNull PlatformTaskManager taskManager(); - @NotNull Npc.Builder newNpcBuilder(); + @NotNull Npc.Builder newNpcBuilder(); + + @NotNull PlatformVersionAccessor versionAccessor(); @NotNull PlatformWorldAccessor worldAccessor(); - @NotNull PlatformPacketAdapter packetFactory(); + @NotNull PlatformPacketAdapter packetFactory(); @NotNull Optional actionController(); - interface Builder { + interface Builder { + + @NotNull Builder debug(boolean debug); + + @NotNull Builder extension(@NotNull E extension); - @NotNull Builder debug(boolean debug); + @NotNull Builder eventBus(@NotNull EventBus eventBus); - @NotNull Builder eventBus(@NotNull EventBus eventBus); + @NotNull Builder npcTracker(@NotNull NpcTracker npcTracker); - @NotNull Builder npcTracker(@NotNull NpcTracker npcTracker); + @NotNull Builder taskManager(@NotNull PlatformTaskManager taskManager); - @NotNull Builder taskManager(@NotNull PlatformTaskManager taskManager); + @NotNull Builder profileResolver(@NotNull ProfileResolver profileResolver); - @NotNull Builder profileResolver(@NotNull ProfileResolver profileResolver); + @NotNull Builder worldAccessor(@NotNull PlatformWorldAccessor worldAccessor); - @NotNull Builder worldAccessor(@NotNull PlatformWorldAccessor worldAccessor); + @NotNull Builder versionAccessor(@NotNull PlatformVersionAccessor versionAccessor); - @NotNull Builder packetFactory(@NotNull PlatformPacketAdapter packetFactory); + @NotNull Builder packetFactory(@NotNull PlatformPacketAdapter packetFactory); - @NotNull Builder actionController(@NotNull Consumer decorator); + @NotNull Builder actionController(@NotNull Consumer decorator); - @NotNull Platform build(); + @NotNull Platform build(); } } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/PlatformVersionAccessor.java b/api/src/main/java/com/github/juliarn/npclib/api/PlatformVersionAccessor.java new file mode 100644 index 0000000..88a8678 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/PlatformVersionAccessor.java @@ -0,0 +1,36 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api; + +public interface PlatformVersionAccessor { + + int major(); + + int minor(); + + int patch(); + + boolean atLeast(int major, int minor, int patch); +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/event/NpcEvent.java b/api/src/main/java/com/github/juliarn/npclib/api/event/NpcEvent.java index 09bbf3f..901cb31 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/event/NpcEvent.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/event/NpcEvent.java @@ -29,5 +29,5 @@ public interface NpcEvent { - @NotNull Npc npc(); + @NotNull Npc npc(); } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/profile/DefaultResolvedProfile.java b/api/src/main/java/com/github/juliarn/npclib/api/profile/DefaultResolvedProfile.java index dbe57fc..ec5416f 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/profile/DefaultResolvedProfile.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/profile/DefaultResolvedProfile.java @@ -38,7 +38,7 @@ public final class DefaultResolvedProfile implements Profile.Resolved { private final UUID uniqueId; private final Set properties; - public DefaultResolvedProfile( + DefaultResolvedProfile( @NotNull String name, @NotNull UUID uniqueId, @NotNull Set properties diff --git a/api/src/main/java/com/github/juliarn/npclib/api/profile/MojangProfileResolver.java b/api/src/main/java/com/github/juliarn/npclib/api/profile/MojangProfileResolver.java index e545848..70fc603 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/profile/MojangProfileResolver.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/profile/MojangProfileResolver.java @@ -94,7 +94,7 @@ final class MojangProfileResolver implements ProfileResolver { Set properties = GSON.fromJson(responseData.get("properties"), PROFILE_PROPERTIES_TYPE); // create the profile from the received data - return new DefaultResolvedProfile(name, uniqueId, properties); + return Profile.resolved(name, uniqueId, properties); })); } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/profile/Profile.java b/api/src/main/java/com/github/juliarn/npclib/api/profile/Profile.java index 7aae369..d042b4e 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/profile/Profile.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/profile/Profile.java @@ -44,10 +44,19 @@ public interface Profile { } static @NotNull Resolved resolved(@NotNull String name, @NotNull UUID uniqueId) { + return resolved(name, uniqueId, Collections.emptySet()); + } + + static @NotNull Resolved resolved( + @NotNull String name, + @NotNull UUID uniqueId, + @NotNull Set properties + ) { Objects.requireNonNull(name, "name"); Objects.requireNonNull(uniqueId, "unique id"); + Objects.requireNonNull(properties, "properties"); - return new DefaultResolvedProfile(name, uniqueId, Collections.emptySet()); + return new DefaultResolvedProfile(name, uniqueId, properties); } boolean resolved(); diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/DefaultNpcSpecificOutboundPacket.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/DefaultNpcSpecificOutboundPacket.java new file mode 100644 index 0000000..7a7bb18 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/DefaultNpcSpecificOutboundPacket.java @@ -0,0 +1,69 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol; + +import com.github.juliarn.npclib.api.Npc; +import java.util.Collection; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; + +final class DefaultNpcSpecificOutboundPacket implements NpcSpecificOutboundPacket { + + private final Npc target; + private final OutboundPacket outboundPacket; + + public DefaultNpcSpecificOutboundPacket( + @NotNull Npc target, + @NotNull OutboundPacket outboundPacket + ) { + this.target = target; + this.outboundPacket = outboundPacket; + } + + @Override + public @NotNull Npc npc() { + return this.target; + } + + @Override + public void scheduleForTracked() { + this.outboundPacket.scheduleForTracked(this.target); + } + + @Override + public void schedule(@NotNull P player) { + this.outboundPacket.schedule(player, this.target); + } + + @Override + public void schedule(@NotNull Collection

players) { + this.outboundPacket.schedule(players, this.target); + } + + @Override + public void schedule(@NotNull Function, Collection

> extractor) { + this.outboundPacket.schedule(extractor, this.target); + } +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/NpcSpecificOutboundPacket.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/NpcSpecificOutboundPacket.java new file mode 100644 index 0000000..7ab1ae3 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/NpcSpecificOutboundPacket.java @@ -0,0 +1,54 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol; + +import com.github.juliarn.npclib.api.Npc; +import java.util.Collection; +import java.util.Objects; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; + +public interface NpcSpecificOutboundPacket { + + static @NotNull NpcSpecificOutboundPacket fromOutboundPacket( + @NotNull Npc npc, + @NotNull OutboundPacket packet + ) { + Objects.requireNonNull(npc, "npc"); + Objects.requireNonNull(packet, "packet"); + + return new DefaultNpcSpecificOutboundPacket<>(npc, packet); + } + + @NotNull Npc npc(); + + void scheduleForTracked(); + + void schedule(@NotNull P player); + + void schedule(@NotNull Collection

players); + + void schedule(@NotNull Function, Collection

> extractor); +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/OutboundPacket.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/OutboundPacket.java index 245d5b6..7a23127 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/protocol/OutboundPacket.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/OutboundPacket.java @@ -26,15 +26,27 @@ import com.github.juliarn.npclib.api.Npc; import java.util.Collection; +import java.util.function.Function; import org.jetbrains.annotations.NotNull; @FunctionalInterface -public interface OutboundPacket { +public interface OutboundPacket { - @NotNull OutboundPacket schedule(@NotNull P player, @NotNull Npc npc); + void schedule(@NotNull P player, @NotNull Npc npc); - default @NotNull OutboundPacket schedule(@NotNull Collection

players, @NotNull Npc npc) { + default void scheduleForTracked(@NotNull Npc npc) { + this.schedule(Npc::trackedPlayers, npc); + } + + default void schedule(@NotNull Function, Collection

> extractor, @NotNull Npc npc) { + this.schedule(extractor.apply(npc), npc); + } + + default void schedule(@NotNull Collection

players, @NotNull Npc npc) { players.forEach(player -> this.schedule(player, npc)); - return this; + } + + default @NotNull NpcSpecificOutboundPacket toSpecific(@NotNull Npc targetNpc) { + return NpcSpecificOutboundPacket.fromOutboundPacket(targetNpc, this); } } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/PlatformPacketAdapter.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/PlatformPacketAdapter.java index 01f4f04..fca2831 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/protocol/PlatformPacketAdapter.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/PlatformPacketAdapter.java @@ -24,17 +24,32 @@ package com.github.juliarn.npclib.api.protocol; +import com.github.juliarn.npclib.api.Platform; +import com.github.juliarn.npclib.api.protocol.enums.EntityAnimation; +import com.github.juliarn.npclib.api.protocol.enums.ItemSlot; +import com.github.juliarn.npclib.api.protocol.enums.PlayerInfoAction; +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory; import org.jetbrains.annotations.NotNull; -public interface PlatformPacketAdapter { +public interface PlatformPacketAdapter { - @NotNull OutboundPacket createEntitySpawnPacket(); + @NotNull OutboundPacket createEntitySpawnPacket(); - @NotNull OutboundPacket createEntityRemovePacket(); + @NotNull OutboundPacket createEntityRemovePacket(); - @NotNull OutboundPacket createPlayerInfoPacket(@NotNull PlayerInfoAction action); + @NotNull OutboundPacket createPlayerInfoPacket(@NotNull PlayerInfoAction action); - @NotNull OutboundPacket createRotationPacket(float yaw, float pitch); + @NotNull OutboundPacket createRotationPacket(float yaw, float pitch); - @NotNull OutboundPacket createAnimationPacket(@NotNull EntityAnimation animation); + @NotNull OutboundPacket createAnimationPacket(@NotNull EntityAnimation animation); + + @NotNull OutboundPacket createEquipmentPacket(@NotNull ItemSlot slot, @NotNull I item); + + @NotNull OutboundPacket createCustomPayloadPacket(@NotNull String channelId, byte[] payload); + + @NotNull OutboundPacket createEntityMetaPacket( + @NotNull T value, + @NotNull EntityMetadataFactory metadata); + + void initialize(@NotNull Platform platform); } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/EntityAnimation.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityAnimation.java similarity index 96% rename from api/src/main/java/com/github/juliarn/npclib/api/protocol/EntityAnimation.java rename to api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityAnimation.java index 075df35..6fdd5f8 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/protocol/EntityAnimation.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityAnimation.java @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package com.github.juliarn.npclib.api.protocol; +package com.github.juliarn.npclib.api.protocol.enums; public enum EntityAnimation { diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityPose.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityPose.java new file mode 100644 index 0000000..a62c143 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/EntityPose.java @@ -0,0 +1,37 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.enums; + +public enum EntityPose { + + STANDING, + FALL_FLYING, + SLEEPING, + SWIMMING, + SPIN_ATTACK, + CROUCHING, + LONG_JUMPING, + DYING +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/ItemSlot.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/ItemSlot.java new file mode 100644 index 0000000..baa6fb4 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/ItemSlot.java @@ -0,0 +1,35 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.enums; + +public enum ItemSlot { + + MAIN_HAND, + OFF_HAND, + FEET, + LEGS, + CHEST, + HEAD +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/PlayerInfoAction.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/PlayerInfoAction.java similarity index 95% rename from api/src/main/java/com/github/juliarn/npclib/api/protocol/PlayerInfoAction.java rename to api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/PlayerInfoAction.java index b9c3af1..2fe8d01 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/protocol/PlayerInfoAction.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/enums/PlayerInfoAction.java @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package com.github.juliarn.npclib.api.protocol; +package com.github.juliarn.npclib.api.protocol.enums; public enum PlayerInfoAction { diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadata.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadata.java new file mode 100644 index 0000000..9f92fbd --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadata.java @@ -0,0 +1,51 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.meta; + +import com.github.juliarn.npclib.api.protocol.enums.EntityPose; + +interface DefaultEntityMetadata { + + // https://wiki.vg/Entity_metadata#Entity - see index 0 and 6 + EntityMetadataFactory SNEAKING = EntityMetadataFactory.metaFactoryBuilder() + .baseIndex(0) + .type(Byte.class) + .inputConverter(value -> (byte) (value ? 0x02 : 0x00)) + .addRelatedMetadata(EntityMetadataFactory.metaFactoryBuilder() + .baseIndex(6) + .type(EntityPose.class) + .inputConverter(value -> value ? EntityPose.CROUCHING : EntityPose.STANDING) + .availabilityChecker(versionAccessor -> versionAccessor.atLeast(1, 14, 0)) + .build()) + .build(); + + // https://wiki.vg/Entity_metadata#Player + EntityMetadataFactory SKIN_LAYERS = EntityMetadataFactory.metaFactoryBuilder() + .baseIndex(10) + .type(Byte.class) + .indexShiftVersions(9, 9, 10, 14, 14, 15, 17) + .inputConverter(value -> (byte) (value ? 0xff : 0x00)) + .build(); +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactory.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactory.java new file mode 100644 index 0000000..d82c9e5 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactory.java @@ -0,0 +1,154 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.meta; + +import com.github.juliarn.npclib.api.PlatformVersionAccessor; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +final class DefaultEntityMetadataFactory implements EntityMetadataFactory { + + private final int baseIndex; + private final int[] indexShitVersions; + + private final Type type; + private final Function inputConverter; + + private final Collection> relatedMetadata; + private final Function availabilityChecker; + + public DefaultEntityMetadataFactory( + int baseIndex, + int[] indexShitVersions, + @NotNull Type type, + @NotNull Function inputConverter, + @NotNull Collection> relatedMetadata, + @NotNull Function availabilityChecker + ) { + this.baseIndex = baseIndex; + this.indexShitVersions = indexShitVersions; + this.type = type; + this.inputConverter = inputConverter; + this.relatedMetadata = Collections.unmodifiableCollection(relatedMetadata); + this.availabilityChecker = availabilityChecker; + } + + @Override + @Unmodifiable + public @NotNull Collection> relatedMetadata() { + return this.relatedMetadata; + } + + @Override + @SuppressWarnings("unchecked") + public @NotNull EntityMetadata create(@NotNull I input, @NotNull PlatformVersionAccessor versionAccessor) { + // check if the meta is available + if (this.availabilityChecker.apply(versionAccessor)) { + // try to convert the given input value + O value = this.inputConverter.apply(input); + if (value != null) { + // calculate the index & create the meta + int index = this.baseIndex + this.calcIndexShift(versionAccessor); + return new AvailableEntityMetadata<>(index, value, this.type); + } + } + + // not available + return (EntityMetadata) UnavailableEntityMetadata.INSTANCE; + } + + private int calcIndexShift(@NotNull PlatformVersionAccessor versionAccessor) { + int shift = 0; + for (int version : this.indexShitVersions) { + if (versionAccessor.minor() >= version) { + shift++; + } + } + return shift; + } + + private static final class AvailableEntityMetadata implements EntityMetadata { + + private final int index; + + private final O value; + private final Type type; + + private AvailableEntityMetadata(int index, @NotNull O value, @NotNull Type type) { + this.index = index; + this.value = value; + this.type = type; + } + + @Override + public int index() { + return this.index; + } + + @Override + public boolean available() { + return true; + } + + @Override + public @NotNull O value() { + return this.value; + } + + @Override + public @NotNull Type type() { + return this.type; + } + } + + private static final class UnavailableEntityMetadata implements EntityMetadata { + + private static final UnavailableEntityMetadata INSTANCE = new UnavailableEntityMetadata(); + + @Override + public int index() { + throw new UnsupportedOperationException("Unavailable entity metadata cannot be accessed"); + } + + @Override + public boolean available() { + return false; + } + + @Override + public @NotNull Object value() { + throw new UnsupportedOperationException("Unavailable entity metadata cannot be accessed"); + } + + @Override + public @NotNull Class type() { + throw new UnsupportedOperationException("Unavailable entity metadata cannot be accessed"); + } + } +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactoryBuilder.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactoryBuilder.java new file mode 100644 index 0000000..b893b71 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/DefaultEntityMetadataFactoryBuilder.java @@ -0,0 +1,111 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.meta; + +import com.github.juliarn.npclib.api.PlatformVersionAccessor; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; + +final class DefaultEntityMetadataFactoryBuilder implements EntityMetadataFactory.Builder { + + private int baseIndex = 0; + private int[] indexShitVersions = new int[0]; + + private Type type; + private Function inputConverter; + + private Collection> relatedMetadata; + private Function availabilityChecker; + + @Override + public @NotNull EntityMetadataFactory.Builder baseIndex(int index) { + this.baseIndex = index; + return this; + } + + @Override + public @NotNull EntityMetadataFactory.Builder indexShiftVersions(int... versions) { + this.indexShitVersions = versions; + return this; + } + + @Override + public @NotNull EntityMetadataFactory.Builder type(@NotNull Type type) { + this.type = Objects.requireNonNull(type, "type"); + return this; + } + + @Override + public @NotNull EntityMetadataFactory.Builder inputConverter(@NotNull Function mapper) { + this.inputConverter = Objects.requireNonNull(mapper, "mapper"); + return this; + } + + @Override + public @NotNull EntityMetadataFactory.Builder addRelatedMetadata( + @NotNull EntityMetadataFactory relatedMetadata + ) { + if (this.relatedMetadata == null) { + this.relatedMetadata = new HashSet<>(); + } + + this.relatedMetadata.add(relatedMetadata); + return this; + } + + @Override + public @NotNull EntityMetadataFactory.Builder availabilityChecker( + @NotNull Function checker + ) { + this.availabilityChecker = Objects.requireNonNull(checker, "checker"); + return this; + } + + @Override + public @NotNull EntityMetadataFactory build() { + // fill in default empty values + if (this.relatedMetadata == null) { + this.relatedMetadata = Collections.emptySet(); + } + + if (this.availabilityChecker == null) { + this.availabilityChecker = accessor -> true; + } + + return new DefaultEntityMetadataFactory<>( + this.baseIndex, + this.indexShitVersions, + Objects.requireNonNull(this.type, "type"), + Objects.requireNonNull(this.inputConverter, "inputConverter"), + this.relatedMetadata, + this.availabilityChecker + ); + } +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadata.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadata.java new file mode 100644 index 0000000..3fa7f27 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadata.java @@ -0,0 +1,39 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.meta; + +import java.lang.reflect.Type; +import org.jetbrains.annotations.NotNull; + +public interface EntityMetadata { + + int index(); + + boolean available(); + + @NotNull O value(); + + @NotNull Type type(); +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadataFactory.java b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadataFactory.java new file mode 100644 index 0000000..260b215 --- /dev/null +++ b/api/src/main/java/com/github/juliarn/npclib/api/protocol/meta/EntityMetadataFactory.java @@ -0,0 +1,69 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.api.protocol.meta; + +import com.github.juliarn.npclib.api.PlatformVersionAccessor; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +public interface EntityMetadataFactory { + + static @NotNull EntityMetadataFactory.Builder metaFactoryBuilder() { + return new DefaultEntityMetadataFactoryBuilder<>(); + } + + static @NotNull EntityMetadataFactory sneakingMetaFactory() { + return DefaultEntityMetadata.SNEAKING; + } + + static @NotNull EntityMetadataFactory skinLayerMetaFactory() { + return DefaultEntityMetadata.SKIN_LAYERS; + } + + @Unmodifiable + @NotNull Collection> relatedMetadata(); + + @NotNull EntityMetadata create(@NotNull I input, @NotNull PlatformVersionAccessor versionAccessor); + + interface Builder { + + @NotNull Builder baseIndex(int index); + + @NotNull Builder indexShiftVersions(int... versions); + + @NotNull Builder type(@NotNull Type type); + + @NotNull Builder inputConverter(@NotNull Function mapper); + + @NotNull Builder addRelatedMetadata(@NotNull EntityMetadataFactory relatedMetadata); + + @NotNull Builder availabilityChecker(@NotNull Function checker); + + @NotNull EntityMetadataFactory build(); + } +} diff --git a/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcProfileResolver.java b/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcProfileResolver.java index b71e9c5..6ac8302 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcProfileResolver.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcProfileResolver.java @@ -36,5 +36,5 @@ public interface NpcProfileResolver

{ return (player, npc) -> CompletableFuture.completedFuture(npc.profile()); } - @NotNull CompletableFuture resolveNpcProfile(@NotNull P player, @NotNull Npc npc); + @NotNull CompletableFuture resolveNpcProfile(@NotNull P player, @NotNull Npc npc); } diff --git a/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcTrackingRule.java b/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcTrackingRule.java index 7e3a569..f65b6ed 100644 --- a/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcTrackingRule.java +++ b/api/src/main/java/com/github/juliarn/npclib/api/settings/NpcTrackingRule.java @@ -43,5 +43,5 @@ public interface NpcTrackingRule

{ return (npc, player) -> npc.includedPlayers().contains(player); } - boolean shouldTrack(@NotNull Npc npc, @NotNull P player); + boolean shouldTrack(@NotNull Npc npc, @NotNull P player); } diff --git a/build.gradle.kts b/build.gradle.kts index 25d7f0b..6cc6b2b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,8 +35,8 @@ allprojects { repositories { mavenCentral() - maven("https://jitpack.io") - maven("https://papermc.io/repo/repository/maven-public/") + maven("https://jitpack.io/") + maven("https://repo.papermc.io/repository/maven-public/") maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") } } @@ -64,15 +64,19 @@ subprojects { } tasks.withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_1_8.toString() - targetCompatibility = JavaVersion.VERSION_1_8.toString() // options + options.release.set(8) options.encoding = "UTF-8" options.isIncremental = true // we are aware that those are there, but we only do that if there is no other way we can use - so please keep the terminal clean! options.compilerArgs = mutableListOf("-Xlint:-deprecation,-unchecked") } + extensions.configure { + disableAutoTargetJvm() + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + } + tasks.withType { maxErrors = 0 maxWarnings = 0 diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts new file mode 100644 index 0000000..f86b281 --- /dev/null +++ b/bukkit/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +dependencies { + api(projects.api) + implementation(libs.paperLib) + implementation(projects.common) + implementation(libs.packetEvents) + + compileOnly(libs.netty) + compileOnly(libs.paper) + compileOnly(libs.protocolLib) +} + +tasks.withType().configureEach { + minimize() + + relocate("net.kyori", "com.github.juliarn.npclib.relocate.kyori") + relocate("com.google.gson", "com.github.juliarn.npclib.relocate.gson") + relocate("io.papermc.lib", "com.github.juliarn.npclib.relocate.paperlib") + relocate("io.github.retrooper", "com.github.juliarn.npclib.relocate.io.packetevents") + relocate("com.github.retrooper", "com.github.juliarn.npclib.relocate.com.packetevents") +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitActionController.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitActionController.java new file mode 100644 index 0000000..b09eacd --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitActionController.java @@ -0,0 +1,219 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit; + +import com.github.juliarn.npclib.api.Npc; +import com.github.juliarn.npclib.api.NpcActionController; +import com.github.juliarn.npclib.api.NpcTracker; +import com.github.juliarn.npclib.api.Position; +import com.github.juliarn.npclib.api.event.NpcEvent; +import com.github.juliarn.npclib.api.event.ShowNpcEvent; +import com.github.juliarn.npclib.api.flag.NpcFlag; +import com.github.juliarn.npclib.api.protocol.enums.EntityAnimation; +import com.github.juliarn.npclib.api.protocol.enums.PlayerInfoAction; +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory; +import com.github.juliarn.npclib.bukkit.util.BukkitPlatformUtil; +import com.github.juliarn.npclib.common.CommonNpcActionController; +import com.github.juliarn.npclib.common.flag.CommonNpcFlaggedBuilder; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import net.kyori.event.EventBus; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +public final class BukkitActionController extends CommonNpcActionController implements Listener { + + private final NpcTracker npcTracker; + + // based on the given flags + private final int spawnDistance; + private final int imitateDistance; + + public BukkitActionController( + @NotNull Map, Optional> flags, + @NotNull Plugin plugin, + @NotNull EventBus eventBus, + @NotNull NpcTracker tracker + ) { + super(flags); + this.npcTracker = tracker; + + // add all listeners + plugin.getServer().getPluginManager().registerEvents(this, plugin); + eventBus.subscribe(ShowNpcEvent.Post.class, event -> { + // remove the npc from the tab list after the given amount of time (never smaller than 0 because of validation) + int tabRemovalTicks = this.flagValueOrDefault(TAB_REMOVAL_TICKS); + plugin.getServer().getScheduler().runTaskLaterAsynchronously(plugin, () -> { + // schedule the removal of the player from the tab list, can be done async + Player player = event.player(); + event.npc().platform().packetFactory() + .createPlayerInfoPacket(PlayerInfoAction.REMOVE_PLAYER) + .schedule(player, event.npc()); + }, tabRemovalTicks); + }); + + // pre-calculate flag values + int spawnDistance = this.flagValueOrDefault(SPAWN_DISTANCE); + this.spawnDistance = spawnDistance * spawnDistance; + + int imitateDistance = this.flagValueOrDefault(IMITATE_DISTANCE); + this.imitateDistance = imitateDistance * imitateDistance; + } + + static @NotNull NpcActionController.Builder actionControllerBuilder( + @NotNull Plugin plugin, + @NotNull EventBus eventBus, + @NotNull NpcTracker npcTracker + ) { + Objects.requireNonNull(plugin, "plugin"); + Objects.requireNonNull(eventBus, "eventBus"); + Objects.requireNonNull(npcTracker, "npcTracker"); + + return new BukkitActionControllerBuilder(plugin, eventBus, npcTracker); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void handleMove(@NotNull PlayerMoveEvent event) { + Location to = event.getTo(); + Location from = event.getFrom(); + + boolean changedWorld = !Objects.equals(from.getWorld(), to.getWorld()); + boolean changedOrientation = from.getYaw() != to.getYaw() || from.getPitch() != to.getPitch(); + boolean changedPosition = from.getX() != to.getX() || from.getY() != to.getY() || from.getZ() != to.getZ(); + + // check if any movement happened (event is also called when standing still) + if (changedPosition || changedOrientation || changedWorld) { + Player player = event.getPlayer(); + for (Npc npc : this.npcTracker.trackedNpcs()) { + // check if the player is still in the same world as the npc + Position pos = npc.position(); + if (!npc.world().equals(player.getWorld()) || !npc.world().isChunkLoaded(pos.chunkX(), pos.chunkZ())) { + // if the player is tracked by the npc, stop that + npc.stopTrackingPlayer(player); + continue; + } + + // check if the player moved in / out of any npc tracking distance + double distance = BukkitPlatformUtil.distance(npc, to); + if (distance > this.spawnDistance) { + // this will only do something if the player is already tracked by the npc + npc.stopTrackingPlayer(player); + continue; + } else { + // this will only do something if the player is not already tracked by the npc + npc.trackPlayer(player); + } + + // check if we should rotate the npc towards the player + if (changedOrientation + && npc.tracksPlayer(player) + && distance <= this.imitateDistance + && npc.flagValueOrDefault(Npc.LOOK_AT_PLAYER)) { + npc.lookAt(BukkitPlatformUtil.positionFromBukkit(to)).schedule(player); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void handleSneak(@NotNull PlayerToggleSneakEvent event) { + Player player = event.getPlayer(); + for (Npc npc : this.npcTracker.trackedNpcs()) { + double distance = BukkitPlatformUtil.distance(npc, player.getLocation()); + + // check if we should imitate the action + if (Objects.equals(player.getWorld(), npc.world()) + && npc.tracksPlayer(player) + && distance <= this.imitateDistance + && npc.flagValueOrDefault(Npc.SNEAK_WHEN_PLAYER_SNEAKS)) { + // let the npc sneak as well + npc.platform().packetFactory() + .createEntityMetaPacket(event.isSneaking(), EntityMetadataFactory.sneakingMetaFactory()) + .schedule(player, npc); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void handleLeftClick(@NotNull PlayerInteractEntityEvent event) { + Player player = event.getPlayer(); + for (Npc npc : this.npcTracker.trackedNpcs()) { + double distance = BukkitPlatformUtil.distance(npc, player.getLocation()); + + // check if we should imitate the action + if (Objects.equals(player.getWorld(), npc.world()) + && npc.tracksPlayer(player) + && distance <= this.imitateDistance + && npc.flagValueOrDefault(Npc.HIT_WHEN_PLAYER_HITS)) { + // let the npc left click as well + npc.platform().packetFactory().createAnimationPacket(EntityAnimation.SWING_MAIN_ARM).schedule(player, npc); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void handleQuit(@NotNull PlayerQuitEvent event) { + for (Npc npc : this.npcTracker.trackedNpcs()) { + // check if the npc tracks the player which disconnected and stop tracking him if so + npc.stopTrackingPlayer(event.getPlayer()); + } + } + + private static final class BukkitActionControllerBuilder + extends CommonNpcFlaggedBuilder + implements NpcActionController.Builder { + + private final Plugin plugin; + private final EventBus eventBus; + private final NpcTracker npcTracker; + + public BukkitActionControllerBuilder( + @NotNull Plugin plugin, + @NotNull EventBus eventBus, + @NotNull NpcTracker npcTracker + ) { + this.plugin = plugin; + this.eventBus = eventBus; + this.npcTracker = npcTracker; + } + + @Override + public @NotNull NpcActionController build() { + return new BukkitActionController(this.flags, this.plugin, this.eventBus, this.npcTracker); + } + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatform.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatform.java new file mode 100644 index 0000000..ad1a93a --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatform.java @@ -0,0 +1,101 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit; + +import com.github.juliarn.npclib.api.NpcActionController; +import com.github.juliarn.npclib.api.Platform; +import com.github.juliarn.npclib.bukkit.protocol.BukkitProtocolAdapter; +import com.github.juliarn.npclib.common.platform.CommonPlatform; +import com.github.juliarn.npclib.common.platform.CommonPlatformBuilder; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +public final class BukkitPlatform extends CommonPlatformBuilder { + + private BukkitPlatform() { + } + + public static @NotNull BukkitPlatform bukkitNpcPlatformBuilder() { + return new BukkitPlatform(); + } + + @Override + protected void prepareBuild() { + // set the profile resolver to a native platform one if not given + if (this.profileResolver == null) { + this.profileResolver = BukkitProfileResolver.profileResolver(); + } + + // set the default task manager + if (this.taskManager == null) { + this.taskManager = BukkitPlatformTaskManager.taskManager(this.extension); + } + + // set the default version accessor + if (this.versionAccessor == null) { + this.versionAccessor = BukkitVersionAccessor.versionAccessor(); + } + + // set the default world accessor + if (this.worldAccessor == null) { + this.worldAccessor = BukkitWorldAccessor.worldAccessor(); + } + + // set the default packet adapter + if (this.packetAdapter == null) { + this.packetAdapter = BukkitProtocolAdapter.packetAdapter(); + } + } + + @Override + protected @NotNull Platform doBuild() { + // check if we need an action controller + NpcActionController actionController = null; + if (this.actionControllerDecorator != null) { + NpcActionController.Builder builder = BukkitActionController.actionControllerBuilder( + this.extension, + this.eventBus, + this.npcTracker); + this.actionControllerDecorator.accept(builder); + actionController = builder.build(); + } + + // build the platform + return new CommonPlatform<>( + this.debug, + this.extension, + this.npcTracker, + this.profileResolver, + this.taskManager, + actionController, + this.versionAccessor, + this.eventBus, + this.worldAccessor, + this.packetAdapter); + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatformTaskManager.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatformTaskManager.java new file mode 100644 index 0000000..3ed25d5 --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitPlatformTaskManager.java @@ -0,0 +1,64 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit; + +import com.github.juliarn.npclib.api.PlatformTaskManager; +import java.util.Objects; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +public final class BukkitPlatformTaskManager implements PlatformTaskManager { + + private final Plugin plugin; + + private BukkitPlatformTaskManager(Plugin plugin) { + this.plugin = plugin; + } + + public static @NotNull PlatformTaskManager taskManager(@NotNull Plugin plugin) { + Objects.requireNonNull(plugin, "plugin"); + return new BukkitPlatformTaskManager(plugin); + } + + @Override + public void scheduleSync(@NotNull Runnable task) { + this.plugin.getServer().getScheduler().runTask(this.plugin, task); + } + + @Override + public void scheduleDelayedSync(@NotNull Runnable task, int delayTicks) { + this.plugin.getServer().getScheduler().runTaskLater(this.plugin, task, delayTicks); + } + + @Override + public void scheduleAsync(@NotNull Runnable task) { + this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, task); + } + + @Override + public void scheduleDelayedAsync(@NotNull Runnable task, int delayTicks) { + this.plugin.getServer().getScheduler().runTaskLaterAsynchronously(this.plugin, task, delayTicks); + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitProfileResolver.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitProfileResolver.java new file mode 100644 index 0000000..c1b8137 --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitProfileResolver.java @@ -0,0 +1,125 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.github.juliarn.npclib.api.profile.Profile; +import com.github.juliarn.npclib.api.profile.ProfileProperty; +import com.github.juliarn.npclib.api.profile.ProfileResolver; +import io.papermc.lib.PaperLib; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; + +public final class BukkitProfileResolver { + + private BukkitProfileResolver() { + throw new UnsupportedOperationException(); + } + + public static @NotNull ProfileResolver profileResolver() { + // check if we're on paper and newer than 1.12 (when the profile API was introduced) + if (PaperLib.isPaper() && PaperLib.isVersion(12)) { + return PaperProfileResolver.INSTANCE; + } + + // check if we're on spigot and newer than 1.18.2 (when the profile API was introduced) + if (PaperLib.isSpigot() && PaperLib.isVersion(18, 2)) { + return SpigotProfileResolver.INSTANCE; + } + + // use fallback resolver + return LegacyResolver.INSTANCE; + } + + private static final class PaperProfileResolver implements ProfileResolver { + + private static final ProfileResolver INSTANCE = new PaperProfileResolver(); + + @Override + public @NotNull CompletableFuture resolveProfile(@NotNull Profile profile) { + return CompletableFuture.supplyAsync(() -> { + // create a profile from the given one and try to complete it + PlayerProfile playerProfile = Bukkit.createProfile(profile.uniqueId(), profile.name()); + playerProfile.complete(true, true); + + // convert the profile properties to the wrapper one + Set properties = playerProfile.getProperties() + .stream() + .map(prop -> ProfileProperty.property(prop.getName(), prop.getValue(), prop.getSignature())) + .collect(Collectors.toSet()); + + // create the resolved profile + //noinspection ConstantConditions + return Profile.resolved(playerProfile.getName(), playerProfile.getId(), properties); + }); + } + } + + private static final class SpigotProfileResolver implements ProfileResolver { + + private static final ProfileResolver INSTANCE = new SpigotProfileResolver(); + private static final Pattern DATA_EXTRACT_PATTERN = Pattern.compile("^CraftPlayerTextures \\[data=(.*)]$"); + + @Override + @SuppressWarnings("deprecation") // deprecated by paper, but we only use this on spigot + public @NotNull CompletableFuture resolveProfile(@NotNull Profile profile) { + // create the profile and fill in the empty values + org.bukkit.profile.PlayerProfile playerProfile = Bukkit.createPlayerProfile(profile.uniqueId(), profile.name()); + return playerProfile.update().thenApply(resolvedProfile -> { + // hack to get the data from the profile + Matcher matcher = DATA_EXTRACT_PATTERN.matcher(resolvedProfile.getTextures().toString()); + if (matcher.matches()) { + // encode the raw texture data + byte[] rawTextureData = matcher.group(1).getBytes(StandardCharsets.UTF_8); + String encodedTextureData = Base64.getEncoder().encodeToString(rawTextureData); + + // create the profile from the spigot one + ProfileProperty textureProperty = ProfileProperty.property("textures", encodedTextureData); + Set properties = Collections.singleton(textureProperty); + + // create the resolved profile + //noinspection ConstantConditions + return Profile.resolved(playerProfile.getName(), playerProfile.getUniqueId(), properties); + } + + // unable to complete the profile + throw new IllegalArgumentException("Profile texture input: " + resolvedProfile.getTextures() + " is invalid"); + }); + } + } + + private static final class LegacyResolver { + + private static final ProfileResolver INSTANCE = ProfileResolver.caching(ProfileResolver.mojang()); + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitVersionAccessor.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitVersionAccessor.java new file mode 100644 index 0000000..5419953 --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitVersionAccessor.java @@ -0,0 +1,64 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit; + +import com.github.juliarn.npclib.api.PlatformVersionAccessor; +import io.papermc.lib.PaperLib; + +public class BukkitVersionAccessor { + + private BukkitVersionAccessor() { + throw new UnsupportedOperationException(); + } + + public static PlatformVersionAccessor versionAccessor() { + return PaperLibPlatformVersionAccessor.INSTANCE; + } + + private static final class PaperLibPlatformVersionAccessor implements PlatformVersionAccessor { + + private static final PaperLibPlatformVersionAccessor INSTANCE = new PaperLibPlatformVersionAccessor(); + + @Override + public int major() { + return 1; + } + + @Override + public int minor() { + return PaperLib.getMinecraftVersion(); + } + + @Override + public int patch() { + return PaperLib.getMinecraftPatchVersion(); + } + + @Override + public boolean atLeast(int major, int minor, int patch) { + return PaperLib.isVersion(minor, patch); + } + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitWorldAccessor.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitWorldAccessor.java new file mode 100644 index 0000000..e793e63 --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/BukkitWorldAccessor.java @@ -0,0 +1,88 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit; + +import com.github.juliarn.npclib.api.PlatformWorldAccessor; +import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class BukkitWorldAccessor { + + private BukkitWorldAccessor() { + throw new UnsupportedOperationException(); + } + + public static @NotNull PlatformWorldAccessor worldAccessor() { + // check if we are on paper and newer (or equal) to 1.16.5 + if (PaperLib.isPaper() && PaperLib.isVersion(16, 5)) { + return ModernAccessor.INSTANCE; + } else { + return LegacyAccessor.INSTANCE; + } + } + + public static @NotNull PlatformWorldAccessor nameBasedAccessor() { + return LegacyAccessor.INSTANCE; + } + + public static @NotNull PlatformWorldAccessor keyBasedAccessor() { + return ModernAccessor.INSTANCE; + } + + private static final class LegacyAccessor implements PlatformWorldAccessor { + + private static final PlatformWorldAccessor INSTANCE = new LegacyAccessor(); + + @Override + public @NotNull String extractWorldIdentifier(@NotNull World world) { + return world.getName(); + } + + @Override + public @Nullable World resolveWorldFromIdentifier(@NotNull String identifier) { + return Bukkit.getWorld(identifier); + } + } + + private static final class ModernAccessor implements PlatformWorldAccessor { + + private static final PlatformWorldAccessor INSTANCE = new ModernAccessor(); + + @Override + public @NotNull String extractWorldIdentifier(@NotNull World world) { + return world.getKey().asString(); + } + + @Override + public @Nullable World resolveWorldFromIdentifier(@NotNull String identifier) { + NamespacedKey key = NamespacedKey.fromString(identifier); + return key == null ? null : Bukkit.getWorld(key); + } + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/BukkitProtocolAdapter.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/BukkitProtocolAdapter.java new file mode 100644 index 0000000..52cba9e --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/BukkitProtocolAdapter.java @@ -0,0 +1,58 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit.protocol; + +import com.github.juliarn.npclib.api.protocol.PlatformPacketAdapter; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +public final class BukkitProtocolAdapter { + + private BukkitProtocolAdapter() { + throw new UnsupportedOperationException(); + } + + public static @NotNull PlatformPacketAdapter packetAdapter() { + // check if protocol lib is available + if (Bukkit.getPluginManager().getPlugin("ProtocolLib") != null) { + return ProtocolLibPacketAdapter.INSTANCE; + } + + // fallback + return PacketEventsPacketAdapter.INSTANCE; + } + + public static @NotNull PlatformPacketAdapter protocolLib() { + return ProtocolLibPacketAdapter.INSTANCE; + } + + public static @NotNull PlatformPacketAdapter packetEvents() { + return PacketEventsPacketAdapter.INSTANCE; + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/PacketEventsPacketAdapter.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/PacketEventsPacketAdapter.java new file mode 100644 index 0000000..1eb8878 --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/PacketEventsPacketAdapter.java @@ -0,0 +1,435 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit.protocol; + +import com.github.juliarn.npclib.api.Npc; +import com.github.juliarn.npclib.api.Platform; +import com.github.juliarn.npclib.api.PlatformVersionAccessor; +import com.github.juliarn.npclib.api.Position; +import com.github.juliarn.npclib.api.event.InteractNpcEvent; +import com.github.juliarn.npclib.api.profile.ProfileProperty; +import com.github.juliarn.npclib.api.protocol.OutboundPacket; +import com.github.juliarn.npclib.api.protocol.PlatformPacketAdapter; +import com.github.juliarn.npclib.api.protocol.enums.EntityAnimation; +import com.github.juliarn.npclib.api.protocol.enums.EntityPose; +import com.github.juliarn.npclib.api.protocol.enums.ItemSlot; +import com.github.juliarn.npclib.api.protocol.enums.PlayerInfoAction; +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadata; +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory; +import com.github.juliarn.npclib.common.event.DefaultAttackNpcEvent; +import com.github.juliarn.npclib.common.event.DefaultInteractNpcEvent; +import com.github.juliarn.npclib.common.util.EventDispatcher; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; +import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent; +import com.github.retrooper.packetevents.manager.player.PlayerManager; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.github.retrooper.packetevents.protocol.player.GameMode; +import com.github.retrooper.packetevents.protocol.player.InteractionHand; +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.protocol.world.Location; +import com.github.retrooper.packetevents.settings.PacketEventsSettings; +import com.github.retrooper.packetevents.util.TimeStampMode; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityAnimation; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEquipment; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityHeadLook; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityRotation; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfo; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPluginMessage; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnPlayer; +import com.google.common.collect.ImmutableMap; +import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; +import io.github.retrooper.packetevents.util.SpigotReflectionUtil; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import net.kyori.adventure.text.Component; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +final class PacketEventsPacketAdapter implements PlatformPacketAdapter { + + static final PacketEventsPacketAdapter INSTANCE = new PacketEventsPacketAdapter(); + + private static final PacketEventsSettings PACKET_EVENTS_SETTINGS = new PacketEventsSettings() + .debug(false) + .bStats(true) + .checkForUpdates(false) + .readOnlyListeners(true) + .timeStampMode(TimeStampMode.NONE); + + private static final EnumMap ITEM_SLOT_CONVERTER; + private static final EnumMap HAND_CONVERTER; + private static final EnumMap PLAYER_INFO_ACTION_CONVERTER; + private static final EnumMap ENTITY_ANIMATION_CONVERTER; + private static final EnumMap ENTITY_POSE_CONVERTER; + + // serializer converters for metadata + private static final Map> ENTITY_DATA_TYPE_LOOKUP; + private static final Map> SERIALIZER_CONVERTERS; + + static { + // associate item slots actions with their respective packet events enum + ITEM_SLOT_CONVERTER = new EnumMap<>(ItemSlot.class); + ITEM_SLOT_CONVERTER.put(ItemSlot.MAIN_HAND, EquipmentSlot.MAINHAND); + ITEM_SLOT_CONVERTER.put(ItemSlot.OFF_HAND, EquipmentSlot.OFFHAND); + ITEM_SLOT_CONVERTER.put(ItemSlot.FEET, EquipmentSlot.BOOTS); + ITEM_SLOT_CONVERTER.put(ItemSlot.LEGS, EquipmentSlot.LEGGINGS); + ITEM_SLOT_CONVERTER.put(ItemSlot.CHEST, EquipmentSlot.CHESTPLATE); + ITEM_SLOT_CONVERTER.put(ItemSlot.HEAD, EquipmentSlot.HELMET); + + // associate hand actions with their respective packet events enum + HAND_CONVERTER = new EnumMap<>(InteractionHand.class); + HAND_CONVERTER.put(InteractionHand.MAIN_HAND, InteractNpcEvent.Hand.MAIN_HAND); + HAND_CONVERTER.put(InteractionHand.OFF_HAND, InteractNpcEvent.Hand.OFF_HAND); + + // associate player info actions with their respective packet events enum + PLAYER_INFO_ACTION_CONVERTER = new EnumMap<>(PlayerInfoAction.class); + PLAYER_INFO_ACTION_CONVERTER.put(PlayerInfoAction.ADD_PLAYER, WrapperPlayServerPlayerInfo.Action.ADD_PLAYER); + PLAYER_INFO_ACTION_CONVERTER.put(PlayerInfoAction.REMOVE_PLAYER, WrapperPlayServerPlayerInfo.Action.REMOVE_PLAYER); + + // associate entity animations with their respective packet events enum + ENTITY_ANIMATION_CONVERTER = new EnumMap<>(EntityAnimation.class); + ENTITY_ANIMATION_CONVERTER.put( + EntityAnimation.SWING_MAIN_ARM, + WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_MAIN_ARM); + ENTITY_ANIMATION_CONVERTER.put( + EntityAnimation.TAKE_DAMAGE, + WrapperPlayServerEntityAnimation.EntityAnimationType.TAKE_DAMAGE); + ENTITY_ANIMATION_CONVERTER.put( + EntityAnimation.LEAVE_BED, + WrapperPlayServerEntityAnimation.EntityAnimationType.LEAVE_BED); + ENTITY_ANIMATION_CONVERTER.put( + EntityAnimation.SWING_OFF_HAND, + WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_OFFHAND); + ENTITY_ANIMATION_CONVERTER.put( + EntityAnimation.CRITICAL_EFFECT, + WrapperPlayServerEntityAnimation.EntityAnimationType.CRITICAL_EFFECT); + ENTITY_ANIMATION_CONVERTER.put( + EntityAnimation.MAGIC_CRITICAL_EFFECT, + WrapperPlayServerEntityAnimation.EntityAnimationType.MAGIC_CRITICAL_EFFECT); + + // associate entity poses with their respective packet events enum + ENTITY_POSE_CONVERTER = new EnumMap<>(EntityPose.class); + ENTITY_POSE_CONVERTER.put( + EntityPose.STANDING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.STANDING); + ENTITY_POSE_CONVERTER.put( + EntityPose.FALL_FLYING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.FALL_FLYING); + ENTITY_POSE_CONVERTER.put( + EntityPose.SLEEPING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.SLEEPING); + ENTITY_POSE_CONVERTER.put( + EntityPose.SWIMMING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.SWIMMING); + ENTITY_POSE_CONVERTER.put( + EntityPose.SPIN_ATTACK, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.SPIN_ATTACK); + ENTITY_POSE_CONVERTER.put( + EntityPose.CROUCHING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.CROUCHING); + ENTITY_POSE_CONVERTER.put( + EntityPose.LONG_JUMPING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.LONG_JUMPING); + ENTITY_POSE_CONVERTER.put( + EntityPose.DYING, + com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.DYING); + + // meta serializers + //noinspection SuspiciousMethodCalls + SERIALIZER_CONVERTERS = ImmutableMap.of(EntityPose.class, ENTITY_POSE_CONVERTER::get); + ENTITY_DATA_TYPE_LOOKUP = ImmutableMap.>builder() + .put(byte.class, EntityDataTypes.BYTE) + .put(int.class, EntityDataTypes.INT) + .put(float.class, EntityDataTypes.FLOAT) + .put(boolean.class, EntityDataTypes.BOOLEAN) + .put(String.class, EntityDataTypes.STRING) + .put(com.github.retrooper.packetevents.protocol.entity.pose.EntityPose.class, EntityDataTypes.ENTITY_POSE) + .build(); + } + + // lazy initialized, then never null again + private ServerVersion serverVersion; + private PlayerManager packetPlayerManager; + + private static Location npcLocation(@NotNull Npc npc) { + Position pos = npc.position(); + return new Location(pos.x(), pos.y(), pos.z(), pos.yaw(), pos.pitch()); + } + + private static @NotNull EntityData createEntityData(int index, @NotNull Type type, @NotNull Object value) { + // get the type information of the value to write + Class valueType; + if (type instanceof Class) { + // direct access via the given class type + valueType = (Class) type; + } else if (type instanceof ParameterizedType) { + // optional type (access via first type parameter) + ParameterizedType parameterizedType = (ParameterizedType) type; + valueType = (Class) parameterizedType.getActualTypeArguments()[0]; + } else { + // unable to handle that + throw new IllegalArgumentException("Unsupported type: " + type); + } + + // pre-convert the value if needed + Function metaConverter = SERIALIZER_CONVERTERS.get(valueType); + if (metaConverter != null) { + value = metaConverter.apply(value); + } + + return new EntityData(index, ENTITY_DATA_TYPE_LOOKUP.get(type), value); + } + + @Override + public @NotNull OutboundPacket createEntitySpawnPacket() { + return (player, npc) -> { + // SpawnPlayer (https://wiki.vg/Protocol#Spawn_Player) + Location location = npcLocation(npc); + PacketWrapper wrapper = new WrapperPlayServerSpawnPlayer(npc.entityId(), npc.profile().uniqueId(), location); + + // send the packet without notifying any listeners + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }; + } + + @Override + public @NotNull OutboundPacket createEntityRemovePacket() { + return (player, npc) -> { + // DestroyEntities (https://wiki.vg/Protocol#Destroy_Entities) + PacketWrapper wrapper = new WrapperPlayServerDestroyEntities(npc.entityId()); + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }; + } + + @Override + public @NotNull OutboundPacket createPlayerInfoPacket( + @NotNull PlayerInfoAction action + ) { + return (player, npc) -> npc.settings().profileResolver().resolveNpcProfile(player, npc).thenAcceptAsync(profile -> { + // convert the profile to a UserProfile + UserProfile userProfile = new UserProfile(profile.uniqueId(), profile.name()); + for (ProfileProperty property : profile.properties()) { + TextureProperty textureProperty = new TextureProperty(property.name(), property.value(), property.signature()); + userProfile.getTextureProperties().add(textureProperty); + } + + // create the player profile data + WrapperPlayServerPlayerInfo.PlayerData playerData = new WrapperPlayServerPlayerInfo.PlayerData( + Component.empty(), + userProfile, + GameMode.CREATIVE, + 20); + + // PlayerInfo (https://wiki.vg/Protocol#Player_Info) + WrapperPlayServerPlayerInfo.Action playerInfoAction = PLAYER_INFO_ACTION_CONVERTER.get(action); + WrapperPlayServerPlayerInfo wrapper = new WrapperPlayServerPlayerInfo(playerInfoAction, playerData); + + // send the packet without notifying any listeners + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }); + } + + @Override + public @NotNull OutboundPacket createRotationPacket(float yaw, float pitch) { + return (player, npc) -> { + Position pos = npc.position(); + + // head rotation (https://wiki.vg/Protocol#Entity_Head_Look) + PacketWrapper headRotation = new WrapperPlayServerEntityHeadLook(npc.entityId(), pos.yaw()); + + // entity teleport (https://wiki.vg/Protocol#Entity_Teleport) or Player Rotation (https://wiki.vg/Protocol#Player_Rotation) + PacketWrapper rotation; + if (this.serverVersion.isNewerThanOrEquals(ServerVersion.V_1_9)) { + // mc 1.9: player rotation + rotation = new WrapperPlayServerEntityRotation(npc.entityId(), pos.yaw(), pos.pitch(), true); + } else { + // mc 1.8: entity teleport + rotation = new WrapperPlayServerEntityTeleport(npc.entityId(), npcLocation(npc), true); + } + + // send the packet without notifying any listeners + this.packetPlayerManager.sendPacketSilently(player, rotation); + this.packetPlayerManager.sendPacketSilently(player, headRotation); + }; + } + + @Override + public @NotNull OutboundPacket createAnimationPacket( + @NotNull EntityAnimation animation + ) { + return (player, npc) -> { + // EntityAnimation (https://wiki.vg/Protocol#Entity_Animation_.28clientbound.29) + WrapperPlayServerEntityAnimation.EntityAnimationType animationType = ENTITY_ANIMATION_CONVERTER.get(animation); + PacketWrapper wrapper = new WrapperPlayServerEntityAnimation(npc.entityId(), animationType); + + // send the packet without notifying any listeners + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }; + } + + @Override + public @NotNull OutboundPacket createEquipmentPacket( + @NotNull ItemSlot slot, + @NotNull ItemStack item + ) { + return (player, npc) -> { + EquipmentSlot equipmentSlot = ITEM_SLOT_CONVERTER.get(slot); + com.github.retrooper.packetevents.protocol.item.ItemStack is = SpigotReflectionUtil.decodeBukkitItemStack(item); + + // EntityEquipment (https://wiki.vg/Protocol#Entity_Equipment) + Equipment equipment = new Equipment(equipmentSlot, is); + PacketWrapper wrapper = new WrapperPlayServerEntityEquipment(npc.entityId(), equipment); + + // send the packet without notifying any listeners + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }; + } + + @Override + public @NotNull OutboundPacket createCustomPayloadPacket( + @NotNull String channelId, + byte[] payload + ) { + return (player, npc) -> { + // CustomPayload (https://wiki.vg/Protocol#Custom_Payload) + PacketWrapper wrapper = new WrapperPlayServerPluginMessage(channelId, payload); + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }; + } + + @Override + public @NotNull OutboundPacket createEntityMetaPacket( + @NotNull T value, + @NotNull EntityMetadataFactory metadata + ) { + return (player, npc) -> { + // create the entity meta + PlatformVersionAccessor versionAccessor = npc.platform().versionAccessor(); + EntityMetadata entityMetadata = metadata.create(value, versionAccessor); + + // check if the meta is available + if (!entityMetadata.available()) { + return; + } + + // construct the meta we want to send out + List entityData = new ArrayList<>(); + entityData.add(createEntityData( + entityMetadata.index(), + entityMetadata.type(), + entityMetadata.value())); + + // add ll dependant metas + for (EntityMetadataFactory relatedMetadata : metadata.relatedMetadata()) { + EntityMetadata related = relatedMetadata.create(value, versionAccessor); + if (related.available()) { + entityData.add(createEntityData(related.index(), related.type(), related.value())); + } + } + + // EntityMetadata (https://wiki.vg/Protocol#Entity_Metadata) + PacketWrapper wrapper = new WrapperPlayServerEntityMetadata(npc.entityId(), entityData); + this.packetPlayerManager.sendPacketSilently(player, wrapper); + }; + } + + @Override + public void initialize(@NotNull Platform platform) { + // build and initialize the packet events api + PacketEventsAPI packetEventsApi = SpigotPacketEventsBuilder.buildNoCache( + platform.extension(), + PACKET_EVENTS_SETTINGS); + packetEventsApi.init(); + + // store the packet player manager & server version + this.packetPlayerManager = packetEventsApi.getPlayerManager(); + this.serverVersion = packetEventsApi.getServerManager().getVersion(); + + // add the packet listener + packetEventsApi.getEventManager().registerListener(new NpcUsePacketAdapter(platform)); + } + + private static final class NpcUsePacketAdapter extends SimplePacketListenerAbstract { + + private final Platform platform; + + public NpcUsePacketAdapter(@NotNull Platform platform) { + super(PacketListenerPriority.MONITOR); + this.platform = platform; + } + + @Override + public void onPacketPlayReceive(@NotNull PacketPlayReceiveEvent event) { + // check for an entity use packet + Object player = event.getPlayer(); + if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) { + WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event); + + // get the associated npc from the tracked entities + Npc npc = this.platform.npcTracker().npcById(packet.getEntityId()); + if (npc != null) { + // call the event + switch (packet.getAction()) { + case ATTACK: + EventDispatcher.dispatch(this.platform, DefaultAttackNpcEvent.attackNpc(npc, player)); + break; + case INTERACT: + InteractNpcEvent.Hand hand = HAND_CONVERTER.get(packet.getHand()); + EventDispatcher.dispatch(this.platform, DefaultInteractNpcEvent.interactNpc(npc, player, hand)); + break; + default: + // we don't handle INTERACT_AT as the client sends it alongside the interact packet (duplicate event call) + break; + } + + // don't pass the packet to the server + event.setCancelled(true); + } + } + } + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/ProtocolLibPacketAdapter.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/ProtocolLibPacketAdapter.java new file mode 100644 index 0000000..7d6d55e --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/protocol/ProtocolLibPacketAdapter.java @@ -0,0 +1,508 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit.protocol; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.comphenix.protocol.wrappers.EnumWrappers; +import com.comphenix.protocol.wrappers.MinecraftKey; +import com.comphenix.protocol.wrappers.Pair; +import com.comphenix.protocol.wrappers.PlayerInfoData; +import com.comphenix.protocol.wrappers.WrappedChatComponent; +import com.comphenix.protocol.wrappers.WrappedDataWatcher; +import com.comphenix.protocol.wrappers.WrappedEnumEntityUseAction; +import com.comphenix.protocol.wrappers.WrappedGameProfile; +import com.comphenix.protocol.wrappers.WrappedSignedProperty; +import com.comphenix.protocol.wrappers.WrappedWatchableObject; +import com.github.juliarn.npclib.api.Npc; +import com.github.juliarn.npclib.api.Platform; +import com.github.juliarn.npclib.api.PlatformVersionAccessor; +import com.github.juliarn.npclib.api.event.InteractNpcEvent; +import com.github.juliarn.npclib.api.profile.ProfileProperty; +import com.github.juliarn.npclib.api.protocol.OutboundPacket; +import com.github.juliarn.npclib.api.protocol.PlatformPacketAdapter; +import com.github.juliarn.npclib.api.protocol.enums.EntityAnimation; +import com.github.juliarn.npclib.api.protocol.enums.EntityPose; +import com.github.juliarn.npclib.api.protocol.enums.ItemSlot; +import com.github.juliarn.npclib.api.protocol.enums.PlayerInfoAction; +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadata; +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory; +import com.github.juliarn.npclib.common.event.DefaultAttackNpcEvent; +import com.github.juliarn.npclib.common.event.DefaultInteractNpcEvent; +import com.github.juliarn.npclib.common.util.EventDispatcher; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +final class ProtocolLibPacketAdapter implements PlatformPacketAdapter { + + static final ProtocolLibPacketAdapter INSTANCE = new ProtocolLibPacketAdapter(); + + private static final ProtocolManager PROTOCOL_MANAGER = ProtocolLibrary.getProtocolManager(); + private static final MinecraftVersion SERVER_VERSION = MinecraftVersion.fromServerVersion(Bukkit.getVersion()); + + private static final WrappedChatComponent EMPTY_COMPONENT = WrappedChatComponent.fromText(""); + + private static final EnumMap ITEM_SLOT_CONVERTER; + private static final EnumMap HAND_CONVERTER; + private static final EnumMap ENTITY_POSE_CONVERTER; + private static final EnumMap PLAYER_INFO_ACTION_CONVERTER; + + // serializer converters for metadata + private static final Map> SERIALIZER_CONVERTERS; + + static { + // associate item slots with their respective protocol lib enum + ITEM_SLOT_CONVERTER = new EnumMap<>(ItemSlot.class); + ITEM_SLOT_CONVERTER.put(ItemSlot.MAIN_HAND, EnumWrappers.ItemSlot.MAINHAND); + ITEM_SLOT_CONVERTER.put(ItemSlot.OFF_HAND, EnumWrappers.ItemSlot.OFFHAND); + ITEM_SLOT_CONVERTER.put(ItemSlot.FEET, EnumWrappers.ItemSlot.FEET); + ITEM_SLOT_CONVERTER.put(ItemSlot.LEGS, EnumWrappers.ItemSlot.LEGS); + ITEM_SLOT_CONVERTER.put(ItemSlot.CHEST, EnumWrappers.ItemSlot.CHEST); + ITEM_SLOT_CONVERTER.put(ItemSlot.HEAD, EnumWrappers.ItemSlot.HEAD); + + // associate hands with their respective protocol lib enum + HAND_CONVERTER = new EnumMap<>(EnumWrappers.Hand.class); + HAND_CONVERTER.put(EnumWrappers.Hand.MAIN_HAND, InteractNpcEvent.Hand.MAIN_HAND); + HAND_CONVERTER.put(EnumWrappers.Hand.OFF_HAND, InteractNpcEvent.Hand.OFF_HAND); + + // associate entity poses with their respective protocol lib enum + ENTITY_POSE_CONVERTER = new EnumMap<>(EntityPose.class); + ENTITY_POSE_CONVERTER.put(EntityPose.STANDING, EnumWrappers.EntityPose.STANDING); + ENTITY_POSE_CONVERTER.put(EntityPose.FALL_FLYING, EnumWrappers.EntityPose.FALL_FLYING); + ENTITY_POSE_CONVERTER.put(EntityPose.SLEEPING, EnumWrappers.EntityPose.SLEEPING); + ENTITY_POSE_CONVERTER.put(EntityPose.SWIMMING, EnumWrappers.EntityPose.SWIMMING); + ENTITY_POSE_CONVERTER.put(EntityPose.SPIN_ATTACK, EnumWrappers.EntityPose.SPIN_ATTACK); + ENTITY_POSE_CONVERTER.put(EntityPose.CROUCHING, EnumWrappers.EntityPose.CROUCHING); + ENTITY_POSE_CONVERTER.put(EntityPose.LONG_JUMPING, EnumWrappers.EntityPose.LONG_JUMPING); + ENTITY_POSE_CONVERTER.put(EntityPose.DYING, EnumWrappers.EntityPose.DYING); + + // associate player info actions with their respective protocol lib enum + PLAYER_INFO_ACTION_CONVERTER = new EnumMap<>(PlayerInfoAction.class); + PLAYER_INFO_ACTION_CONVERTER.put(PlayerInfoAction.ADD_PLAYER, EnumWrappers.PlayerInfoAction.ADD_PLAYER); + PLAYER_INFO_ACTION_CONVERTER.put(PlayerInfoAction.REMOVE_PLAYER, EnumWrappers.PlayerInfoAction.REMOVE_PLAYER); + + // meta serializers + //noinspection SuspiciousMethodCalls + SERIALIZER_CONVERTERS = ImmutableMap.of(EntityPose.class, ENTITY_POSE_CONVERTER::get); + } + + private static @NotNull WrappedWatchableObject createWatchableObject( + int index, + @NotNull Type type, + @NotNull Object value + ) { + // get the type information of the value to write + Class valueType; + if (type instanceof Class) { + // direct access via the given class type + valueType = (Class) type; + } else if (type instanceof ParameterizedType) { + // optional type (access via first type parameter) + ParameterizedType parameterizedType = (ParameterizedType) type; + valueType = (Class) parameterizedType.getActualTypeArguments()[0]; + } else { + // unable to handle that + throw new IllegalArgumentException("Unsupported type: " + type); + } + + // pre-convert the value if needed + Function metaConverter = SERIALIZER_CONVERTERS.get(valueType); + if (metaConverter != null) { + value = metaConverter.apply(value); + } + + if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { + // mc 1.9: watchable object now contains a serializer for the type + WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.get( + valueType, + type instanceof ParameterizedType); + // create the watchable object + return new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(index, serializer), value); + } else { + // mc 1.8: watchable object id + return new WrappedWatchableObject(index, value); + } + } + + @Override + public @NotNull OutboundPacket createEntitySpawnPacket() { + return (player, npc) -> { + // SpawnPlayer (https://wiki.vg/Protocol#Spawn_Player) + PacketContainer container = new PacketContainer(PacketType.Play.Server.NAMED_ENTITY_SPAWN); + + // base information + container.getIntegers().write(0, npc.entityId()); + container.getUUIDs().write(0, npc.profile().uniqueId()); + + // position + if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { + // mc 1.9: new position format (plain doubles) + container.getDoubles() + .write(0, npc.position().x()) + .write(1, npc.position().y()) + .write(2, npc.position().z()); + } else { + // mc 1.8: old position format (rotation angles) + container.getIntegers() + .write(1, (int) Math.floor(npc.position().x() * 32.0D)) + .write(2, (int) Math.floor(npc.position().y() * 32.0D)) + .write(3, (int) Math.floor(npc.position().z() * 32.0D)); + } + + // rotation (angles) + container.getBytes() + .write(0, (byte) (npc.position().yaw() * 256F / 360F)) + .write(1, (byte) (npc.position().pitch() * 256F / 360F)); + + // metadata if on an old server version (< 15) + if (MinecraftVersion.VILLAGE_UPDATE.isAtLeast(SERVER_VERSION)) { + container.getDataWatcherModifier().write(0, new WrappedDataWatcher()); + } + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }; + } + + @Override + public @NotNull OutboundPacket createEntityRemovePacket() { + return (player, npc) -> { + // DestroyEntities (https://wiki.vg/Protocol#Destroy_Entities) + PacketContainer container = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY); + + // entity id + if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { + // mc 1.17: entity ids is a list + container.getIntLists().write(0, Lists.newArrayList(npc.entityId())); + } else { + // mc 1.8: entity ids is an int array + container.getIntegerArrays().write(0, new int[]{npc.entityId()}); + } + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }; + } + + @Override + public @NotNull OutboundPacket createPlayerInfoPacket( + @NotNull PlayerInfoAction action + ) { + return (player, npc) -> { + // PlayerInfo (https://wiki.vg/Protocol#Player_Info) + PacketContainer container = new PacketContainer(PacketType.Play.Server.PLAYER_INFO); + + // action + EnumWrappers.PlayerInfoAction playerInfoAction = PLAYER_INFO_ACTION_CONVERTER.get(action); + container.getPlayerInfoAction().write(0, playerInfoAction); + + // player info + npc.settings().profileResolver().resolveNpcProfile(player, npc).thenAcceptAsync(resolvedProfile -> { + // convert to a protocol lib profile + WrappedGameProfile profile = new WrappedGameProfile(resolvedProfile.uniqueId(), resolvedProfile.name()); + for (ProfileProperty prop : resolvedProfile.properties()) { + WrappedSignedProperty wrapped = new WrappedSignedProperty(prop.name(), prop.value(), prop.signature()); + profile.getProperties().put(prop.name(), wrapped); + } + + // add the player info data + PlayerInfoData playerInfoData = new PlayerInfoData( + profile, + 20, + EnumWrappers.NativeGameMode.CREATIVE, + EMPTY_COMPONENT); + container.getPlayerInfoDataLists().write(0, Lists.newArrayList(playerInfoData)); + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }); + }; + } + + @Override + public @NotNull OutboundPacket createRotationPacket(float yaw, float pitch) { + return (player, npc) -> { + // pre-calculate the yaw and pitch angle values + byte yawAngle = (byte) (yaw * 256F / 360F); + byte pitchAngle = (byte) (pitch * 256F / 360F); + + // head rotation (https://wiki.vg/Protocol#Entity_Head_Look) + PacketContainer headRotation = new PacketContainer(PacketType.Play.Server.ENTITY_HEAD_ROTATION); + headRotation.getBytes().write(0, yawAngle); + headRotation.getIntegers().write(0, npc.entityId()); + + // entity teleport (https://wiki.vg/Protocol#Entity_Teleport) or Player Rotation (https://wiki.vg/Protocol#Player_Rotation) + PacketContainer rotation; + if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { + // mc 1.9: player rotation + rotation = new PacketContainer(PacketType.Play.Server.ENTITY_LOOK); + } else { + // mc 1.8: entity teleport + rotation = new PacketContainer(PacketType.Play.Server.ENTITY_TELEPORT); + rotation.getIntegers() + .write(1, (int) Math.floor(npc.position().x() * 32.0D)) + .write(2, (int) Math.floor(npc.position().y() * 32.0D)) + .write(3, (int) Math.floor(npc.position().z() * 32.0D)); + } + + // entity id + rotation.getIntegers().write(0, npc.entityId()); + + // rotation (angles) + rotation.getBytes() + .write(0, yawAngle) + .write(1, pitchAngle); + + // ground status + rotation.getBooleans().write(0, true); + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, rotation, false); + PROTOCOL_MANAGER.sendServerPacket(player, headRotation, false); + }; + } + + @Override + public @NotNull OutboundPacket createAnimationPacket( + @NotNull EntityAnimation animation + ) { + return (player, npc) -> { + // EntityAnimation (https://wiki.vg/Protocol#Entity_Animation_.28clientbound.29) + PacketContainer container = new PacketContainer(PacketType.Play.Server.ANIMATION); + + // entity id & animation id + container.getIntegers() + .write(0, npc.entityId()) + .write(1, animation.id()); + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }; + } + + @Override + public @NotNull OutboundPacket createEquipmentPacket( + @NotNull ItemSlot slot, + @NotNull ItemStack item + ) { + return (player, npc) -> { + // EntityEquipment (https://wiki.vg/Protocol#Entity_Equipment) + PacketContainer container = new PacketContainer(PacketType.Play.Server.ENTITY_EQUIPMENT); + + // entity id + container.getIntegers().write(0, npc.entityId()); + + // item + if (MinecraftVersion.NETHER_UPDATE.atOrAbove()) { + // mc 1.16: item slot & item stack pairs + EnumWrappers.ItemSlot itemSlot = ITEM_SLOT_CONVERTER.get(slot); + container.getSlotStackPairLists().write(0, Lists.newArrayList(new Pair<>(itemSlot, item))); + } else { + if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { + // mc 1.9: item slot + container.getItemSlots().write(0, ITEM_SLOT_CONVERTER.get(slot)); + } else { + // mc 1.8: item slot id + int slotId = slot.ordinal(); + if (slotId > 0) { + // off-hand did not exist in 1.8, so all ids are shifted one down + slotId -= 1; + } + + container.getIntegers().write(1, slotId); + } + + // the actual item + container.getItemModifier().write(0, item); + } + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }; + } + + @Override + public @NotNull OutboundPacket createCustomPayloadPacket( + @NotNull String channelId, + byte[] payload + ) { + return (player, npc) -> { + // CustomPayload (https://wiki.vg/Protocol#Custom_Payload) + PacketContainer container = new PacketContainer(PacketType.Play.Server.CUSTOM_PAYLOAD); + + // channel id + if (MinecraftVersion.AQUATIC_UPDATE.atOrAbove()) { + // mc 1.13: channel id is now in the format of a resource location + String[] parts = channelId.split(":", 2); + MinecraftKey key = parts.length == 1 ? new MinecraftKey(channelId) : new MinecraftKey(parts[0], parts[1]); + + container.getMinecraftKeys().write(0, key); + } else { + // mc 1.8: channel id is a string + container.getStrings().write(0, channelId); + } + + // payload + ByteBuf buffer = Unpooled.copiedBuffer(payload); + Object wrappedSerializableBuffer = MinecraftReflection.getPacketDataSerializer(buffer); + container.getModifier().withType(ByteBuf.class).write(0, wrappedSerializableBuffer); + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }; + } + + @Override + public @NotNull OutboundPacket createEntityMetaPacket( + @NotNull T value, + @NotNull EntityMetadataFactory metadata + ) { + return (player, npc) -> { + // create the entity meta + PlatformVersionAccessor versionAccessor = npc.platform().versionAccessor(); + EntityMetadata entityMetadata = metadata.create(value, versionAccessor); + + // check if the meta is available + if (!entityMetadata.available()) { + return; + } + + // construct the meta we want to send out + List watchableObjects = new ArrayList<>(); + watchableObjects.add(createWatchableObject( + entityMetadata.index(), + entityMetadata.type(), + entityMetadata.value())); + + // add ll dependant metas + for (EntityMetadataFactory relatedMetadata : metadata.relatedMetadata()) { + EntityMetadata related = relatedMetadata.create(value, versionAccessor); + if (related.available()) { + watchableObjects.add(createWatchableObject(related.index(), related.type(), related.value())); + } + } + + // EntityMetadata (https://wiki.vg/Protocol#Entity_Metadata) + PacketContainer container = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA); + + // entity id & metadata + container.getIntegers().write(0, npc.entityId()); + container.getWatchableCollectionModifier().write(0, watchableObjects); + + // send the packet without notifying any bound packet listeners + PROTOCOL_MANAGER.sendServerPacket(player, container, false); + }; + } + + @Override + public void initialize(@NotNull Platform platform) { + PROTOCOL_MANAGER.addPacketListener(new NpcUsePacketAdapter(platform)); + } + + private static final class NpcUsePacketAdapter extends PacketAdapter { + + private final Platform platform; + + public NpcUsePacketAdapter(@NotNull Platform platform) { + super(platform.extension(), ListenerPriority.HIGHEST, PacketType.Play.Client.USE_ENTITY); + this.platform = platform; + } + + @Override + public void onPacketReceiving(@NotNull PacketEvent event) { + // get the entity id of the clicked entity + Player player = event.getPlayer(); + PacketContainer packet = event.getPacket(); + int entityId = packet.getIntegers().read(0); + + // get the associated npc from the tracked entities + Npc npc = this.platform.npcTracker().npcById(entityId); + if (npc != null) { + // extract the used hand and interact action + EnumWrappers.EntityUseAction action; + EnumWrappers.Hand hand = EnumWrappers.Hand.MAIN_HAND; + + if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { + // mc 1.17: hand & action are now in an internal wrapper class + WrappedEnumEntityUseAction useAction = packet.getEnumEntityUseActions().read(0); + action = useAction.getAction(); + + // the hand is not explicitly send for attacks (always the main hand) + if (action != EnumWrappers.EntityUseAction.ATTACK) { + hand = useAction.getHand(); + } + } else { + // mc 1.8: hand & action are fields in the packet (or the hand is not even present) + action = packet.getEntityUseActions().read(0); + + // the hand is not explicitly send for attacks (always the main hand) + if (action != EnumWrappers.EntityUseAction.ATTACK && MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { + // mc 1.9: hand is now a thing + hand = packet.getHands().read(0); + } + } + + // call the event + switch (action) { + case ATTACK: + EventDispatcher.dispatch(this.platform, DefaultAttackNpcEvent.attackNpc(npc, player)); + break; + case INTERACT: + InteractNpcEvent.Hand usedHand = HAND_CONVERTER.get(hand); + EventDispatcher.dispatch(this.platform, DefaultInteractNpcEvent.interactNpc(npc, player, usedHand)); + break; + default: + // we don't handle INTERACT_AT as the client sends it alongside the interact packet (duplicate event call) + break; + } + + // don't pass the packet to the server + event.setCancelled(true); + } + } + } +} diff --git a/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/util/BukkitPlatformUtil.java b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/util/BukkitPlatformUtil.java new file mode 100644 index 0000000..d27b6f4 --- /dev/null +++ b/bukkit/src/main/java/com/github/juliarn/npclib/bukkit/util/BukkitPlatformUtil.java @@ -0,0 +1,54 @@ +/* + * This file is part of npc-lib, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 Julian M., Pasqual K. and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.github.juliarn.npclib.bukkit.util; + +import static org.bukkit.util.NumberConversions.square; + +import com.github.juliarn.npclib.api.Npc; +import com.github.juliarn.npclib.api.Position; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +public final class BukkitPlatformUtil { + + private BukkitPlatformUtil() { + throw new UnsupportedOperationException(); + } + + public static double distance(@NotNull Npc npc, @NotNull Location location) { + Position pos = npc.position(); + return square(location.getX() - pos.x()) + square(location.getY() - pos.y()) + square(location.getZ() - pos.z()); + } + + public static @NotNull Position positionFromBukkit(@NotNull Location loc) { + return Position.position( + loc.getX(), + loc.getY(), + loc.getZ(), + loc.getYaw(), + loc.getPitch(), + loc.getWorld().getName()); + } +} diff --git a/common/src/main/java/com/github/juliarn/npclib/common/CommonNpcTracker.java b/common/src/main/java/com/github/juliarn/npclib/common/CommonNpcTracker.java index 069cb15..37a9fde 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/CommonNpcTracker.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/CommonNpcTracker.java @@ -35,13 +35,17 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; -public abstract class CommonNpcTracker implements NpcTracker { +public class CommonNpcTracker implements NpcTracker { - protected final Set> trackedNpcs = Collections.synchronizedSet(new HashSet<>()); + protected final Set> trackedNpcs = Collections.synchronizedSet(new HashSet<>()); + + public static @NotNull CommonNpcTracker newNpcTracker() { + return new CommonNpcTracker<>(); + } @Override - public @Nullable Npc npcById(int entityId) { - for (Npc trackedNpc : this.trackedNpcs) { + public @Nullable Npc npcById(int entityId) { + for (Npc trackedNpc : this.trackedNpcs) { if (trackedNpc.entityId() == entityId) { return trackedNpc; } @@ -51,8 +55,8 @@ public abstract class CommonNpcTracker implements NpcTracker { } @Override - public @Nullable Npc npcByUniqueId(@NotNull UUID uniqueId) { - for (Npc trackedNpc : this.trackedNpcs) { + public @Nullable Npc npcByUniqueId(@NotNull UUID uniqueId) { + for (Npc trackedNpc : this.trackedNpcs) { if (trackedNpc.profile().uniqueId().equals(uniqueId)) { return trackedNpc; } @@ -62,17 +66,17 @@ public abstract class CommonNpcTracker implements NpcTracker { } @Override - public void trackNpc(@NotNull Npc npc) { + public void trackNpc(@NotNull Npc npc) { this.trackedNpcs.add(npc); } @Override - public void stopTrackingNpc(@NotNull Npc npc) { + public void stopTrackingNpc(@NotNull Npc npc) { this.trackedNpcs.remove(npc); } @Override - public @UnmodifiableView @NotNull Collection> trackedNpcs() { + public @UnmodifiableView @NotNull Collection> trackedNpcs() { return Collections.unmodifiableCollection(this.trackedNpcs); } } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/event/CommonNpcEvent.java b/common/src/main/java/com/github/juliarn/npclib/common/event/CommonNpcEvent.java index 93a586d..97d5a97 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/event/CommonNpcEvent.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/event/CommonNpcEvent.java @@ -30,15 +30,15 @@ public abstract class CommonNpcEvent implements NpcEvent { - protected final Npc npc; + protected final Npc npc; - public CommonNpcEvent(@NotNull Npc npc) { + public CommonNpcEvent(@NotNull Npc npc) { this.npc = npc; } @Override @SuppressWarnings("unchecked") - public @NotNull Npc npc() { - return (Npc) this.npc; + public @NotNull Npc npc() { + return (Npc) this.npc; } } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/event/CommonPlayerNpcEvent.java b/common/src/main/java/com/github/juliarn/npclib/common/event/CommonPlayerNpcEvent.java index 0eb5e47..979a931 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/event/CommonPlayerNpcEvent.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/event/CommonPlayerNpcEvent.java @@ -32,7 +32,7 @@ public abstract class CommonPlayerNpcEvent extends CommonNpcEvent implements Pla protected final Object player; - public CommonPlayerNpcEvent(@NotNull Npc npc, @NotNull Object player) { + public CommonPlayerNpcEvent(@NotNull Npc npc, @NotNull Object player) { super(npc); this.player = player; } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultAttackNpcEvent.java b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultAttackNpcEvent.java index b26483e..bbe6c24 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultAttackNpcEvent.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultAttackNpcEvent.java @@ -33,11 +33,11 @@ public final class DefaultAttackNpcEvent extends CommonPlayerNpcEvent implements private boolean cancelled = false; - private DefaultAttackNpcEvent(@NotNull Npc npc, @NotNull Object player) { + private DefaultAttackNpcEvent(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } - public static @NotNull AttackNpcEvent attackNpc(@NotNull Npc npc, @NotNull Object player) { + public static @NotNull AttackNpcEvent attackNpc(@NotNull Npc npc, @NotNull Object player) { Objects.requireNonNull(npc, "npc"); Objects.requireNonNull(player, "player"); diff --git a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultHideNpcEvent.java b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultHideNpcEvent.java index b54e16a..4ce7aa2 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultHideNpcEvent.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultHideNpcEvent.java @@ -31,18 +31,18 @@ public class DefaultHideNpcEvent extends CommonPlayerNpcEvent implements HideNpcEvent { - private DefaultHideNpcEvent(@NotNull Npc npc, @NotNull Object player) { + private DefaultHideNpcEvent(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } - public static @NotNull Pre pre(@NotNull Npc npc, @NotNull Object player) { + public static @NotNull Pre pre(@NotNull Npc npc, @NotNull Object player) { Objects.requireNonNull(npc, "npc"); Objects.requireNonNull(player, "player"); return new DefaultPre(npc, player); } - public static @NotNull Post post(@NotNull Npc npc, @NotNull Object player) { + public static @NotNull Post post(@NotNull Npc npc, @NotNull Object player) { Objects.requireNonNull(npc, "npc"); Objects.requireNonNull(player, "player"); @@ -53,7 +53,7 @@ private static final class DefaultPre extends DefaultHideNpcEvent implements Hid private boolean cancelled = false; - private DefaultPre(@NotNull Npc npc, @NotNull Object player) { + private DefaultPre(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } @@ -70,7 +70,7 @@ public void cancelled(boolean cancelled) { private static final class DefaultPost extends DefaultHideNpcEvent implements HideNpcEvent.Post { - private DefaultPost(@NotNull Npc npc, @NotNull Object player) { + private DefaultPost(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultInteractNpcEvent.java b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultInteractNpcEvent.java index 9fd85bc..eefc61a 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultInteractNpcEvent.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultInteractNpcEvent.java @@ -34,13 +34,13 @@ public final class DefaultInteractNpcEvent extends CommonPlayerNpcEvent implemen private final Hand hand; private boolean cancelled = false; - private DefaultInteractNpcEvent(@NotNull Npc npc, @NotNull Object player, @NotNull Hand hand) { + private DefaultInteractNpcEvent(@NotNull Npc npc, @NotNull Object player, @NotNull Hand hand) { super(npc, player); this.hand = hand; } public static @NotNull InteractNpcEvent interactNpc( - @NotNull Npc npc, + @NotNull Npc npc, @NotNull Object player, @NotNull Hand hand ) { diff --git a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultShowNpcEvent.java b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultShowNpcEvent.java index 40d41b6..d30ecfd 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultShowNpcEvent.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/event/DefaultShowNpcEvent.java @@ -31,18 +31,18 @@ public class DefaultShowNpcEvent extends CommonPlayerNpcEvent implements ShowNpcEvent { - private DefaultShowNpcEvent(@NotNull Npc npc, @NotNull Object player) { + private DefaultShowNpcEvent(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } - public static @NotNull Pre pre(@NotNull Npc npc, @NotNull Object player) { + public static @NotNull Pre pre(@NotNull Npc npc, @NotNull Object player) { Objects.requireNonNull(npc, "npc"); Objects.requireNonNull(player, "player"); return new DefaultPre(npc, player); } - public static @NotNull Post post(@NotNull Npc npc, @NotNull Object player) { + public static @NotNull Post post(@NotNull Npc npc, @NotNull Object player) { Objects.requireNonNull(npc, "npc"); Objects.requireNonNull(player, "player"); @@ -53,7 +53,7 @@ private static final class DefaultPre extends DefaultShowNpcEvent implements Sho private boolean cancelled = false; - private DefaultPre(@NotNull Npc npc, @NotNull Object player) { + private DefaultPre(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } @@ -70,7 +70,7 @@ public void cancelled(boolean cancelled) { private static final class DefaultPost extends DefaultShowNpcEvent implements ShowNpcEvent.Post { - private DefaultPost(@NotNull Npc npc, @NotNull Object player) { + private DefaultPost(@NotNull Npc npc, @NotNull Object player) { super(npc, player); } } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpc.java b/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpc.java index 394e6bc..a60c0d9 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpc.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpc.java @@ -30,7 +30,9 @@ import com.github.juliarn.npclib.api.Position; import com.github.juliarn.npclib.api.flag.NpcFlag; import com.github.juliarn.npclib.api.profile.Profile; -import com.github.juliarn.npclib.api.protocol.PlayerInfoAction; +import com.github.juliarn.npclib.api.protocol.NpcSpecificOutboundPacket; +import com.github.juliarn.npclib.api.protocol.enums.ItemSlot; +import com.github.juliarn.npclib.api.protocol.enums.PlayerInfoAction; import com.github.juliarn.npclib.api.settings.NpcSettings; import com.github.juliarn.npclib.common.event.DefaultHideNpcEvent; import com.github.juliarn.npclib.common.event.DefaultShowNpcEvent; @@ -45,7 +47,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; -public class CommonNpc extends CommonNpcFlaggedObject implements Npc { +public class CommonNpc extends CommonNpcFlaggedObject implements Npc { protected final int entityId; protected final Profile.Resolved profile; @@ -53,7 +55,7 @@ public class CommonNpc extends CommonNpcFlaggedObject implements Npc platform; + protected final Platform platform; protected final NpcSettings

npcSettings; protected final Set

trackedPlayers = Collections.synchronizedSet(new HashSet<>()); @@ -65,7 +67,7 @@ public CommonNpc( @NotNull Profile.Resolved profile, @NotNull W world, @NotNull Position pos, - @NotNull Platform platform, + @NotNull Platform platform, @NotNull NpcSettings

npcSettings ) { super(flags); @@ -103,12 +105,12 @@ public int entityId() { } @Override - public @NotNull Platform platform() { + public @NotNull Platform platform() { return this.platform; } @Override - public @NotNull NpcTracker npcTracker() { + public @NotNull NpcTracker npcTracker() { return this.platform.npcTracker(); } @@ -123,13 +125,18 @@ public boolean shouldIncludePlayer(@NotNull P player) { } @Override - public @NotNull Npc addIncludedPlayer(@NotNull P player) { + public boolean includesPlayer(@NotNull P player) { + return this.includedPlayers.contains(player); + } + + @Override + public @NotNull Npc addIncludedPlayer(@NotNull P player) { this.includedPlayers.add(player); return this; } @Override - public @NotNull Npc removeIncludedPlayer(@NotNull P player) { + public @NotNull Npc removeIncludedPlayer(@NotNull P player) { this.includedPlayers.remove(player); return this; } @@ -140,7 +147,12 @@ public boolean shouldIncludePlayer(@NotNull P player) { } @Override - public @NotNull Npc trackPlayer(@NotNull P player) { + public boolean tracksPlayer(@NotNull P player) { + return this.trackedPlayers.contains(player); + } + + @Override + public @NotNull Npc trackPlayer(@NotNull P player) { // check if we should track the player if (this.shouldIncludePlayer(player)) { return this.forceTrackPlayer(player); @@ -151,7 +163,7 @@ public boolean shouldIncludePlayer(@NotNull P player) { } @Override - public @NotNull Npc forceTrackPlayer(@NotNull P player) { + public @NotNull Npc forceTrackPlayer(@NotNull P player) { // check if the player is not already tracked if (this.trackedPlayers.add(player)) { // break early if the add is not wanted by plugin @@ -177,7 +189,7 @@ public boolean shouldIncludePlayer(@NotNull P player) { } @Override - public @NotNull Npc stopTrackingPlayer(@NotNull P player) { + public @NotNull Npc stopTrackingPlayer(@NotNull P player) { // check if the player was previously tracked if (this.trackedPlayers.remove(player)) { // break early if the removal is not wanted by plugin @@ -196,4 +208,30 @@ public boolean shouldIncludePlayer(@NotNull P player) { // for chaining return this; } + + @Override + public @NotNull NpcSpecificOutboundPacket lookAt(@NotNull Position position) { + double diffX = position.x() - this.pos.x(); + double diffY = position.y() - this.pos.y(); + double diffZ = position.z() - this.pos.z(); + + double distanceXZ = Math.sqrt(diffX * diffX + diffZ * diffZ); + double distanceY = Math.sqrt(distanceXZ * distanceXZ + diffY * diffY); + + double yaw = Math.toDegrees(Math.acos(diffX / distanceXZ)); + double pitch = Math.toDegrees(Math.acos(diffY / distanceY)) - 90; + + // correct yaw according to difference + if (diffZ < 0) { + yaw += Math.abs(180 - yaw) * 2; + } + yaw -= 90; + + return this.platform.packetFactory().createRotationPacket((float) yaw, (float) pitch).toSpecific(this); + } + + @Override + public @NotNull NpcSpecificOutboundPacket changeItem(@NotNull ItemSlot slot, @NotNull I item) { + return this.platform.packetFactory().createEquipmentPacket(slot, item).toSpecific(this); + } } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpcBuilder.java b/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpcBuilder.java index 6c5b9ce..28ef666 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpcBuilder.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/npc/CommonNpcBuilder.java @@ -39,11 +39,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class CommonNpcBuilder - extends CommonNpcFlaggedBuilder> - implements Npc.Builder { +public class CommonNpcBuilder + extends CommonNpcFlaggedBuilder> + implements Npc.Builder { - protected final Platform platform; + protected final Platform platform; protected int entityId = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); @@ -53,12 +53,12 @@ public class CommonNpcBuilder protected Profile.Resolved profile; protected NpcSettings

npcSettings; - public CommonNpcBuilder(@NotNull Platform platform) { + public CommonNpcBuilder(@NotNull Platform platform) { this.platform = platform; } @Override - public @NotNull Npc.Builder entityId(int id) { + public @NotNull Npc.Builder entityId(int id) { // validate the npc entity id if (id < 0) { throw new IllegalArgumentException("NPC entity id must be positive"); @@ -69,7 +69,7 @@ public CommonNpcBuilder(@NotNull Platform platform) { } @Override - public @NotNull Npc.Builder position(@NotNull Position position) { + public @NotNull Npc.Builder position(@NotNull Position position) { Objects.requireNonNull(position, "position"); // try to resolve the world from the given position @@ -86,13 +86,13 @@ public CommonNpcBuilder(@NotNull Platform platform) { } @Override - public @NotNull Npc.Builder profile(@NotNull Profile.Resolved profile) { + public @NotNull Npc.Builder profile(@NotNull Profile.Resolved profile) { this.profile = Objects.requireNonNull(profile, "profile"); return this; } @Override - public @NotNull CompletableFuture> profile( + public @NotNull CompletableFuture> profile( @Nullable ProfileResolver resolver, @NotNull Profile profile ) { @@ -106,7 +106,7 @@ public CommonNpcBuilder(@NotNull Platform platform) { } @Override - public @NotNull Npc.Builder npcSettings(@NotNull Consumer> decorator) { + public @NotNull Npc.Builder npcSettings(@NotNull Consumer> decorator) { // build the npc settings NpcSettings.Builder

builder = new CommonNpcSettingsBuilder<>(); decorator.accept(builder); @@ -116,7 +116,7 @@ public CommonNpcBuilder(@NotNull Platform platform) { } @Override - public @NotNull Npc build() { + public @NotNull Npc build() { // fill in empty npc settings if not given if (this.npcSettings == null) { this.npcSettings(builder -> { @@ -134,8 +134,8 @@ public CommonNpcBuilder(@NotNull Platform platform) { } @Override - public @NotNull Npc buildAndTrack() { - Npc npc = this.build(); + public @NotNull Npc buildAndTrack() { + Npc npc = this.build(); this.platform.npcTracker().trackNpc(npc); return npc; diff --git a/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatform.java b/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatform.java index b5c09be..97cd229 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatform.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatform.java @@ -29,6 +29,7 @@ import com.github.juliarn.npclib.api.NpcTracker; import com.github.juliarn.npclib.api.Platform; import com.github.juliarn.npclib.api.PlatformTaskManager; +import com.github.juliarn.npclib.api.PlatformVersionAccessor; import com.github.juliarn.npclib.api.PlatformWorldAccessor; import com.github.juliarn.npclib.api.event.NpcEvent; import com.github.juliarn.npclib.api.profile.ProfileResolver; @@ -39,35 +40,44 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class CommonPlatform implements Platform { +public class CommonPlatform implements Platform { + protected final E extension; protected final boolean debug; protected final EventBus eventBus; - protected final NpcTracker npcTracker; + protected final NpcTracker npcTracker; protected final ProfileResolver profileResolver; protected final PlatformTaskManager taskManager; protected final NpcActionController actionController; + protected final PlatformVersionAccessor versionAccessor; protected final PlatformWorldAccessor worldAccessor; - protected final PlatformPacketAdapter packetAdapter; + protected final PlatformPacketAdapter packetAdapter; public CommonPlatform( boolean debug, - @NotNull NpcTracker npcTracker, + @NotNull E extension, + @NotNull NpcTracker npcTracker, @NotNull ProfileResolver profileResolver, @NotNull PlatformTaskManager taskManager, @Nullable NpcActionController actionController, + @NotNull PlatformVersionAccessor versionAccessor, @NotNull EventBus eventBus, @NotNull PlatformWorldAccessor worldAccessor, - @NotNull PlatformPacketAdapter packetAdapter + @NotNull PlatformPacketAdapter packetAdapter ) { this.debug = debug; + this.extension = extension; this.npcTracker = npcTracker; this.profileResolver = profileResolver; this.taskManager = taskManager; this.actionController = actionController; + this.versionAccessor = versionAccessor; this.eventBus = eventBus; this.worldAccessor = worldAccessor; this.packetAdapter = packetAdapter; + + // register the packet listeners + this.packetAdapter.initialize(this); } @Override @@ -76,7 +86,12 @@ public boolean debug() { } @Override - public @NotNull NpcTracker npcTracker() { + public @NotNull E extension() { + return this.extension; + } + + @Override + public @NotNull NpcTracker npcTracker() { return this.npcTracker; } @@ -91,10 +106,15 @@ public boolean debug() { } @Override - public @NotNull Npc.Builder newNpcBuilder() { + public @NotNull Npc.Builder newNpcBuilder() { return new CommonNpcBuilder<>(this); } + @Override + public @NotNull PlatformVersionAccessor versionAccessor() { + return this.versionAccessor; + } + @Override public @NotNull EventBus eventBus() { return this.eventBus; @@ -106,7 +126,7 @@ public boolean debug() { } @Override - public @NotNull PlatformPacketAdapter packetFactory() { + public @NotNull PlatformPacketAdapter packetFactory() { return this.packetAdapter; } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatformBuilder.java b/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatformBuilder.java index 8107f05..cdf9c79 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatformBuilder.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/platform/CommonPlatformBuilder.java @@ -28,72 +28,103 @@ import com.github.juliarn.npclib.api.NpcTracker; import com.github.juliarn.npclib.api.Platform; import com.github.juliarn.npclib.api.PlatformTaskManager; +import com.github.juliarn.npclib.api.PlatformVersionAccessor; import com.github.juliarn.npclib.api.PlatformWorldAccessor; import com.github.juliarn.npclib.api.event.NpcEvent; import com.github.juliarn.npclib.api.profile.ProfileResolver; import com.github.juliarn.npclib.api.protocol.PlatformPacketAdapter; +import com.github.juliarn.npclib.common.CommonNpcTracker; import java.util.Objects; +import java.util.function.Consumer; import net.kyori.event.EventBus; import org.jetbrains.annotations.NotNull; -public abstract class CommonPlatformBuilder implements Platform.Builder { +public abstract class CommonPlatformBuilder implements Platform.Builder { protected static final boolean DEFAULT_DEBUG = Boolean.getBoolean("npc.lib.debug"); protected static final ProfileResolver DEFAULT_PROFILE_RESOLVER = ProfileResolver.caching(ProfileResolver.mojang()); + protected E extension; protected boolean debug = DEFAULT_DEBUG; protected EventBus eventBus; - protected NpcTracker npcTracker; + protected NpcTracker npcTracker; protected ProfileResolver profileResolver; protected PlatformTaskManager taskManager; - protected NpcActionController actionController; + protected PlatformVersionAccessor versionAccessor; protected PlatformWorldAccessor worldAccessor; - protected PlatformPacketAdapter packetAdapter; + protected PlatformPacketAdapter packetAdapter; + protected Consumer actionControllerDecorator; @Override - public Platform.@NotNull Builder debug(boolean debug) { + public @NotNull Platform.Builder debug(boolean debug) { this.debug = debug; return this; } @Override - public @NotNull Platform.Builder eventBus(@NotNull EventBus eventBus) { + public @NotNull Platform.Builder extension(@NotNull E extension) { + this.extension = Objects.requireNonNull(extension, "extension"); + return this; + } + + @Override + public @NotNull Platform.Builder eventBus(@NotNull EventBus eventBus) { this.eventBus = Objects.requireNonNull(eventBus, "eventBus"); return this; } @Override - public @NotNull Platform.Builder npcTracker(@NotNull NpcTracker npcTracker) { + public @NotNull Platform.Builder npcTracker(@NotNull NpcTracker npcTracker) { this.npcTracker = Objects.requireNonNull(npcTracker, "npcTracker"); return this; } @Override - public Platform.@NotNull Builder taskManager(@NotNull PlatformTaskManager taskManager) { + public @NotNull Platform.Builder taskManager(@NotNull PlatformTaskManager taskManager) { this.taskManager = Objects.requireNonNull(taskManager, "taskManager"); return this; } @Override - public @NotNull Platform.Builder profileResolver(@NotNull ProfileResolver profileResolver) { + public @NotNull Platform.Builder profileResolver(@NotNull ProfileResolver profileResolver) { this.profileResolver = Objects.requireNonNull(profileResolver, "profileResolver"); return this; } @Override - public @NotNull Platform.Builder worldAccessor(@NotNull PlatformWorldAccessor worldAccessor) { + public @NotNull Platform.Builder worldAccessor(@NotNull PlatformWorldAccessor worldAccessor) { this.worldAccessor = Objects.requireNonNull(worldAccessor, "worldAccessor"); return this; } @Override - public @NotNull Platform.Builder packetFactory(@NotNull PlatformPacketAdapter packetFactory) { + public @NotNull Platform.Builder versionAccessor(@NotNull PlatformVersionAccessor versionAccessor) { + this.versionAccessor = Objects.requireNonNull(versionAccessor, "versionAccessor"); + return this; + } + + @Override + public @NotNull Platform.Builder packetFactory(@NotNull PlatformPacketAdapter packetFactory) { this.packetAdapter = Objects.requireNonNull(packetFactory, "packetFactory"); return this; } @Override - public @NotNull Platform build() { + public @NotNull CommonPlatformBuilder actionController( + @NotNull Consumer decorator + ) { + this.actionControllerDecorator = Objects.requireNonNull(decorator, "decorator"); + return this; + } + + @Override + public @NotNull Platform build() { + // validate that the required values are present + Objects.requireNonNull(this.extension, "extension"); + + // let the downstream builder set all default values if required + this.prepareBuild(); + // use the default profile resolver if no specific one was specified if (this.profileResolver == null) { this.profileResolver = DEFAULT_PROFILE_RESOLVER; @@ -104,14 +135,15 @@ public abstract class CommonPlatformBuilder implements Platform.Builder this.eventBus = EventBus.create(NpcEvent.class); } - return new CommonPlatform<>( - this.debug, - Objects.requireNonNull(this.npcTracker, "npcTracker"), - Objects.requireNonNull(this.profileResolver, "profileResolver"), - Objects.requireNonNull(this.taskManager, "taskManager"), - Objects.requireNonNull(this.actionController, "actionController"), - Objects.requireNonNull(this.eventBus, "eventBus"), - Objects.requireNonNull(this.worldAccessor, "worldAccessor"), - Objects.requireNonNull(this.packetAdapter, "packetAdapter")); + // use a new npc tracker if none is given + if (this.npcTracker == null) { + this.npcTracker = CommonNpcTracker.newNpcTracker(); + } + + return this.doBuild(); } + + protected abstract void prepareBuild(); + + protected abstract @NotNull Platform doBuild(); } diff --git a/common/src/main/java/com/github/juliarn/npclib/common/util/EventDispatcher.java b/common/src/main/java/com/github/juliarn/npclib/common/util/EventDispatcher.java index 3e3463f..cee230a 100644 --- a/common/src/main/java/com/github/juliarn/npclib/common/util/EventDispatcher.java +++ b/common/src/main/java/com/github/juliarn/npclib/common/util/EventDispatcher.java @@ -38,9 +38,9 @@ private EventDispatcher() { throw new UnsupportedOperationException(); } - public static @NotNull E dispatch( - @NotNull Platform platform, - @NotNull E event + public static @NotNull N dispatch( + @NotNull Platform platform, + @NotNull N event ) { // post the event PostResult result = platform.eventBus().post(event); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe9410e..b888e45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ licenser = "0.6.1" # general gson = "2.9.0" +netty = "4.1.77.Final" annotations = "23.0.0" event = "5.0.0-SNAPSHOT" @@ -16,29 +17,31 @@ event = "5.0.0-SNAPSHOT" sponge = "9.0.0" nukkitX = "1.0-SNAPSHOT" minestom = "master-SNAPSHOT" -spigot = "1.8.8-R0.1-SNAPSHOT" +paper = "1.19-R0.1-SNAPSHOT" # platform extensions paperLib = "1.0.7" +packetEvents = "2.0-SNAPSHOT" protocolLib = "master-SNAPSHOT" - [libraries] # general event = { group = "net.kyori", name = "event-api", version.ref = "event" } +netty = { group = "io.netty", name = "netty-buffer", version.ref = "netty" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } # platform api nukkitX = { group = "cn.nukkit", name = "nukkit", version.ref = "nukkitX" } -spigot = { group = "org.spigotmc", name = "spigot-api", version.ref = "spigot" } +paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } sponge = { group = "org.spongepowered", name = "spongeapi", version.ref = "sponge" } minestom = { group = "com.github.Minestom", name = "Minestom", version.ref = "minestom" } # platform extensions paperLib = { group = "io.papermc", name = "paperlib", version.ref = "paperLib" } protocolLib = { group = "com.github.dmulloy2", name = "ProtocolLib", version.ref = "protocolLib" } +packetEvents = { group = "com.github.retrooper.packetevents", name = "spigot", version.ref = "packetEvents" } [plugins] diff --git a/settings.gradle.kts b/settings.gradle.kts index dcc5ceb..e69acec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,4 +25,4 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "npc-lib2" -include(":api", ":common") +include(":api", ":common", ":bukkit")