From ffc4c52c9ad3dc802ac938bef2bffc094c87c130 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 3 Mar 2025 21:57:54 +0100 Subject: [PATCH] Add libs --- build.gradle.kts | 9 + gradle.properties | 3 + gradle/wrapper/gradle-wrapper.properties | 2 +- libraries/common/.gitignore | 122 +++ libraries/common/README.md | 13 + libraries/common/build.gradle.kts | 91 ++ .../java/de/oliver/fancylib/ConfigHelper.java | 16 + .../java/de/oliver/fancylib/FancyLib.java | 59 ++ .../java/de/oliver/fancylib/FileUtils.java | 111 +++ .../de/oliver/fancylib/MessageHelper.java | 129 +++ .../main/java/de/oliver/fancylib/Metrics.java | 881 ++++++++++++++++++ .../java/de/oliver/fancylib/RandomUtils.java | 16 + .../de/oliver/fancylib/ReflectionUtils.java | 54 ++ .../java/de/oliver/fancylib/UUIDFetcher.java | 154 +++ .../de/oliver/fancylib/VersionConfig.java | 111 +++ .../oliver/fancylib/databases/Database.java | 45 + .../fancylib/databases/MySqlDatabase.java | 88 ++ .../fancylib/databases/SqliteDatabase.java | 41 + .../fancylib/featureFlags/FeatureFlag.java | 38 + .../featureFlags/FeatureFlagConfig.java | 79 ++ .../CustomPlayerInventory.java | 47 + .../gui/customInventories/PageInventory.java | 43 + .../InventoryClickListener.java | 28 + .../InventoryClickRegistry.java | 19 + .../inventoryClick/InventoryItemClick.java | 46 + .../impl/CancelInventoryItemClick.java | 23 + .../impl/ChangePageInventoryItemClick.java | 47 + .../oliver/fancylib/itemClick/ItemClick.java | 30 + .../fancylib/itemClick/ItemClickRegistry.java | 18 + .../itemClick/PlayerInteractListener.java | 25 + .../main/java/de/oliver/fancylib/jdb/JDB.java | 137 +++ .../de/oliver/fancylib/jdb/JDocument.java | 152 +++ .../serverSoftware/ServerSoftware.java | 46 + .../schedulers/BukkitScheduler.java | 61 ++ .../schedulers/FancyScheduler.java | 69 ++ .../schedulers/FoliaScheduler.java | 71 ++ .../de/oliver/fancylib/tests/Expectable.java | 243 +++++ .../de/oliver/fancylib/tests/FPTestClass.java | 147 +++ .../tests/annotations/FPAfterEach.java | 16 + .../tests/annotations/FPBeforeEach.java | 16 + .../fancylib/tests/annotations/FPTest.java | 31 + .../fancylib/translations/Language.java | 35 + .../fancylib/translations/TextConfig.java | 12 + .../fancylib/translations/Translator.java | 135 +++ .../translations/message/Message.java | 129 +++ .../translations/message/MultiMessage.java | 120 +++ .../translations/message/SimpleMessage.java | 97 ++ .../versionFetcher/HangarVersionFetcher.java | 32 + .../versionFetcher/MasterVersionFetcher.java | 36 + .../ModrinthVersionFetcher.java | 48 + .../ReposiliteVersionFetcher.java | 39 + .../versionFetcher/VersionFetcher.java | 31 + .../java/de/oliver/fancylib/jdb/JDBTest.java | 232 +++++ .../de/oliver/fancylib/jdb/JDocumentTest.java | 619 ++++++++++++ libraries/packets/.gitignore | 46 + libraries/packets/README.md | 58 ++ libraries/packets/api/build.gradle.kts | 25 + .../oliver/fancysitula/api/IFancySitula.java | 7 + .../api/entities/FS_BlockDisplay.java | 31 + .../fancysitula/api/entities/FS_Display.java | 214 +++++ .../fancysitula/api/entities/FS_Entity.java | 246 +++++ .../api/entities/FS_ItemDisplay.java | 31 + .../fancysitula/api/entities/FS_Player.java | 31 + .../api/entities/FS_RealPlayer.java | 40 + .../api/entities/FS_TextDisplay.java | 123 +++ .../FS_ClientboundAddEntityPacket.java | 166 ++++ ...S_ClientboundCreateOrUpdateTeamPacket.java | 324 +++++++ .../api/packets/FS_ClientboundPacket.java | 31 + .../FS_ClientboundPlayerInfoRemovePacket.java | 24 + .../FS_ClientboundPlayerInfoUpdatePacket.java | 59 ++ .../FS_ClientboundRemoveEntitiesPacket.java | 26 + .../FS_ClientboundRotateHeadPacket.java | 28 + .../FS_ClientboundSetEntityDataPacket.java | 67 ++ .../FS_ClientboundSetEquipmentPacket.java | 33 + .../FS_ClientboundSetPassengersPacket.java | 30 + .../FS_ClientboundTeleportEntityPacket.java | 78 ++ .../fancysitula/api/packets/FS_Color.java | 31 + .../api/teams/FS_CollisionRule.java | 18 + .../api/teams/FS_NameTagVisibility.java | 18 + .../oliver/fancysitula/api/teams/FS_Team.java | 113 +++ .../fancysitula/api/utils/AngelConverter.java | 9 + .../api/utils/FS_EquipmentSlot.java | 24 + .../fancysitula/api/utils/FS_GameProfile.java | 62 ++ .../fancysitula/api/utils/FS_GameType.java | 24 + .../fancysitula/api/utils/ServerVersion.java | 70 ++ .../utils/entityData/FS_BlockDisplayData.java | 12 + .../api/utils/entityData/FS_DisplayData.java | 82 ++ .../api/utils/entityData/FS_EntityData.java | 45 + .../utils/entityData/FS_ItemDisplayData.java | 12 + .../utils/entityData/FS_TextDisplayData.java | 31 + .../utils/reflections/ReflectionUtils.java | 72 ++ libraries/packets/build.gradle.kts | 87 ++ libraries/packets/factories/build.gradle.kts | 28 + .../fancysitula/factories/EntityFactory.java | 72 ++ .../fancysitula/factories/FancySitula.java | 8 + .../fancysitula/factories/PacketFactory.java | 549 +++++++++++ .../fancysitula/factories/TeamFactory.java | 77 ++ libraries/packets/fancysitula_title.png | Bin 0 -> 176618 bytes libraries/packets/gradle.properties | 2 + .../implementations/1_20_6/build.gradle.kts | 23 + .../ClientboundAddEntityPacketImpl.java | 48 + ...ientboundCreateOrUpdateTeamPacketImpl.java | 128 +++ ...ClientboundPlayerInfoRemovePacketImpl.java | 30 + ...ClientboundPlayerInfoUpdatePacketImpl.java | 52 ++ .../ClientboundRemoveEntitiesPacketImpl.java | 37 + .../ClientboundRotateHeadPacketImpl.java | 38 + .../ClientboundSetEntityDataPacketImpl.java | 67 ++ .../ClientboundSetEquipmentPacketImpl.java | 45 + .../ClientboundSetPassengersPacketImpl.java | 45 + .../ClientboundTeleportEntityPacketImpl.java | 43 + .../v1_20_6/utils/GameProfileImpl.java | 33 + .../v1_20_6/utils/VanillaPlayerAdapter.java | 12 + .../ClientboundAddEntityPacketImplTest.java | 64 ++ ...ntboundPlayerInfoRemovePacketImplTest.java | 22 + ...ntboundPlayerInfoUpdatePacketImplTest.java | 61 ++ ...ientboundRemoveEntitiesPacketImplTest.java | 20 + .../ClientboundRotateHeadPacketImplTest.java | 21 + ...lientboundSetEntityDataPacketImplTest.java | 28 + ...ClientboundSetEquipmentPacketImplTest.java | 28 + ...lientboundSetPassengersPacketImplTest.java | 28 + ...ientboundTeleportEntityPacketImplTest.java | 31 + .../implementations/1_21/build.gradle.kts | 24 + .../ClientboundAddEntityPacketImplTest.java | 65 ++ ...ntboundPlayerInfoRemovePacketImplTest.java | 23 + ...ntboundPlayerInfoUpdatePacketImplTest.java | 62 ++ ...ientboundRemoveEntitiesPacketImplTest.java | 21 + .../ClientboundRotateHeadPacketImplTest.java | 22 + ...lientboundSetEntityDataPacketImplTest.java | 29 + ...ClientboundSetEquipmentPacketImplTest.java | 29 + ...ientboundTeleportEntityPacketImplTest.java | 32 + .../implementations/1_21_1/build.gradle.kts | 24 + .../ClientboundAddEntityPacketImplTest.java | 65 ++ ...ntboundPlayerInfoRemovePacketImplTest.java | 23 + ...ntboundPlayerInfoUpdatePacketImplTest.java | 62 ++ ...ientboundRemoveEntitiesPacketImplTest.java | 21 + .../ClientboundRotateHeadPacketImplTest.java | 22 + ...lientboundSetEntityDataPacketImplTest.java | 29 + ...ClientboundSetEquipmentPacketImplTest.java | 29 + ...ientboundTeleportEntityPacketImplTest.java | 32 + .../implementations/1_21_3/build.gradle.kts | 23 + .../ClientboundAddEntityPacketImpl.java | 48 + ...ientboundCreateOrUpdateTeamPacketImpl.java | 128 +++ ...ClientboundPlayerInfoRemovePacketImpl.java | 30 + ...ClientboundPlayerInfoUpdatePacketImpl.java | 53 ++ .../ClientboundRemoveEntitiesPacketImpl.java | 37 + .../ClientboundRotateHeadPacketImpl.java | 38 + .../ClientboundSetEntityDataPacketImpl.java | 67 ++ .../ClientboundSetEquipmentPacketImpl.java | 45 + .../ClientboundSetPassengersPacketImpl.java | 45 + .../ClientboundTeleportEntityPacketImpl.java | 43 + .../v1_21_3/utils/GameProfileImpl.java | 33 + .../v1_21_3/utils/VanillaPlayerAdapter.java | 12 + .../ClientboundAddEntityPacketImplTest.java | 65 ++ ...ntboundPlayerInfoRemovePacketImplTest.java | 23 + ...ntboundPlayerInfoUpdatePacketImplTest.java | 62 ++ ...ientboundRemoveEntitiesPacketImplTest.java | 21 + .../ClientboundRotateHeadPacketImplTest.java | 22 + ...lientboundSetEntityDataPacketImplTest.java | 29 + ...ClientboundSetEquipmentPacketImplTest.java | 29 + ...lientboundSetPassengersPacketImplTest.java | 29 + ...ientboundTeleportEntityPacketImplTest.java | 32 + .../implementations/1_21_4/build.gradle.kts | 23 + .../ClientboundAddEntityPacketImpl.java | 48 + ...ientboundCreateOrUpdateTeamPacketImpl.java | 128 +++ ...ClientboundPlayerInfoRemovePacketImpl.java | 30 + ...ClientboundPlayerInfoUpdatePacketImpl.java | 54 ++ .../ClientboundRemoveEntitiesPacketImpl.java | 37 + .../ClientboundRotateHeadPacketImpl.java | 38 + .../ClientboundSetEntityDataPacketImpl.java | 67 ++ .../ClientboundSetEquipmentPacketImpl.java | 45 + .../ClientboundSetPassengersPacketImpl.java | 45 + .../ClientboundTeleportEntityPacketImpl.java | 43 + .../v1_21_4/utils/GameProfileImpl.java | 33 + .../v1_21_4/utils/VanillaPlayerAdapter.java | 12 + .../ClientboundAddEntityPacketImplTest.java | 65 ++ ...ntboundPlayerInfoRemovePacketImplTest.java | 23 + ...ntboundPlayerInfoUpdatePacketImplTest.java | 62 ++ ...ientboundRemoveEntitiesPacketImplTest.java | 21 + .../ClientboundRotateHeadPacketImplTest.java | 22 + ...lientboundSetEntityDataPacketImplTest.java | 29 + ...ClientboundSetEquipmentPacketImplTest.java | 29 + ...lientboundSetPassengersPacketImplTest.java | 29 + ...ientboundTeleportEntityPacketImplTest.java | 32 + .../packets/implementations/build.gradle.kts | 3 + libraries/packets/settings.gradle.kts | 12 + .../packets/test_plugin/build.gradle.kts | 66 ++ .../oliver/fancysitula/FancySitulaPlugin.java | 16 + .../fancysitula/commands/FancySitulaCMD.java | 118 +++ .../FancySitulaPluginBootstrapper.java | 19 + .../loaders/FancySitulaPluginLoader.java | 12 + plugins/fancyholograms/api/build.gradle.kts | 2 +- plugins/fancyholograms/build.gradle.kts | 12 +- .../implementation_1_19_4/build.gradle.kts | 2 +- .../implementation_1_20_1/build.gradle.kts | 2 +- .../implementation_1_20_2/build.gradle.kts | 2 +- .../implementation_1_20_4/build.gradle.kts | 2 +- settings.gradle.kts | 14 +- 197 files changed, 11722 insertions(+), 13 deletions(-) create mode 100644 gradle.properties create mode 100644 libraries/common/.gitignore create mode 100644 libraries/common/README.md create mode 100644 libraries/common/build.gradle.kts create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/ConfigHelper.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/FancyLib.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/FileUtils.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/MessageHelper.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/Metrics.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/RandomUtils.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/ReflectionUtils.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/UUIDFetcher.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/VersionConfig.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/databases/Database.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/databases/MySqlDatabase.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/databases/SqliteDatabase.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlag.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlagConfig.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/CustomPlayerInventory.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/PageInventory.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickListener.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickRegistry.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryItemClick.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/CancelInventoryItemClick.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/ChangePageInventoryItemClick.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClick.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClickRegistry.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/itemClick/PlayerInteractListener.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/jdb/JDB.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/jdb/JDocument.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/ServerSoftware.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/BukkitScheduler.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FancyScheduler.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FoliaScheduler.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/tests/Expectable.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/tests/FPTestClass.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPAfterEach.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPBeforeEach.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPTest.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/translations/Language.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/translations/TextConfig.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/translations/Translator.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/translations/message/Message.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/translations/message/MultiMessage.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/translations/message/SimpleMessage.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/HangarVersionFetcher.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/MasterVersionFetcher.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ModrinthVersionFetcher.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ReposiliteVersionFetcher.java create mode 100644 libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/VersionFetcher.java create mode 100644 libraries/common/src/test/java/de/oliver/fancylib/jdb/JDBTest.java create mode 100644 libraries/common/src/test/java/de/oliver/fancylib/jdb/JDocumentTest.java create mode 100644 libraries/packets/.gitignore create mode 100644 libraries/packets/README.md create mode 100644 libraries/packets/api/build.gradle.kts create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/IFancySitula.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_BlockDisplay.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Display.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Entity.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_ItemDisplay.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Player.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_RealPlayer.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_TextDisplay.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundAddEntityPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundCreateOrUpdateTeamPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoRemovePacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoUpdatePacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRemoveEntitiesPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRotateHeadPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEntityDataPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEquipmentPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetPassengersPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundTeleportEntityPacket.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_Color.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_CollisionRule.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_NameTagVisibility.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_Team.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/AngelConverter.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_EquipmentSlot.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameProfile.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameType.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/ServerVersion.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_BlockDisplayData.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_DisplayData.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_EntityData.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_ItemDisplayData.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_TextDisplayData.java create mode 100644 libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/reflections/ReflectionUtils.java create mode 100644 libraries/packets/build.gradle.kts create mode 100644 libraries/packets/factories/build.gradle.kts create mode 100644 libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/EntityFactory.java create mode 100644 libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/FancySitula.java create mode 100644 libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/PacketFactory.java create mode 100644 libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/TeamFactory.java create mode 100644 libraries/packets/fancysitula_title.png create mode 100644 libraries/packets/gradle.properties create mode 100644 libraries/packets/implementations/1_20_6/build.gradle.kts create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundCreateOrUpdateTeamPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/GameProfileImpl.java create mode 100644 libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/VanillaPlayerAdapter.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImplTest.java create mode 100644 libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/build.gradle.kts create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundAddEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/build.gradle.kts create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundAddEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/build.gradle.kts create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundCreateOrUpdateTeamPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/GameProfileImpl.java create mode 100644 libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/VanillaPlayerAdapter.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/build.gradle.kts create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundCreateOrUpdateTeamPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/GameProfileImpl.java create mode 100644 libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/VanillaPlayerAdapter.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImplTest.java create mode 100644 libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImplTest.java create mode 100644 libraries/packets/implementations/build.gradle.kts create mode 100644 libraries/packets/settings.gradle.kts create mode 100644 libraries/packets/test_plugin/build.gradle.kts create mode 100644 libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/FancySitulaPlugin.java create mode 100644 libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/commands/FancySitulaCMD.java create mode 100644 libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginBootstrapper.java create mode 100644 libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginLoader.java diff --git a/build.gradle.kts b/build.gradle.kts index 3f323e83..34898d0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,12 @@ +plugins { + id("com.gradleup.shadow") version "9.0.0-beta9" apply false + id("io.papermc.paperweight.userdev") version "2.0.0-beta.14" apply false + id("xyz.jpenilla.run-paper") version "2.3.1" apply false + id("net.minecrell.plugin-yml.paper") version "0.6.0" apply false + id("io.papermc.hangar-publish-plugin") version "0.1.2" apply false + id("com.modrinth.minotaur") version "2.+" apply false +} + allprojects { group = "de.oliver" description = "Minecraft plugins of FancyInnovations" diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..3620c405 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b415..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libraries/common/.gitignore b/libraries/common/.gitignore new file mode 100644 index 00000000..1dec6e1b --- /dev/null +++ b/libraries/common/.gitignore @@ -0,0 +1,122 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Eclipse +.classpath +.project +.settings/ +plugin/bin/ +api/bin/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ +build +.gradle +/*.jar \ No newline at end of file diff --git a/libraries/common/README.md b/libraries/common/README.md new file mode 100644 index 00000000..0407ae23 --- /dev/null +++ b/libraries/common/README.md @@ -0,0 +1,13 @@ +# How to use + +```kotlin +repositories { + maven("https://repo.fancyplugins.de/releases/") + // or + maven("https://repo.fancyplugins.de/snapshots/") +} + +dependencies { + implementation("de.oliver:FancyLib:") +} +``` \ No newline at end of file diff --git a/libraries/common/build.gradle.kts b/libraries/common/build.gradle.kts new file mode 100644 index 00000000..fdc9a3e2 --- /dev/null +++ b/libraries/common/build.gradle.kts @@ -0,0 +1,91 @@ +plugins { + id("java") + id("maven-publish") + id("com.github.johnrengelman.shadow") +} + +group = "de.oliver" +version = "35" +description = "Library for all Fancy plugins" + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) +} + +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.fancyplugins.de/releases") +} + +dependencies { + compileOnly("dev.folia:folia-api:1.20.4-R0.1-SNAPSHOT") + compileOnly("de.oliver.FancyAnalytics:logger:0.0.5") + + // database drivers + compileOnly("org.xerial:sqlite-jdbc:3.46.0.0") + compileOnly("mysql:mysql-connector-java:8.0.33") + + // testing + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.1") + testImplementation("com.google.code.gson:gson:2.11.0") +} + +tasks { + publishing { + repositories { + maven { + name = "fancypluginsReleases" + url = uri("https://repo.fancyplugins.de/releases") + credentials(PasswordCredentials::class) + authentication { + isAllowInsecureProtocol = true + create("basic") + } + } + + maven { + name = "fancypluginsSnapshots" + url = uri("https://repo.fancyplugins.de/snapshots") + credentials(PasswordCredentials::class) + authentication { + isAllowInsecureProtocol = true + create("basic") + } + } + } + publications { + create("maven") { + groupId = project.group.toString() + artifactId = project.name + version = project.version.toString() + from(project.components["java"]) + } + } + } + + compileJava { + options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything + + // Set the release flag. This configures what version bytecode the compiler will emit, as well as what JDK APIs are usable. + // See https://openjdk.java.net/jeps/247 for more information. + options.release.set(17) + } + + java { + withSourcesJar() + withJavadocJar() + } + + javadoc { + options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything + } + processResources { + filteringCharset = Charsets.UTF_8.name() // We want UTF-8 for everything + } + + test { + useJUnitPlatform() + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/ConfigHelper.java b/libraries/common/src/main/java/de/oliver/fancylib/ConfigHelper.java new file mode 100644 index 00000000..4002d8f1 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/ConfigHelper.java @@ -0,0 +1,16 @@ +package de.oliver.fancylib; + +import org.bukkit.configuration.file.FileConfiguration; + +public class ConfigHelper { + + public static Object getOrDefault(FileConfiguration config, String path, Object defaultVal) { + if (!config.contains(path)) { + config.set(path, defaultVal); + return defaultVal; + } + + return config.get(path); + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/FancyLib.java b/libraries/common/src/main/java/de/oliver/fancylib/FancyLib.java new file mode 100644 index 00000000..dd2cd863 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/FancyLib.java @@ -0,0 +1,59 @@ +package de.oliver.fancylib; + +import de.oliver.fancyanalytics.logger.ExtendedFancyLogger; +import de.oliver.fancylib.gui.inventoryClick.InventoryClickListener; +import de.oliver.fancylib.gui.inventoryClick.impl.CancelInventoryItemClick; +import de.oliver.fancylib.gui.inventoryClick.impl.ChangePageInventoryItemClick; +import de.oliver.fancylib.itemClick.PlayerInteractListener; +import de.oliver.fancylib.serverSoftware.ServerSoftware; +import de.oliver.fancylib.serverSoftware.schedulers.BukkitScheduler; +import de.oliver.fancylib.serverSoftware.schedulers.FancyScheduler; +import de.oliver.fancylib.serverSoftware.schedulers.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; + +public class FancyLib { + + private static FancyLib instance; + private static ExtendedFancyLogger logger = new ExtendedFancyLogger("FancyLib"); + + private final JavaPlugin plugin; + private final FancyScheduler scheduler; + + public FancyLib(JavaPlugin plugin) { + instance = this; + this.plugin = plugin; + this.scheduler = ServerSoftware.isFolia() + ? new FoliaScheduler(plugin) + : new BukkitScheduler(plugin); + } + + public static FancyLib getInstance() { + return instance; + } + + public static ExtendedFancyLogger getLogger() { + return logger; + } + + /** + * Registers the listeners for the inventory click and player interact events. + */ + public void registerListeners() { + CancelInventoryItemClick.INSTANCE.register(); + ChangePageInventoryItemClick.INSTANCE.register(); + + Bukkit.getPluginManager().registerEvents(new InventoryClickListener(), plugin); + Bukkit.getPluginManager().registerEvents(new PlayerInteractListener(), plugin); + } + + @ApiStatus.Internal + public FancyScheduler getScheduler() { + return scheduler; + } + + public JavaPlugin getPlugin() { + return plugin; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/FileUtils.java b/libraries/common/src/main/java/de/oliver/fancylib/FileUtils.java new file mode 100644 index 00000000..7499da21 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/FileUtils.java @@ -0,0 +1,111 @@ +package de.oliver.fancylib; + +import org.bukkit.plugin.Plugin; + +import java.io.*; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class FileUtils { + + public static String getSHA256Checksum(File file) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] byteArray = new byte[1024]; + int bytesCount; + + // Read the file data and update the MessageDigest + while ((bytesCount = fis.read(byteArray)) != -1) { + digest.update(byteArray, 0, bytesCount); + } + } + + // Get the final hash bytes + byte[] hashBytes = digest.digest(); + + // Convert hash bytes to hex format + StringBuilder hexString = new StringBuilder(); + for (byte hashByte : hashBytes) { + String hex = Integer.toHexString(0xff & hashByte); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (IOException | NoSuchAlgorithmException e) { + return "N/A"; + } + } + + public static File findFirstFileByName(File directory, String name) { + File[] files = directory.listFiles(); + if (files == null) { + return null; + } + for (File file : files) { + if (file.getName().toLowerCase().contains(name.toLowerCase())) { + return file; + } + } + return null; + } + + public String readResource(String name) { + URL url = getClass().getClassLoader().getResource(name); + if (url == null) { + return null; + } + URLConnection connection = null; + try { + connection = url.openConnection(); + } catch (IOException e) { + e.printStackTrace(); + } + connection.setUseCaches(false); + try (InputStream inputStream = connection.getInputStream()) { + byte[] file_raw = new byte[inputStream.available()]; + inputStream.read(file_raw); + inputStream.close(); + return new String(file_raw, StandardCharsets.UTF_8); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public void saveFile(Plugin plugin, String name) { + URL url = getClass().getClassLoader().getResource(name); + if (url == null) { + return; + } + File file = new File(plugin.getDataFolder() + File.separator + name); + if (file.exists()) return; + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + URLConnection connection = null; + try { + connection = url.openConnection(); + } catch (IOException e) { + e.printStackTrace(); + } + connection.setUseCaches(false); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + int read; + InputStream inputStream = connection.getInputStream(); + byte[] bytes = new byte[1024]; + while ((read = inputStream.read(bytes)) != -1) { + outputStream.write(bytes, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/MessageHelper.java b/libraries/common/src/main/java/de/oliver/fancylib/MessageHelper.java new file mode 100644 index 00000000..723e8dfb --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/MessageHelper.java @@ -0,0 +1,129 @@ +package de.oliver.fancylib; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Deprecated +public class MessageHelper { + private static String primaryColor = "#59bdff"; + private static String secondaryColor = "#6696e3"; + private static String successColor = "#81e366"; + private static String warningColor = "#e3ca66"; + private static String errorColor = "#e36666"; + + @Deprecated + public static @NotNull String transform(@NotNull String message) { + message = message.replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", ""); + + return message; + } + + @Deprecated + public static @NotNull Component send(@Nullable CommandSender receiver, @NotNull String message) { + Component msg = MiniMessage.miniMessage().deserialize(transform(message)); + if (receiver != null) { + receiver.sendMessage(msg); + } + return msg; + } + + @Deprecated + public static @NotNull Component info(@Nullable CommandSender receiver, @NotNull String message) { + Component msg = MiniMessage.miniMessage().deserialize(transform("" + message)); + if (receiver != null) { + receiver.sendMessage(msg); + } + return msg; + } + + @Deprecated + public static @NotNull Component success(@Nullable CommandSender receiver, @NotNull String message) { + Component msg = MiniMessage.miniMessage().deserialize(transform("" + message)); + if (receiver != null) { + receiver.sendMessage(msg); + } + return msg; + } + + @Deprecated + public static @NotNull Component warning(@Nullable CommandSender receiver, @NotNull String message) { + Component msg = MiniMessage.miniMessage().deserialize(transform("" + message)); + if (receiver != null) { + receiver.sendMessage(msg); + } + return msg; + } + + @Deprecated + public static @NotNull Component error(@Nullable CommandSender receiver, @NotNull String message) { + Component msg = MiniMessage.miniMessage().deserialize(transform("" + message)); + if (receiver != null) { + receiver.sendMessage(msg); + } + return msg; + } + + @Deprecated + public static String getPrimaryColor() { + return primaryColor; + } + + @Deprecated + public static void setPrimaryColor(String primaryColor) { + MessageHelper.primaryColor = primaryColor; + } + + @Deprecated + public static String getSecondaryColor() { + return secondaryColor; + } + + @Deprecated + public static void setSecondaryColor(String secondaryColor) { + MessageHelper.secondaryColor = secondaryColor; + } + + @Deprecated + public static String getSuccessColor() { + return successColor; + } + + @Deprecated + public static void setSuccessColor(String successColor) { + MessageHelper.successColor = successColor; + } + + @Deprecated + public static String getWarningColor() { + return warningColor; + } + + @Deprecated + public static void setWarningColor(String warningColor) { + MessageHelper.warningColor = warningColor; + } + + @Deprecated + public static String getErrorColor() { + return errorColor; + } + + @Deprecated + public static void setErrorColor(String errorColor) { + MessageHelper.errorColor = errorColor; + } + + @Deprecated + public static Component removeDecoration(Component component, TextDecoration decoration) { + return component.decoration(decoration, TextDecoration.State.FALSE); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/Metrics.java b/libraries/common/src/main/java/de/oliver/fancylib/Metrics.java new file mode 100644 index 00000000..8f89741c --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/Metrics.java @@ -0,0 +1,881 @@ +/* + * This Metrics class was auto-generated and can be copied into your project if you are + * not using a build tool like Gradle or Maven for dependency management. + * + * IMPORTANT: You are not allowed to modify this class, except changing the package. + * + * Disallowed modifications include but are not limited to: + * - Remove the option for users to opt-out + * - Change the frequency for data submission + * - Obfuscate the code (every obfuscator should allow you to make an exception for specific files) + * - Reformat the code (if you use a linter, add an exception) + * + * Violations will result in a ban of your plugin and account from bStats. + */ +package de.oliver.fancylib; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public class Metrics { + + private final Plugin plugin; + + private final MetricsBase metricsBase; + + /** + * Creates a new Metrics instance. + * + * @param plugin Your plugin instance. + * @param serviceId The id of the service. It can be found at What is my plugin id? + */ + public Metrics(JavaPlugin plugin, int serviceId) { + this.plugin = plugin; + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true); + config.addDefault("serverUuid", UUID.randomUUID().toString()); + config.addDefault("logFailedRequests", false); + config.addDefault("logSentData", false); + config.addDefault("logResponseStatusText", false); + // Inform the server owners about bStats + config + .options() + .header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous.") + .copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { + } + } + // Load the data + boolean enabled = config.getBoolean("enabled", true); + String serverUUID = config.getString("serverUuid"); + boolean logErrors = config.getBoolean("logFailedRequests", false); + boolean logSentData = config.getBoolean("logSentData", false); + boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); + metricsBase = + new MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + this::appendPlatformData, + this::appendServiceData, + submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), + plugin::isEnabled, + (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), + (message) -> this.plugin.getLogger().log(Level.INFO, message), + logErrors, + logSentData, + logResponseStatusText); + } + + /** Shuts down the underlying scheduler service. */ + public void shutdown() { + metricsBase.shutdown(); + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + metricsBase.addCustomChart(chart); + } + + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("playerAmount", getPlayerAmount()); + builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); + builder.appendField("bukkitVersion", Bukkit.getVersion()); + builder.appendField("bukkitName", Bukkit.getName()); + builder.appendField("javaVersion", System.getProperty("java.version")); + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + + private void appendServiceData(JsonObjectBuilder builder) { + builder.appendField("pluginVersion", plugin.getDescription().getVersion()); + } + + private int getPlayerAmount() { + try { + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + return onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + // Just use the new method if the reflection failed + return Bukkit.getOnlinePlayers().size(); + } + } + + public static class MetricsBase { + + /** The version of the Metrics class. */ + public static final String METRICS_VERSION = "3.0.2"; + + private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; + + private final ScheduledExecutorService scheduler; + + private final String platform; + + private final String serverUuid; + + private final int serviceId; + + private final Consumer appendPlatformDataConsumer; + + private final Consumer appendServiceDataConsumer; + + private final Consumer submitTaskConsumer; + + private final Supplier checkServiceEnabledSupplier; + + private final BiConsumer errorLogger; + + private final Consumer infoLogger; + + private final boolean logErrors; + + private final boolean logSentData; + + private final boolean logResponseStatusText; + + private final Set customCharts = new HashSet<>(); + + private final boolean enabled; + + /** + * Creates a new MetricsBase class instance. + * + * @param platform The platform of the service. + * @param serviceId The id of the service. + * @param serverUuid The server uuid. + * @param enabled Whether or not data sending is enabled. + * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all platform-specific data. + * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all service-specific data. + * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be + * used to delegate the data collection to a another thread to prevent errors caused by + * concurrency. Can be {@code null}. + * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. + * @param errorLogger A consumer that accepts log message and an error. + * @param infoLogger A consumer that accepts info log messages. + * @param logErrors Whether or not errors should be logged. + * @param logSentData Whether or not the sent data should be logged. + * @param logResponseStatusText Whether or not the response status text should be logged. + */ + public MetricsBase( + String platform, + String serverUuid, + int serviceId, + boolean enabled, + Consumer appendPlatformDataConsumer, + Consumer appendServiceDataConsumer, + Consumer submitTaskConsumer, + Supplier checkServiceEnabledSupplier, + BiConsumer errorLogger, + Consumer infoLogger, + boolean logErrors, + boolean logSentData, + boolean logResponseStatusText) { + ScheduledThreadPoolExecutor scheduler = + new ScheduledThreadPoolExecutor(1, task -> new Thread(task, "bStats-Metrics")); + // We want delayed tasks (non-periodic) that will execute in the future to be + // cancelled when the scheduler is shutdown. + // Otherwise, we risk preventing the server from shutting down even when + // MetricsBase#shutdown() is called + scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.scheduler = scheduler; + this.platform = platform; + this.serverUuid = serverUuid; + this.serviceId = serviceId; + this.enabled = enabled; + this.appendPlatformDataConsumer = appendPlatformDataConsumer; + this.appendServiceDataConsumer = appendServiceDataConsumer; + this.submitTaskConsumer = submitTaskConsumer; + this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; + this.errorLogger = errorLogger; + this.infoLogger = infoLogger; + this.logErrors = logErrors; + this.logSentData = logSentData; + this.logResponseStatusText = logResponseStatusText; + checkRelocation(); + if (enabled) { + // WARNING: Removing the option to opt-out will get your plugin banned from + // bStats + startSubmitting(); + } + } + + public void addCustomChart(CustomChart chart) { + this.customCharts.add(chart); + } + + public void shutdown() { + scheduler.shutdown(); + } + + private void startSubmitting() { + final Runnable submitTask = + () -> { + if (!enabled || !checkServiceEnabledSupplier.get()) { + // Submitting data or service is disabled + scheduler.shutdown(); + return; + } + if (submitTaskConsumer != null) { + submitTaskConsumer.accept(this::submitData); + } else { + this.submitData(); + } + }; + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven + // distribution of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into + // the initial and second delay. + // WARNING: You must not modify and part of this Metrics class, including the + // submit delay or frequency! + // WARNING: Modifying this code will get your plugin banned on bStats. Just + // don't do it! + long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); + long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); + scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate( + submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); + } + + private void submitData() { + final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); + appendPlatformDataConsumer.accept(baseJsonBuilder); + final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); + appendServiceDataConsumer.accept(serviceJsonBuilder); + JsonObjectBuilder.JsonObject[] chartData = + customCharts.stream() + .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) + .filter(Objects::nonNull) + .toArray(JsonObjectBuilder.JsonObject[]::new); + serviceJsonBuilder.appendField("id", serviceId); + serviceJsonBuilder.appendField("customCharts", chartData); + baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); + baseJsonBuilder.appendField("serverUUID", serverUuid); + baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); + JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); + scheduler.execute( + () -> { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (logErrors) { + errorLogger.accept("Could not submit bStats metrics data", e); + } + } + }); + } + + private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { + if (logSentData) { + infoLogger.accept("Sent bStats metrics data: " + data.toString()); + } + String url = String.format(REPORT_URL, platform); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", "Metrics-Service/1"); + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(compressedData); + } + StringBuilder builder = new StringBuilder(); + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + } + if (logResponseStatusText) { + infoLogger.accept("Sent data to bStats and received response: " + builder); + } + } + + /** Checks that the class was properly relocated. */ + private void checkRelocation() { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null + || !System.getProperty("bstats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this + // little "trick" ... :D + final String defaultPackage = + new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); + final String examplePackage = + new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure no one just copy & pastes the example and uses the wrong + // package names + if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) + || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + /** + * Gzips the given string. + * + * @param str The string to gzip. + * @return The gzipped string. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + } + return outputStream.toByteArray(); + } + } + + public static class SimplePie extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimplePie(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + public static class MultiLineChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public MultiLineChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedPie extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedPie(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class SimpleBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimpleBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().length == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class DrilldownPie extends CustomChart { + + private final Callable>> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public DrilldownPie(String chartId, Callable>> callable) { + super(chartId); + this.callable = callable; + } + + @Override + public JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public abstract static class CustomChart { + + private final String chartId; + + protected CustomChart(String chartId) { + if (chartId == null) { + throw new IllegalArgumentException("chartId must not be null"); + } + this.chartId = chartId; + } + + public JsonObjectBuilder.JsonObject getRequestJsonObject( + BiConsumer errorLogger, boolean logErrors) { + JsonObjectBuilder builder = new JsonObjectBuilder(); + builder.appendField("chartId", chartId); + try { + JsonObjectBuilder.JsonObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + builder.appendField("data", data); + } catch (Throwable t) { + if (logErrors) { + errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return builder.build(); + } + + protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; + } + + public static class SingleLineChart extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SingleLineChart(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + /** + * An extremely simple JSON builder. + * + *

While this class is neither feature-rich nor the most performant one, it's sufficient enough + * for its use-case. + */ + public static class JsonObjectBuilder { + + private StringBuilder builder = new StringBuilder(); + + private boolean hasAtLeastOneField = false; + + public JsonObjectBuilder() { + builder.append("{"); + } + + /** + * Appends a null field to the JSON. + * + * @param key The key of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendNull(String key) { + appendFieldUnescaped(key, "null"); + return this; + } + + /** + * Appends a string field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String value) { + if (value == null) { + throw new IllegalArgumentException("JSON value must not be null"); + } + appendFieldUnescaped(key, "\"" + escape(value) + "\""); + return this; + } + + /** + * Appends an integer field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int value) { + appendFieldUnescaped(key, String.valueOf(value)); + return this; + } + + /** + * Appends an object to the JSON. + * + * @param key The key of the field. + * @param object The object. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject object) { + if (object == null) { + throw new IllegalArgumentException("JSON object must not be null"); + } + appendFieldUnescaped(key, object.toString()); + return this; + } + + /** + * Appends a string array to the JSON. + * + * @param key The key of the field. + * @param values The string array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values) + .map(value -> "\"" + escape(value) + "\"") + .collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an integer array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an object array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends a field to the object. + * + * @param key The key of the field. + * @param escapedValue The escaped value of the field. + */ + private void appendFieldUnescaped(String key, String escapedValue) { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + if (key == null) { + throw new IllegalArgumentException("JSON key must not be null"); + } + if (hasAtLeastOneField) { + builder.append(","); + } + builder.append("\"").append(escape(key)).append("\":").append(escapedValue); + hasAtLeastOneField = true; + } + + /** + * Builds the JSON string and invalidates this builder. + * + * @return The built JSON string. + */ + public JsonObject build() { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + JsonObject object = new JsonObject(builder.append("}").toString()); + builder = null; + return object; + } + + /** + * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. + * + *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. + * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). + * + * @param value The value to escape. + * @return The escaped value. + */ + private static String escape(String value) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"') { + builder.append("\\\""); + } else if (c == '\\') { + builder.append("\\\\"); + } else if (c <= '\u000F') { + builder.append("\\u000").append(Integer.toHexString(c)); + } else if (c <= '\u001F') { + builder.append("\\u00").append(Integer.toHexString(c)); + } else { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * A super simple representation of a JSON object. + * + *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not + * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, + * JsonObject)}. + */ + public static class JsonObject { + + private final String value; + + private JsonObject(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + } +} \ No newline at end of file diff --git a/libraries/common/src/main/java/de/oliver/fancylib/RandomUtils.java b/libraries/common/src/main/java/de/oliver/fancylib/RandomUtils.java new file mode 100644 index 00000000..1ada0b11 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/RandomUtils.java @@ -0,0 +1,16 @@ +package de.oliver.fancylib; + +import java.util.concurrent.ThreadLocalRandom; + +public class RandomUtils { + + public static boolean random(double percentage) { + if (ThreadLocalRandom.current().nextDouble(0, 100) <= percentage) return true; + return false; + } + + public static double randomInRange(double min, double max) { + return (ThreadLocalRandom.current().nextDouble() * (max - min)) + min; + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/ReflectionUtils.java b/libraries/common/src/main/java/de/oliver/fancylib/ReflectionUtils.java new file mode 100644 index 00000000..4a097b00 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/ReflectionUtils.java @@ -0,0 +1,54 @@ +package de.oliver.fancylib; + +import java.lang.reflect.Field; + +public class ReflectionUtils { + + public static Object getValue(Object instance, String name) { + Object result = null; + + try { + Field field = instance.getClass().getDeclaredField(name); + + field.setAccessible(true); + result = field.get(instance); + field.setAccessible(false); + + } catch (Exception e) { + e.printStackTrace(); + } + + return result; + } + + public static Object getStaticValue(Class clazz, String name) { + Object result = null; + + try { + Field field = clazz.getDeclaredField(name); + + field.setAccessible(true); + result = field.get(clazz); + field.setAccessible(false); + + } catch (Exception e) { + e.printStackTrace(); + } + + return result; + } + + public static void setValue(Object instance, String name, Object value) { + try { + Field field = instance.getClass().getDeclaredField(name); + + field.setAccessible(true); + field.set(instance, value); + field.setAccessible(false); + + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/UUIDFetcher.java b/libraries/common/src/main/java/de/oliver/fancylib/UUIDFetcher.java new file mode 100644 index 00000000..6ecbe10e --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/UUIDFetcher.java @@ -0,0 +1,154 @@ +package de.oliver.fancylib; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + + +/* + From: https://gist.github.com/Jofkos/d0c469528b032d820f42 + */ + +public class UUIDFetcher { + + /** + * Date when name changes were introduced + * + * @see UUIDFetcher#getUUIDAt(String, long) + */ + public static final long FEBRUARY_2015 = 1422748800000L; + private static final String UUID_URL = "https://api.mojang.com/users/profiles/minecraft/%s?at=%d"; + private static final String NAME_URL = "https://api.mojang.com/user/profile/%s"; + private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + private static Map uuidCache = new HashMap(); + private static Map nameCache = new HashMap(); + + private static ExecutorService pool = Executors.newCachedThreadPool(); + + private String name; + private UUID id; + + /** + * Fetches the uuid asynchronously and passes it to the consumer + * + * @param name The name + * @param action Do what you want to do with the uuid her + */ + public static void getUUID(String name, Consumer action) { + pool.execute(() -> action.accept(getUUID(name))); + } + + /** + * Fetches the uuid synchronously and returns it + * + * @param name The name + * @return The uuid + */ + public static UUID getUUID(String name) { + return getUUIDAt(name, System.currentTimeMillis()); + } + + /** + * Fetches the uuid synchronously for a specified name and time and passes the result to the consumer + * + * @param name The name + * @param timestamp Time when the player had this name in milliseconds + * @param action Do what you want to do with the uuid her + */ + public static void getUUIDAt(String name, long timestamp, Consumer action) { + pool.execute(() -> action.accept(getUUIDAt(name, timestamp))); + } + + /** + * Fetches the uuid synchronously for a specified name and time + * + * @param name The name + * @param timestamp Time when the player had this name in milliseconds + * @see UUIDFetcher#FEBRUARY_2015 + */ + public static UUID getUUIDAt(String name, long timestamp) { + name = name.toLowerCase(); + if (uuidCache.containsKey(name)) { + return uuidCache.get(name); + } + try { + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(UUID_URL, name, timestamp / 1000)).openConnection(); + connection.setReadTimeout(5000); + UUIDFetcher data = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), UUIDFetcher.class); + + uuidCache.put(name, data.id); + nameCache.put(data.id, data.name); + + return data.id; + } catch (Exception e) { + return null; + } + } + + /** + * Fetches the name asynchronously and passes it to the consumer + * + * @param uuid The uuid + * @param action Do what you want to do with the name her + */ + public static void getName(UUID uuid, Consumer action) { + pool.execute(() -> action.accept(getName(uuid))); + } + + /** + * Fetches the name synchronously and returns it + * + * @param uuid The uuid + * @return The name + */ + public static String getName(UUID uuid) { + if (nameCache.containsKey(uuid)) { + return nameCache.get(uuid); + } + try { + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(NAME_URL, UUIDTypeAdapter.fromUUID(uuid))).openConnection(); + connection.setReadTimeout(5000); + UUIDFetcher currentNameData = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), UUIDFetcher.class); + + uuidCache.put(currentNameData.name.toLowerCase(), uuid); + nameCache.put(uuid, currentNameData.name); + + return currentNameData.name; + } catch (Exception e) { + return null; + } + } + +} + +class UUIDTypeAdapter extends TypeAdapter { + public static String fromUUID(UUID value) { + return value.toString().replace("-", ""); + } + + public static UUID fromString(String input) { + return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); + } + + public void write(JsonWriter out, UUID value) throws IOException { + out.value(fromUUID(value)); + } + + public UUID read(JsonReader in) throws IOException { + return fromString(in.nextString()); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/VersionConfig.java b/libraries/common/src/main/java/de/oliver/fancylib/VersionConfig.java new file mode 100644 index 00000000..b3117f21 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/VersionConfig.java @@ -0,0 +1,111 @@ +package de.oliver.fancylib; + +import de.oliver.fancylib.versionFetcher.VersionFetcher; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import java.util.function.Function; + +public class VersionConfig { + + private final Plugin plugin; + private final VersionFetcher fetcher; + private String version; + private String build; + private String hash; + + public VersionConfig(Plugin plugin, VersionFetcher fetcher) { + this.plugin = plugin; + this.fetcher = fetcher; + } + + public void load() { + YamlConfiguration config = new YamlConfiguration(); + try { + config.loadFromString(new FileUtils().readResource("version.yml")); + } catch (InvalidConfigurationException e) { + e.printStackTrace(); + } + + version = config.getString("version"); + build = config.getString("build"); + hash = config.getString("hash"); + } + + public void checkVersionAndDisplay(CommandSender sender, boolean displayOnlyIfOutdated) { + String newestVersion = usingLatestVersion(); + + if (newestVersion == null) { + MessageHelper.error(sender, "Could not fetch latest version"); + return; + } + + if (!newestVersion.isEmpty()) { + MessageHelper.warning(sender, outdatedVersion(newestVersion, fetcher.getDownloadUrl())); + return; + } + + if (!displayOnlyIfOutdated) { + MessageHelper.success(sender, latestVersion()); + } + } + + /** + * @return null if could not fetch, empty string if it's the newest, the newest version if it's not the current + */ + private String usingLatestVersion() { + ComparableVersion newestVersion = fetcher.fetchNewestVersion(); + ComparableVersion currentVersion = new ComparableVersion(version); + if (newestVersion == null) { + return null; + } + + if (newestVersion.compareTo(currentVersion) <= 0) { + return ""; + } + + return newestVersion.toString(); + } + + private String latestVersion() { + String result = "This server is using the latest version of {plugin}!\n" + + "Version: {version} (Git: {hash})" + + "{build}"; + + result = result.replace("{plugin}", plugin.getName()) + .replace("{version}", version) + .replace("{hash}", hash.substring(0, 7)) + .replace("{build}", build.equalsIgnoreCase("undefined") ? "" : "\nBuild: " + build); + + return result; + } + + private String outdatedVersion(String latestVersion, String downloadUrl) { + String result = "This server is using an outdated version of {plugin}\n" + + "Current version: {current_ver}\n" + + "Latest version: {latest_ver}\n" + + "Download latest version: click here"; + + result = result.replace("{plugin}", plugin.getName()) + .replace("{current_ver}", version) + .replace("{latest_ver}", latestVersion) + .replace("{download_url}", downloadUrl); + + return result; + } + + public String getVersion() { + return version; + } + + public String getBuild() { + return build; + } + + public String getHash() { + return hash; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/databases/Database.java b/libraries/common/src/main/java/de/oliver/fancylib/databases/Database.java new file mode 100644 index 00000000..d2f6dd8d --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/databases/Database.java @@ -0,0 +1,45 @@ +package de.oliver.fancylib.databases; + +import java.sql.Connection; +import java.sql.ResultSet; + +public interface Database { + + /** + * Connects to the database + * + * @return true if success, false if failed + */ + boolean connect(); + + /** + * Closes the database connection + * + * @return true if success, false if failed + */ + boolean close(); + + /** + * @return true if connected, false if not + */ + boolean isConnected(); + + /** + * @return the connection object, null if not connected + */ + Connection getConnection(); + + /** + * Executes a statement on the database + * + * @return true if success, false if failed + */ + boolean executeNonQuery(String sql); + + /** + * Executes a query on the database + * + * @return the result or null if failed + */ + ResultSet executeQuery(String query); +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/databases/MySqlDatabase.java b/libraries/common/src/main/java/de/oliver/fancylib/databases/MySqlDatabase.java new file mode 100644 index 00000000..d7c2dfe7 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/databases/MySqlDatabase.java @@ -0,0 +1,88 @@ +package de.oliver.fancylib.databases; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; + +public class MySqlDatabase implements Database { + + protected final String host; + protected final String port; + protected final String database; + protected final String username; + protected final String password; + protected Connection connection; + + public MySqlDatabase(String host, String port, String database, String username, String password) { + this.host = host; + this.port = port; + this.database = database; + this.username = username; + this.password = password; + this.connection = null; + } + + @Override + public boolean connect() { + try { + if (isConnected()) { + return true; + } + +// Class.forName("com.mysql/.jdbc.Driver"); + connection = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/" + database, username, password); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean close() { + try { + if (!isConnected()) { + return true; + } + + connection.close(); + connection = null; + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean isConnected() { + return connection != null; + } + + @Override + public Connection getConnection() { + return connection; + } + + @Override + public boolean executeNonQuery(String sql) { + try { + connection.createStatement().execute(sql); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public ResultSet executeQuery(String query) { + try { + ResultSet resultSet = connection.createStatement().executeQuery(query); + return resultSet; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/databases/SqliteDatabase.java b/libraries/common/src/main/java/de/oliver/fancylib/databases/SqliteDatabase.java new file mode 100644 index 00000000..375c86cc --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/databases/SqliteDatabase.java @@ -0,0 +1,41 @@ +package de.oliver.fancylib.databases; + +import org.bukkit.Bukkit; + +import java.io.File; +import java.sql.DriverManager; + +public class SqliteDatabase extends MySqlDatabase { + + protected final String path; + protected File file; + + public SqliteDatabase(String path) { + super(null, null, null, null, null); + this.path = path; + this.file = new File(path); + } + + @Override + public boolean connect() { + try { + if (isConnected()) { + return true; + } + + if (!file.exists()) { + if (!file.createNewFile()) { + Bukkit.getLogger().warning("Could not create database file."); + return false; + } + } + + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + file.getPath()); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlag.java b/libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlag.java new file mode 100644 index 00000000..6c80b119 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlag.java @@ -0,0 +1,38 @@ +package de.oliver.fancylib.featureFlags; + +public class FeatureFlag { + + private final String name; + private final String description; + private boolean enabled; + private final boolean forceDisabled; + + public FeatureFlag(String name, String description, boolean forceDisabled) { + this.name = name; + this.description = description; + this.forceDisabled = forceDisabled; + this.enabled = false; + } + + public boolean isEnabled() { + if(forceDisabled) return false; + + return enabled; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isForceDisabled() { + return forceDisabled; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlagConfig.java b/libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlagConfig.java new file mode 100644 index 00000000..1fd54bc4 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/featureFlags/FeatureFlagConfig.java @@ -0,0 +1,79 @@ +package de.oliver.fancylib.featureFlags; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FeatureFlagConfig { + + private final Plugin plugin; + private final File configFile; + private final List featureFlags; + + public FeatureFlagConfig(Plugin plugin) { + this.plugin = plugin; + this.configFile = new File("plugins" + File.separator + plugin.getName() + File.separator + "featureFlags.yml"); + this.featureFlags = new ArrayList<>(); + } + + public void load() { + if (!configFile.exists()) { + try { + new File(configFile.getParent()).mkdirs(); + configFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + for (FeatureFlag featureFlag : featureFlags) { + config.setInlineComments("feature-flags." + featureFlag.getName(), List.of(featureFlag.getDescription())); + + if (config.isSet("feature-flags." + featureFlag.getName())) { + continue; + } + + config.set("feature-flags." + featureFlag.getName(), false); + } + + for (String flagName : config.getConfigurationSection("feature-flags").getKeys(false)) { + boolean enabled = config.getBoolean("feature-flags." + flagName, false); + FeatureFlag flag = getFeatureFlag(flagName); + if (flag == null) { + flag = new FeatureFlag(flagName, "", false); + featureFlags.add(flag); + } + + flag.setEnabled(enabled); + } + + try { + config.save(configFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public FeatureFlag getFeatureFlag(String name) { + for (FeatureFlag featureFlag : featureFlags) { + if (featureFlag.getName().equalsIgnoreCase(name.toLowerCase())) { + return featureFlag; + } + } + + return null; + } + + public void addFeatureFlag(FeatureFlag featureFlag) { + if (!featureFlags.contains(featureFlag)) { + featureFlags.add(featureFlag); + } + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/CustomPlayerInventory.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/CustomPlayerInventory.java new file mode 100644 index 00000000..5c7430da --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/CustomPlayerInventory.java @@ -0,0 +1,47 @@ +package de.oliver.fancylib.gui.customInventories; + +import de.oliver.fancylib.gui.inventoryClick.InventoryItemClick; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +public abstract class CustomPlayerInventory implements InventoryHolder { + + protected final Player player; + protected Inventory inventory; + + protected CustomPlayerInventory(Player player, int amountRows, Component title) { + this.player = player; + this.inventory = Bukkit.createInventory(this, amountRows * 9, title); + } + + public static ItemStack getPlaceholder() { + ItemStack item = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + item.editMeta(itemMeta -> { + itemMeta.displayName(Component.empty()); + itemMeta.getPersistentDataContainer().set(InventoryItemClick.ON_CLICK_KEY, PersistentDataType.STRING, "cancelClick"); + }); + + return item; + } + + public Player getPlayer() { + return player; + } + + @NotNull + @Override + public Inventory getInventory() { + return inventory; + } + + public void open() { + player.openInventory(inventory); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/PageInventory.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/PageInventory.java new file mode 100644 index 00000000..0ea3c403 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/customInventories/PageInventory.java @@ -0,0 +1,43 @@ +package de.oliver.fancylib.gui.customInventories; + +import de.oliver.fancylib.FancyLib; +import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.gui.inventoryClick.InventoryItemClick; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +public interface PageInventory { + + NamespacedKey PAGE_KEY = new NamespacedKey(FancyLib.getInstance().getPlugin(), "page"); + + static ItemStack previousPage(int currentPage) { + ItemStack previousPage = new ItemStack(Material.ARROW); + previousPage.editMeta(itemMeta -> { + itemMeta.displayName(MessageHelper.removeDecoration(MiniMessage.miniMessage().deserialize("Previous page"), TextDecoration.ITALIC)); + + itemMeta.getPersistentDataContainer().set(PageInventory.PAGE_KEY, PersistentDataType.INTEGER, currentPage - 1); + itemMeta.getPersistentDataContainer().set(InventoryItemClick.ON_CLICK_KEY, PersistentDataType.STRING, "changePage"); + }); + + return previousPage; + } + + static ItemStack nextPage(int currentPage) { + ItemStack nextPage = new ItemStack(Material.ARROW); + nextPage.editMeta(itemMeta -> { + itemMeta.displayName(MessageHelper.removeDecoration(MiniMessage.miniMessage().deserialize("Next page"), TextDecoration.ITALIC)); + + itemMeta.getPersistentDataContainer().set(PageInventory.PAGE_KEY, PersistentDataType.INTEGER, currentPage + 1); + itemMeta.getPersistentDataContainer().set(InventoryItemClick.ON_CLICK_KEY, PersistentDataType.STRING, "changePage"); + }); + + return nextPage; + } + + void loadPage(int page); + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickListener.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickListener.java new file mode 100644 index 00000000..920a8832 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickListener.java @@ -0,0 +1,28 @@ +package de.oliver.fancylib.gui.inventoryClick; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +public class InventoryClickListener implements Listener { + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + + try { + ItemStack item = event.getCurrentItem(); + + if (item.getItemMeta().getPersistentDataContainer().has(InventoryItemClick.ON_CLICK_KEY)) { + String id = item.getItemMeta().getPersistentDataContainer().get(InventoryItemClick.ON_CLICK_KEY, PersistentDataType.STRING); + InventoryClickRegistry.getInventoryItemClick(id).onClick(event, (Player) event.getWhoClicked()); + } + + } catch (NullPointerException ignore) { + } + + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickRegistry.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickRegistry.java new file mode 100644 index 00000000..91cfc5e8 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryClickRegistry.java @@ -0,0 +1,19 @@ +package de.oliver.fancylib.gui.inventoryClick; + + +import java.util.HashMap; +import java.util.Map; + +public class InventoryClickRegistry { + + private static final Map inventoryItemClickMap = new HashMap<>(); + + public static InventoryItemClick getInventoryItemClick(String id) { + return inventoryItemClickMap.getOrDefault(id, InventoryItemClick.EMPTY); + } + + public static void registerInventoryItemClick(InventoryItemClick inventoryItemClick) { + inventoryItemClickMap.put(inventoryItemClick.getId(), inventoryItemClick); + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryItemClick.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryItemClick.java new file mode 100644 index 00000000..253c5b97 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/InventoryItemClick.java @@ -0,0 +1,46 @@ +package de.oliver.fancylib.gui.inventoryClick; + +import de.oliver.fancylib.FancyLib; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; + +import java.util.List; + +public interface InventoryItemClick { + + NamespacedKey ON_CLICK_KEY = new NamespacedKey(FancyLib.getInstance().getPlugin(), "onclick"); + + InventoryItemClick EMPTY = new InventoryItemClick() { + @Override + public String getId() { + return ""; + } + + @Override + public void onClick(InventoryClickEvent event, Player player) { + } + }; + + static boolean hasKeys(ItemStack item, List keys) { + PersistentDataContainer data = item.getItemMeta().getPersistentDataContainer(); + for (NamespacedKey key : keys) { + if (!data.has(key)) { + return false; + } + } + + return true; + } + + String getId(); + + void onClick(InventoryClickEvent event, Player player); + + default void register() { + InventoryClickRegistry.registerInventoryItemClick(this); + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/CancelInventoryItemClick.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/CancelInventoryItemClick.java new file mode 100644 index 00000000..4aae89f9 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/CancelInventoryItemClick.java @@ -0,0 +1,23 @@ +package de.oliver.fancylib.gui.inventoryClick.impl; + +import de.oliver.fancylib.gui.inventoryClick.InventoryItemClick; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +public class CancelInventoryItemClick implements InventoryItemClick { + + public static final CancelInventoryItemClick INSTANCE = new CancelInventoryItemClick(); + + private CancelInventoryItemClick() { + } + + @Override + public String getId() { + return "cancelClick"; + } + + @Override + public void onClick(InventoryClickEvent event, Player player) { + event.setCancelled(true); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/ChangePageInventoryItemClick.java b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/ChangePageInventoryItemClick.java new file mode 100644 index 00000000..ef2d8c36 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/gui/inventoryClick/impl/ChangePageInventoryItemClick.java @@ -0,0 +1,47 @@ +package de.oliver.fancylib.gui.inventoryClick.impl; + +import de.oliver.fancylib.gui.customInventories.PageInventory; +import de.oliver.fancylib.gui.inventoryClick.InventoryItemClick; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Collections; +import java.util.List; + +public class ChangePageInventoryItemClick implements InventoryItemClick { + + public static final ChangePageInventoryItemClick INSTANCE = new ChangePageInventoryItemClick(); + + private final static List REQUIRED_KEYS = Collections.singletonList( + PageInventory.PAGE_KEY + ); + + private ChangePageInventoryItemClick() { + } + + @Override + public String getId() { + return "changePage"; + } + + @Override + public void onClick(InventoryClickEvent event, Player player) { + ItemStack item = event.getCurrentItem(); + + if (item != null && InventoryItemClick.hasKeys(item, REQUIRED_KEYS)) { + event.setCancelled(true); + + int page = item.getItemMeta().getPersistentDataContainer().get(PageInventory.PAGE_KEY, PersistentDataType.INTEGER); + + if (event.getInventory().getHolder() == null || !(event.getInventory().getHolder() instanceof PageInventory pageInventory)) { + return; + } + + pageInventory.loadPage(page); + } + + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClick.java b/libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClick.java new file mode 100644 index 00000000..c8cad356 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClick.java @@ -0,0 +1,30 @@ +package de.oliver.fancylib.itemClick; + +import de.oliver.fancylib.FancyLib; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerInteractEvent; + +public interface ItemClick { + NamespacedKey ON_CLICK_KEY = new NamespacedKey(FancyLib.getInstance().getPlugin(), "oninteract"); + + ItemClick EMPTY = new ItemClick() { + @Override + public String getId() { + return null; + } + + @Override + public void onClick(PlayerInteractEvent event, Player player) { + } + }; + + String getId(); + + void onClick(PlayerInteractEvent event, Player player); + + default void register() { + ItemClickRegistry.registerItemClick(this); + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClickRegistry.java b/libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClickRegistry.java new file mode 100644 index 00000000..502cda94 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/itemClick/ItemClickRegistry.java @@ -0,0 +1,18 @@ +package de.oliver.fancylib.itemClick; + +import java.util.HashMap; +import java.util.Map; + +public class ItemClickRegistry { + + private static final Map itemClickMap = new HashMap<>(); + + public static ItemClick getItemClick(String id) { + return itemClickMap.getOrDefault(id, ItemClick.EMPTY); + } + + public static void registerItemClick(ItemClick itemClick) { + itemClickMap.put(itemClick.getId(), itemClick); + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/itemClick/PlayerInteractListener.java b/libraries/common/src/main/java/de/oliver/fancylib/itemClick/PlayerInteractListener.java new file mode 100644 index 00000000..c8c44d51 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/itemClick/PlayerInteractListener.java @@ -0,0 +1,25 @@ +package de.oliver.fancylib.itemClick; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +public class PlayerInteractListener implements Listener { + + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event) { + try { + ItemStack item = event.getItem(); + if (event.getHand() == EquipmentSlot.HAND && item.getItemMeta().getPersistentDataContainer().has(ItemClick.ON_CLICK_KEY)) { + String id = item.getItemMeta().getPersistentDataContainer().get(ItemClick.ON_CLICK_KEY, PersistentDataType.STRING); + + ItemClickRegistry.getItemClick(id).onClick(event, event.getPlayer()); + } + } catch (NullPointerException ignore) { + } + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/jdb/JDB.java b/libraries/common/src/main/java/de/oliver/fancylib/jdb/JDB.java new file mode 100644 index 00000000..fbba075d --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/jdb/JDB.java @@ -0,0 +1,137 @@ +package de.oliver.fancylib.jdb; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * The JDB class provides a simple JSON document-based storage system in a specified directory. + */ +public class JDB { + private final static Gson GSON = new GsonBuilder() + .serializeNulls() + .setPrettyPrinting() + .create(); + + private static final String FILE_EXTENSION = ".json"; + private final @NotNull String basePath; + private final @NotNull File baseDirectory; + + /** + * Constructs a new JDB instance with the specified base path. + * + * @param basePath the base directory path where documents will be stored + */ + public JDB(@NotNull String basePath) { + this.basePath = basePath; + this.baseDirectory = new File(basePath); + } + + /** + * Retrieves a document from the specified path, deserializing it into the given class type. + * + * @param the type of the object to be returned + * @param path the relative path (excluding .json extension) where the document is located + * @param clazz the class type to which the document should be deserialized + * @return a JDocument containing the deserialized object and its path, or null if the file does not exist + * @throws IOException if an I/O error occurs during file reading + */ + public T get(@NotNull String path, @NotNull Class clazz) throws IOException { + File documentFile = new File(baseDirectory, createFilePath(path)); + if (!documentFile.exists()) { + return null; + } + BufferedReader bufferedReader = Files.newBufferedReader(documentFile.toPath()); + return GSON.fromJson(bufferedReader, clazz); + } + + /** + * Retrieves a document from the specified path. + * + * @param path the relative path (excluding .json extension) where the document is located + * @return a JDocument containing the deserialized data or null if the file does not exist + * @throws IOException if an I/O error occurs during file reading + */ + public JDocument getDocument(@NotNull String path) throws IOException { + File documentFile = new File(baseDirectory, createFilePath(path)); + if (!documentFile.exists()) { + return null; + } + BufferedReader bufferedReader = Files.newBufferedReader(documentFile.toPath()); + Map data = (Map) GSON.fromJson(bufferedReader, Map.class); + return new JDocument(data); + } + + /** + * Retrieves all documents from the specified directory path, deserializing them into the given class type. + * + * @param the type of objects to be returned + * @param path the relative directory path containing the documents + * @param clazz the class type to which the documents should be deserialized + * @return a List of JDocument objects containing the deserialized objects and their paths, or null if the directory or files do not exist + * @throws IOException if an I/O error occurs during file reading + */ + public List getAll(@NotNull String path, @NotNull Class clazz) throws IOException { + File directory = new File(baseDirectory, path); + if (!directory.exists()) { + return new ArrayList<>(); + } + File[] files = directory.listFiles(); + if (files == null) { + return new ArrayList<>(); + } + List documents = new ArrayList<>(files.length); + for (File file : files) { + documents.add(get(path + "/" + file.getName().replace(FILE_EXTENSION, ""), clazz)); + } + return documents; + } + + /** + * Saves the given value as a document at the specified path. + * + * @param the type of the object to be saved + * @param path the relative path (excluding .json extension) where the document will be saved + * @param value the object to be saved as a JSON document + * @throws IOException if an I/O error occurs during file writing + */ + public void set(@NotNull String path, @NotNull T value) throws IOException { + File documentFile = new File(baseDirectory, createFilePath(path)); + if (!documentFile.exists()) { + documentFile.getParentFile().mkdirs(); + documentFile.createNewFile(); + } + String json = GSON.toJson(value); + Files.write(documentFile.toPath(), json.getBytes()); + } + + /** + * Deletes the document(s) at the specified path. + * + * @param path the relative path (excluding .json extension) where the document(s) are located + */ + public void delete(@NotNull String path) { + File documentFile = new File(baseDirectory, createFilePath(path)); + if (documentFile.exists()) { + documentFile.delete(); + } + } + + /** + * Creates the file path by appending the base path, provided path, and the file extension. + * + * @param path the relative path (excluding .json extension) + * @return the full file path + */ + private String createFilePath(@NotNull String path) { + return path + FILE_EXTENSION; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/jdb/JDocument.java b/libraries/common/src/main/java/de/oliver/fancylib/jdb/JDocument.java new file mode 100644 index 00000000..d23082bc --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/jdb/JDocument.java @@ -0,0 +1,152 @@ +package de.oliver.fancylib.jdb; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Represents a document that holds a map of key-value pairs with support for nested keys. + */ +public class JDocument { + private final @NotNull Map data; + + public JDocument(@NotNull Map data) { + this.data = data; + } + + /** + * Retrieves a value from the document using the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the value associated with the given key, or null if the key is not found + */ + public Object get(String key) { + return getValue(key, Object.class); + } + + /** + * Checks if the document contains a value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return true if the given key exists in the document, otherwise false + */ + public boolean contains(String key) { + return get(key) != null; + } + + /** + * Retrieves the set of keys from a map value associated with a given key in the document. + * + * @param key the dot-separated key used to locate the map value in the document (e.g. "foo.bar.baz") + * @return a set of keys from the map associated with the given key, or an empty set if the key + * is not found or the value is not a map + */ + public Set getKeys(String key) { + Map map = (Map) getValue(key, Map.class); + return map != null ? map.keySet() : new HashSet<>(); + } + + /** + * Retrieves a string value from the document using the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the string value associated with the given key, or an empty string if the key is not found or the value is not a string + */ + public String getString(String key) { + return (String) getValueOrDefault(key, String.class, ""); + } + + /** + * Retrieves a boolean value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the boolean value associated with the given key, or false if the key is not found or the value is not a boolean + */ + public boolean getBoolean(String key) { + return (boolean) getValueOrDefault(key, Boolean.class, false); + } + + /** + * Retrieves a byte value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the byte value associated with the given key, or 0 if the key is not found or the value is not a byte + */ + public byte getByte(String key) { + return (byte) getValueOrDefault(key, Byte.class, (byte) 0); + } + + /** + * Retrieves a short value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the short value associated with the given key, or 0 if the key is not found or the value is not a short + */ + public short getShort(String key) { + return (short) getValueOrDefault(key, Short.class, (short) 0); + } + + /** + * Retrieves an integer value associated with the given key from the document. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the integer value associated with the given key, or 0 if the key is not found or the value is not an integer + */ + public int getInt(String key) { + return (int) getValueOrDefault(key, Integer.class, 0); + } + + /** + * Retrieves a long value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the long value associated with the given key, or 0 if the key is not found or the value is not a long + */ + public long getLong(String key) { + return (long) getValueOrDefault(key, Long.class, 0L); + } + + /** + * Retrieves a float value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the float value associated with the given key, or 0 if the key is not found or the value is not a float + */ + public float getFloat(String key) { + return (float) getValueOrDefault(key, Float.class, 0f); + } + + /** + * Retrieves a double value associated with the given key. + * + * @param key the dot-separated key used to locate the value in the document (e.g. "foo.bar.baz") + * @return the double value associated with the given key, or 0 if the key is not found or the value is not a double + */ + public double getDouble(String key) { + return (double) getValueOrDefault(key, Double.class, 0d); + } + + private Object getValue(String key, Class clazz) { + String[] parts = key.split("\\."); + Map current = data; + + for (int i = 0; i < parts.length; i++) { + Object value = current.get(parts[i]); + if (value == null || (i < parts.length - 1 && !(value instanceof Map))) { + return null; + } + if (i == parts.length - 1) { + return clazz.isInstance(value) ? value : null; + } + current = (Map) value; + } + return null; + } + + private T getValueOrDefault(String key, Class clazz, T defaultValue) { + T value = (T) getValue(key, clazz); + return value != null ? value : defaultValue; + } +} \ No newline at end of file diff --git a/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/ServerSoftware.java b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/ServerSoftware.java new file mode 100644 index 00000000..82cefdb4 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/ServerSoftware.java @@ -0,0 +1,46 @@ +package de.oliver.fancylib.serverSoftware; + +import de.oliver.fancylib.serverSoftware.schedulers.BukkitScheduler; +import de.oliver.fancylib.serverSoftware.schedulers.FancyScheduler; +import de.oliver.fancylib.serverSoftware.schedulers.FoliaScheduler; +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.bukkit.plugin.java.JavaPlugin; + +import java.lang.reflect.Method; +import java.util.Arrays; + +public class ServerSoftware { + + public static boolean isFolia() { + return Arrays.stream(PluginMeta.class.getDeclaredMethods()) + .map(Method::getName) + .anyMatch(s -> s.equals("isFoliaSupported")); + } + + public static boolean isPaper() { + try { + Class.forName("io.papermc.paper.event.player.AsyncChatEvent"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static boolean isBukkit() { + try { + Class.forName("org.bukkit.Bukkit"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static FancyScheduler getCorrectScheduler(JavaPlugin plugin) { + if (isFolia()) { + return new FoliaScheduler(plugin); + } + + return new BukkitScheduler(plugin); + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/BukkitScheduler.java b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/BukkitScheduler.java new file mode 100644 index 00000000..d1362602 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/BukkitScheduler.java @@ -0,0 +1,61 @@ +package de.oliver.fancylib.serverSoftware.schedulers; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +public class BukkitScheduler implements FancyScheduler { + + BukkitTask bukkitTask; + JavaPlugin plugin; + + public BukkitScheduler(JavaPlugin plugin) { + this.plugin = plugin; + } + + @Override + public @NotNull FancyScheduler runTask(Location location, Runnable task) { + bukkitTask = Bukkit.getScheduler().runTask(plugin, task); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskAsynchronously(Runnable task) { + bukkitTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, task); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskLater(Location location, long delay, Runnable task) { + bukkitTask = Bukkit.getScheduler().runTaskLater(plugin, task, delay); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskLaterAsynchronously(long delay, Runnable task) { + bukkitTask = Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, task, delay); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskTimer(Location location, long delay, long period, Runnable task) { + bukkitTask = Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskTimerAsynchronously(long delay, long period, Runnable task) { + bukkitTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delay, period); + return this; + } + + @Override + public void cancel() { + if (!bukkitTask.isCancelled()) { + bukkitTask.cancel(); + } + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FancyScheduler.java b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FancyScheduler.java new file mode 100644 index 00000000..2f0cdbc1 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FancyScheduler.java @@ -0,0 +1,69 @@ +package de.oliver.fancylib.serverSoftware.schedulers; + +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +public interface FancyScheduler { + + /** + * Run the task. + * + * @param task task... + * @param location required for Folia, in Bukkit can be null + * @return The created {@link FancyScheduler}. + */ + @NotNull FancyScheduler runTask(Location location, Runnable task); + + /** + * Run the task asynchronously. + * + * @param task task... + * @return The created {@link FancyScheduler} + */ + @NotNull FancyScheduler runTaskAsynchronously(Runnable task); + + /** + * Run the task after a specified number of ticks. + * + * @param location required for Folia, in Bukkit can be null + * @param task task... + * @param delay The number of ticks to wait. + * @return The created {@link FancyScheduler} + */ + @NotNull FancyScheduler runTaskLater(Location location, long delay, Runnable task); + + /** + * Run the task asynchronously after a specified number of ticks. + * + * @param task task... + * @param delay The number of ticks to wait. + * @return The created {@link FancyScheduler} + */ + @NotNull FancyScheduler runTaskLaterAsynchronously(long delay, Runnable task); + + /** + * Run the task repeatedly on a timer. + * + * @param location required for Folia, in Bukkit can be null + * @param task task... + * @param delay The delay before the task is first run (in ticks). + * @param period The ticks elapsed before the task is run again. + * @return The created {@link FancyScheduler} + */ + @NotNull FancyScheduler runTaskTimer(Location location, long delay, long period, Runnable task); + + /** + * Run the task repeatedly on a timer asynchronously. + * + * @param task task... + * @param delay The delay before the task is first run (in ticks). + * @param period The ticks elapsed before the task is run again. + * @return The created {@link FancyScheduler} + */ + @NotNull FancyScheduler runTaskTimerAsynchronously(long delay, long period, Runnable task); + + /** + * Cancel the task. + */ + void cancel(); +} \ No newline at end of file diff --git a/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FoliaScheduler.java b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FoliaScheduler.java new file mode 100644 index 00000000..ed65f1a2 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/serverSoftware/schedulers/FoliaScheduler.java @@ -0,0 +1,71 @@ +package de.oliver.fancylib.serverSoftware.schedulers; + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import org.bukkit.Location; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; + +public class FoliaScheduler implements FancyScheduler { + private static final long NANOSECONDS_PER_TICK = 50000000L; + + private final JavaPlugin plugin; + private ScheduledTask scheduledTask; + + public FoliaScheduler(JavaPlugin plugin) { + this.plugin = plugin; + } + + @Override + public @NotNull FancyScheduler runTask(Location location, Runnable task) { + if (location != null) { + scheduledTask = plugin.getServer().getRegionScheduler().run(plugin, location, scheduledTask1 -> task.run()); + } else { + scheduledTask = plugin.getServer().getGlobalRegionScheduler().run(plugin, scheduledTask1 -> task.run()); + } + return this; + } + + @Override + public @NotNull FancyScheduler runTaskAsynchronously(Runnable task) { + scheduledTask = plugin.getServer().getAsyncScheduler().runNow(plugin, scheduledTask1 -> task.run()); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskLater(Location location, long delay, Runnable task) { + if (location != null) { + scheduledTask = plugin.getServer().getRegionScheduler().runDelayed(plugin, location, scheduledTask1 -> task.run(), delay); + } else { + scheduledTask = plugin.getServer().getGlobalRegionScheduler().runDelayed(plugin, scheduledTask1 -> task.run(), delay); + } + return this; + } + + @Override + public @NotNull FancyScheduler runTaskLaterAsynchronously(long delay, Runnable task) { + scheduledTask = plugin.getServer().getAsyncScheduler().runDelayed(plugin, scheduledTask1 -> task.run(), delay * NANOSECONDS_PER_TICK, TimeUnit.NANOSECONDS); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskTimer(Location location, long delay, long period, Runnable task) { + scheduledTask = plugin.getServer().getRegionScheduler().runAtFixedRate(plugin, location, scheduledTask1 -> task.run(), delay, period); + return this; + } + + @Override + public @NotNull FancyScheduler runTaskTimerAsynchronously(long delay, long period, Runnable task) { + scheduledTask = plugin.getServer().getAsyncScheduler().runAtFixedRate(plugin, scheduledTask1 -> task.run(), delay * NANOSECONDS_PER_TICK, period * NANOSECONDS_PER_TICK, TimeUnit.NANOSECONDS); + return this; + } + + @Override + public void cancel() { + if (!scheduledTask.isCancelled()) { + scheduledTask.cancel(); + } + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/tests/Expectable.java b/libraries/common/src/main/java/de/oliver/fancylib/tests/Expectable.java new file mode 100644 index 00000000..ed9e6efc --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/tests/Expectable.java @@ -0,0 +1,243 @@ +package de.oliver.fancylib.tests; + +/** + * A generic class for making assertions on the expected values. + * + * @param the type of the value to be asserted. + */ +public class Expectable { + + /** + * The value that is being wrapped by this Expectable instance. + * This is the object against which all expectations will be verified. + */ + private final T t; + + private Expectable(T t) { + this.t = t; + } + + /** + * Creates a new instance of Expectable for the given value. + * + * @param the type of the value being tested + * @param t the actual value to create an expectation for + * @return a new Expectable instance for the given value + */ + public static Expectable expect(T t) { + return new Expectable<>(t); + } + + /** + * Ensures that the actual value is not null. + *

+ * Throws an AssertionError if the value of the field 't' is null, + * indicating that the actual value is expected to be non-null. + * + * @throws AssertionError if the value of the field 't' is null + */ + public void toBeDefined() { + if (t == null) { + throw new AssertionError("Expected not null but got null"); + } + } + + /** + * Asserts that the value of the field 't' is null. + *

+ * Throws an AssertionError if the value of 't' is not null, + * indicating the expectation that the value should be null. + * + * @throws AssertionError if the value of 't' is not null + */ + public void toBeNull() { + if (t != null) { + throw new AssertionError("Expected null but got not null"); + } + } + + /** + * Asserts that the actual value is equal to the expected value. + * + * @param expected the value that the actual value is expected to be equal to + * @throws AssertionError if the actual value is not equal to the expected value + */ + public void toBe(T expected) { + if (t != expected) { + throw new AssertionError("Expected " + expected + " but got " + t); + } + } + + /** + * Asserts that the actual value is equal to the expected value using the {@code equals} method. + * + * @param expected the value that the actual value is expected to be equal to + * @throws AssertionError if the actual value is not equal to the expected value + */ + public void toEqual(T expected) { + if (!t.equals(expected)) { + throw new AssertionError("Expected " + expected + " but got " + t); + } + } + + /** + * Asserts that the actual value is greater than the expected value. + * + * @param expected the value that the actual value is expected to be greater than + * @throws AssertionError if the actual value is not greater than the expected value, + * or if the type of the actual value is not one of Integer, Long, Float, or Double + */ + public void toBeGreaterThan(T expected) { + if (t instanceof Integer) { + if ((Integer) t <= (Integer) expected) { + throw new AssertionError("Expected " + t + " to be greater than " + expected); + } else { + return; + } + } else if (t instanceof Long) { + if ((Long) t <= (Long) expected) { + throw new AssertionError("Expected " + t + " to be greater than " + expected); + } else { + return; + } + } else if (t instanceof Float) { + if ((Float) t <= (Float) expected) { + throw new AssertionError("Expected " + t + " to be greater than " + expected); + } else { + return; + } + } else if (t instanceof Double) { + if ((Double) t <= (Double) expected) { + throw new AssertionError("Expected " + t + " to be greater than " + expected); + } else { + return; + } + } + + throw new AssertionError("toBeGreaterThan can only be used on Integers, Longs, Floats, and Doubles"); + } + + /** + * Asserts that the actual value is less than the expected value. + * + * @param expected the value that the actual value is expected to be less than + * @throws AssertionError if the actual value is not less than the expected value, + * or if the type of the actual value is not one of Integer, Long, Float, or Double + */ + public void toBeLessThan(T expected) { + if (t instanceof Integer) { + if ((Integer) t >= (Integer) expected) { + throw new AssertionError("Expected " + t + " to be less than " + expected); + } else { + return; + } + } else if (t instanceof Long) { + if ((Long) t >= (Long) expected) { + throw new AssertionError("Expected " + t + " to be less than " + expected); + } else { + return; + } + } else if (t instanceof Float) { + if ((Float) t >= (Float) expected) { + throw new AssertionError("Expected " + t + " to be less than " + expected); + } else { + return; + } + } else if (t instanceof Double) { + if ((Double) t >= (Double) expected) { + throw new AssertionError("Expected " + t + " to be less than " + expected); + } else { + return; + } + } + + throw new AssertionError("toBeLessThan can only be used on Integers, Longs, Floats, and Doubles"); + } + + /** + * Asserts that the actual value is an instance of the expected class. + * This method checks whether the value held in the field 't' is an instance of the provided Class. + * + * @param expected the Class object that the actual value is expected to be an instance of + * @throws AssertionError if the actual value is not an instance of the expected class + */ + public void toBeInstanceOf(Class expected) { + if (!expected.isInstance(t)) { + throw new AssertionError("Expected " + t + " to be an instance of " + expected); + } + } + + /** + * Asserts that the given expected value is contained within the actual value. + *

+ * This method checks if the expected value is present in a String, Iterable, or Array. + * If the actual value is a String, it uses the contains method to check if the expected value + * is a substring. If the actual value is an Iterable, it checks if the expected value is an element. + * If the actual value is an Array, it checks if the expected value is present in the array. + * + * @param expected the value that is expected to be contained within the actual value + * @throws AssertionError if the expected value is not contained within the actual value + */ + public void toContain(Object expected) { + if (t instanceof String) { + if (!((String) t).contains((String) expected)) { + throw new AssertionError("Expected " + expected + " to be contained in " + t); + } else { + return; + } + } else if (t instanceof Iterable) { + if (!((Iterable) t).spliterator().tryAdvance(o -> { + if (o.equals(expected)) { + return; + } + throw new AssertionError("Expected " + expected + " to be contained in " + t); + })) { + throw new AssertionError("Expected " + expected + " to be contained in " + t); + } else { + return; + } + } else if (t instanceof Object[]) { + for (Object o : (Object[]) t) { + if (o.equals(expected)) { + return; + } + } + throw new AssertionError("Expected " + expected + " to be contained in " + t); + } + + throw new AssertionError("toContain can only be used on Strings, Iterables and Arrays"); + } + + /** + * Asserts that the actual value has the expected length. + * This method checks if the actual value is a String, Iterable, or Array, + * and compares their length or size to the given expected length. + * + * @param expected the expected length of the actual value + * @throws AssertionError if the actual value does not have the expected length, + * or if the actual value is not of type String, Iterable, or Array + */ + public void toHaveLength(int expected) { + if (t instanceof String) { + if (((String) t).length() != expected) { + throw new AssertionError("Expected " + expected + " but got " + ((String) t).length()); + } else { + return; + } + } else if (t instanceof Iterable) { + if (((Iterable) t).spliterator().getExactSizeIfKnown() != expected) { + throw new AssertionError("Expected " + expected + " but got " + ((Iterable) t).spliterator().getExactSizeIfKnown()); + } else { + return; + } + } else if (t instanceof Object[]) { + if (((Object[]) t).length != expected) { + throw new AssertionError("Expected " + expected + " but got " + ((Object[]) t).length); + } else { + return; + } + } + + throw new AssertionError("toHaveLength can only be used on Strings"); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/tests/FPTestClass.java b/libraries/common/src/main/java/de/oliver/fancylib/tests/FPTestClass.java new file mode 100644 index 00000000..c9f221a8 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/tests/FPTestClass.java @@ -0,0 +1,147 @@ +package de.oliver.fancylib.tests; + +import de.oliver.fancylib.tests.annotations.FPAfterEach; +import de.oliver.fancylib.tests.annotations.FPBeforeEach; +import de.oliver.fancylib.tests.annotations.FPTest; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.entity.Player; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * FPTestClass is a record that encapsulates information about a test class and its associated test methods. + * This class supports running tests annotated with {@link FPTest}. + * + * @param testClass the test class to run tests for (must be annotated with {@link FPTest}) + * @param beforeEach the method annotated with {@link FPBeforeEach} to run before each test + * @param afterEach the method annotated with {@link FPAfterEach} to run after each test + * @param testMethods the list of test methods annotated with {@link FPTest} + */ +public record FPTestClass( + Class testClass, + Method beforeEach, + Method afterEach, + List testMethods +) { + + private static final Logger logger = Logger.getLogger(FPTestClass.class.getName()); + + /** + * Creates an instance of FPTestClass by inspecting the provided test class for methods annotated + * with FPTest, FPBeforeEach, and FPAfterEach annotations. + * These methods are used to define the setup, teardown, and test methods for the class. + * + * @param testClass the class to be inspected for annotated methods + * @return an instance of FPTestClass containing the test class and its annotated methods + */ + public static FPTestClass fromClass(Class testClass) { + Method beforeEach = null; + Method afterEach = null; + List testMethods = new ArrayList<>(); + + for (Method method : testClass.getDeclaredMethods()) { + if (method.isAnnotationPresent(FPTest.class)) { + if (method.getParameterCount() != 1) continue; + if (method.getParameterTypes()[0] != Player.class) continue; + + testMethods.add(method); + continue; + } + + if (method.isAnnotationPresent(FPBeforeEach.class)) { + if (method.getParameterCount() != 1) continue; + if (method.getParameterTypes()[0] != Player.class) continue; + + beforeEach = method; + continue; + } + + if (method.isAnnotationPresent(FPAfterEach.class)) { + if (method.getParameterCount() != 1) continue; + if (method.getParameterTypes()[0] != Player.class) continue; + + afterEach = method; + } + } + + return new FPTestClass(testClass, beforeEach, afterEach, testMethods); + } + + /** + * Runs the test methods belonging to the test class, performing any necessary setup and teardown operations. + * + * @param player The player context to pass to the test methods. + * @return true if all tests completed successfully, false if any test failed or an unexpected exception occurred. + */ + public boolean runTests(Player player) { + logger.info("Running tests for " + testClass.getSimpleName()); + player.sendMessage(MiniMessage.miniMessage().deserialize("Running tests for " + testClass.getSimpleName())); + + for (Method testMethod : testMethods) { + Object testClassObj; + try { + testClassObj = testClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + logger.warning("Failed to create test class instance: " + e.getMessage()); + return false; + } + + FPTest fpTest = testMethod.getAnnotation(FPTest.class); + if (fpTest.skip()) { + logger.info("Skipping test " + displayName(testMethod)); + player.sendMessage(MiniMessage.miniMessage().deserialize("Skipping test " + displayName(testMethod))); + continue; + } + + + long testStart = System.currentTimeMillis(); + + try { + if (beforeEach != null) beforeEach.invoke(testClassObj, player); + + testMethod.invoke(testClassObj, player); + + if (afterEach != null) afterEach.invoke(testClassObj, player); + } catch (InvocationTargetException e) { + logger.warning("Test " + displayName(testMethod) + " failed with exception: " + e.getCause().getMessage()); + player.sendMessage(MiniMessage.miniMessage().deserialize("Test " + displayName(testMethod) + " failed with exception: " + e.getCause().getMessage())); + return false; + } catch (Exception e) { + logger.warning("Unexpected exception in test " + fpTest.name() + ": " + e.getMessage()); + return false; + } + + long testEnd = System.currentTimeMillis(); + logger.info("Test " + displayName(testMethod) + " took " + (testEnd - testStart) + "ms"); + player.sendMessage(MiniMessage.miniMessage().deserialize("Test " + displayName(testMethod) + " took " + (testEnd - testStart) + "ms")); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + logger.warning("Thread interrupted while waiting between tests: " + e.getMessage()); + } + } + + return true; + } + + /** + * Generates a display name for a given test method, incorporating annotation details if present. + * + * @param m the method for which to generate the display name + * @return a display name that includes the test class and method name, and optionally the value of the FPTest annotation's name attribute if the annotation is present + */ + public String displayName(Method m) { + if (!m.isAnnotationPresent(FPTest.class)) { + return testClass.getSimpleName() + "#" + m.getName(); + } + + FPTest fpTest = m.getAnnotation(FPTest.class); + return testClass.getSimpleName() + "#" + m.getName() + " (" + fpTest.name() + ")"; + } + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPAfterEach.java b/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPAfterEach.java new file mode 100644 index 00000000..e32279b1 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPAfterEach.java @@ -0,0 +1,16 @@ +package de.oliver.fancylib.tests.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a method that should be executed after each test case in a test class. + * This annotation is used to identify methods that perform teardown operations, ensuring + * that the test environment is cleaned up and reset after each individual test method is executed. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface FPAfterEach { +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPBeforeEach.java b/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPBeforeEach.java new file mode 100644 index 00000000..c8ba9f8a --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPBeforeEach.java @@ -0,0 +1,16 @@ +package de.oliver.fancylib.tests.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * FPBeforeEach is a custom annotation designed to be used on methods that should be executed before each test method. + * Methods annotated with FPBeforeEach are typically used to perform setup operations needed before executing each test case. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface FPBeforeEach { +} + diff --git a/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPTest.java b/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPTest.java new file mode 100644 index 00000000..3add29bb --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/tests/annotations/FPTest.java @@ -0,0 +1,31 @@ +package de.oliver.fancylib.tests.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * FPTest is a custom annotation designed to be used on methods for marking them as test cases. + * It helps to identify methods that should be treated as test cases in the testing framework. + * The annotation's attributes allow for providing a human-readable test name and an optional flag to skip the test. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface FPTest { + + /** + * Specifies the name of the test case. This name is used to identify the test case + * in reports, logs, and other contexts where the test case is referenced. + * + * @return the name of the test case + */ + String name(); + + /** + * Indicates whether the annotated test case should be skipped during test execution. + * + * @return true if the test case should be skipped, false otherwise + */ + boolean skip() default false; +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/translations/Language.java b/libraries/common/src/main/java/de/oliver/fancylib/translations/Language.java new file mode 100644 index 00000000..61f74850 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/translations/Language.java @@ -0,0 +1,35 @@ +package de.oliver.fancylib.translations; + +import de.oliver.fancylib.translations.message.Message; + +import java.util.HashMap; +import java.util.Map; + +public class Language { + + private final String languageCode; + private final String languageName; + private final Map messages; + + public Language(String languageCode, String languageName) { + this.languageCode = languageCode; + this.languageName = languageName; + this.messages = new HashMap<>(); + } + + public void addMessage(String key, Message message) { + messages.put(key, message); + } + + public Message getMessage(String key) { + return messages.getOrDefault(key, null); + } + + public String getLanguageCode() { + return languageCode; + } + + public String getLanguageName() { + return languageName; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/translations/TextConfig.java b/libraries/common/src/main/java/de/oliver/fancylib/translations/TextConfig.java new file mode 100644 index 00000000..f9e6756c --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/translations/TextConfig.java @@ -0,0 +1,12 @@ +package de.oliver.fancylib.translations; + +public record TextConfig( + String primaryColor, + String secondaryColor, + String successColor, + String warningColor, + String errorColor, + String prefix +) { + +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/translations/Translator.java b/libraries/common/src/main/java/de/oliver/fancylib/translations/Translator.java new file mode 100644 index 00000000..1504e39b --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/translations/Translator.java @@ -0,0 +1,135 @@ +package de.oliver.fancylib.translations; + +import de.oliver.fancylib.translations.message.Message; +import de.oliver.fancylib.translations.message.MultiMessage; +import de.oliver.fancylib.translations.message.SimpleMessage; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +public class Translator { + + private final TextConfig textConfig; + private final List languages; + private Language selectedLanguage; + private Language fallbackLanguage; + + public Translator(TextConfig textConfig) { + this.textConfig = textConfig; + this.languages = new ArrayList<>(); + } + + public void loadLanguages(String pluginFolderPath) { + languages.clear(); + selectedLanguage = null; + fallbackLanguage = null; + + File langFolder = new File(pluginFolderPath + File.separator + "languages"); + if (!langFolder.exists()) { + if (!langFolder.mkdirs()) { + throw new RuntimeException("Could not create languages folder"); + } + } + + File defaultFile = new File(langFolder, "default.yml"); + try { + InputStream defaultStream = getClass().getResourceAsStream("/languages/default.yml"); + if (defaultStream == null) { + throw new RuntimeException("Could not find default language file"); + } + + // only copy if hash is different + Files.copy(defaultStream, defaultFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException("Could not copy default language file"); + } + + File[] langFiles = langFolder.listFiles(); + if (langFiles == null) { + throw new RuntimeException("Could not list language files"); + } + + for (File langFile : langFiles) { + languages.add(loadLanguageFile(langFile)); + } + + fallbackLanguage = languages.stream() + .filter(language -> language.getLanguageCode().equals("default")) + .findFirst() + .orElse(null); + + if (fallbackLanguage == null) { + throw new RuntimeException("Could not find fallback language"); + } + } + + private Language loadLanguageFile(File langFile) { + String fileName = langFile.getName(); + String languageCode = fileName.substring(0, fileName.lastIndexOf('.')); + + YamlConfiguration lang = YamlConfiguration.loadConfiguration(langFile); + String languageName = lang.getString("language_name", languageCode); + + Language language = new Language(languageCode, languageName); + + + ConfigurationSection messages = lang.getConfigurationSection("messages"); + if (messages == null) { + throw new RuntimeException("Language file " + langFile.getName() + " does not contain a messages section"); + } + + for (String key : messages.getKeys(true)) { + if (messages.isString(key)) { + SimpleMessage message = new SimpleMessage(textConfig, messages.getString(key)); + language.addMessage(key, message); + continue; + } + + if (messages.isList(key)) { + List list = messages.getStringList(key); + language.addMessage(key, new MultiMessage(textConfig, list)); + } + + } + + return language; + } + + public Message translate(String key) { + Message message = selectedLanguage.getMessage(key); + + if (message == null) { + message = fallbackLanguage.getMessage(key); + } + + if (message == null) { + return new SimpleMessage(textConfig, "Missing translation for key " + key); + } + + return message.copy(); + } + + public List getLanguages() { + return languages; + } + + public Language getSelectedLanguage() { + return selectedLanguage; + } + + public Translator setSelectedLanguage(Language selectedLanguage) { + this.selectedLanguage = selectedLanguage; + return this; + } + + public Language getFallbackLanguage() { + return fallbackLanguage; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/translations/message/Message.java b/libraries/common/src/main/java/de/oliver/fancylib/translations/message/Message.java new file mode 100644 index 00000000..ee55ed27 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/translations/message/Message.java @@ -0,0 +1,129 @@ +package de.oliver.fancylib.translations.message; + +import de.oliver.fancylib.translations.TextConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public abstract class Message { + + protected final TextConfig config; + protected TagResolver.Builder tagResolverBuilder = TagResolver.builder(); + + public Message(TextConfig config) { + this.config = config; + } + + public Message addTagResolver(TagResolver resolver) { + tagResolverBuilder = tagResolverBuilder.resolver(resolver); + return this; + } + + protected void applyColorPlaceholders() { + replace("primaryColor", ""); + replace("secondaryColor", ""); + replace("successColor", ""); + replace("warningColor", ""); + replace("errorColor", ""); + } + + /** + * Replaces a placeholder in the message + * + * @param placeholder the placeholder to replace + * @param replacement the replacement + * @return this message + */ + public abstract Message replace(String placeholder, String replacement); + + /** + * Replaces a placeholder in the message, all known tags are stripped from the replacement + * + * @param placeholder the placeholder to replace + * @param replacement the replacement + * @return this message + */ + public abstract Message replaceStripped(String placeholder, String replacement); + + /** + * Adds the prefix to the message + * + * @return this message + */ + public abstract Message withPrefix(); + + /** + * Adds the primary color to the message + * + * @return this message + */ + public abstract Message primary(); + + /** + * Adds the secondary color to the message + * + * @return this message + */ + public abstract Message secondary(); + + /** + * Adds the success color to the message + * + * @return this message + */ + public abstract Message success(); + + /** + * Adds the warning color to the message + * + * @return this message + */ + public abstract Message warning(); + + /** + * Adds the error color to the message + * + * @return this message + */ + public abstract Message error(); + + /** + * Applies custom placeholders to the message + * + * @return this message + */ + public abstract Message applyCustomPlaceholders(); + + /** + * Builds the message as a component + * + * @return the built component + */ + public abstract Component buildComponent(); + + /** + * Copies the message + * + * @return the copied message + */ + public abstract Message copy(); + + public void send(CommandSender receiver) { + Component msg = buildComponent(); + if (Component.empty().equals(msg)) { + return; + } + + receiver.sendMessage(msg); + } + + public void actionbar(Player receiver) { + Component msg = buildComponent(); + if (Component.empty().equals(msg)) { + return; + } + + receiver.sendActionBar(msg); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/translations/message/MultiMessage.java b/libraries/common/src/main/java/de/oliver/fancylib/translations/message/MultiMessage.java new file mode 100644 index 00000000..f1f74f5f --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/translations/message/MultiMessage.java @@ -0,0 +1,120 @@ +package de.oliver.fancylib.translations.message; + +import de.oliver.fancylib.translations.TextConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +import java.util.ArrayList; +import java.util.List; + +public class MultiMessage extends Message { + + private final List messages; + + public MultiMessage(TextConfig config, List messages) { + super(config); + this.messages = new ArrayList<>(messages); + + applyColorPlaceholders(); + } + + @Override + public Message replace(String placeholder, String replacement) { + messages.replaceAll(s -> s + .replace("{" + placeholder + "}", replacement) + .replace("%" + placeholder + "%", replacement)); + + return this; + } + + @Override + public Message replaceStripped(String placeholder, String replacement) { + return replace(placeholder, MiniMessage.miniMessage().stripTags(replacement, tagResolverBuilder.build())); + } + + @Override + public Message withPrefix() { + messages.replaceAll(s -> config.prefix() + s); + return this; + } + + @Override + public Message primary() { + messages.replaceAll(s -> "" + s); + return this; + } + + @Override + public Message secondary() { + messages.replaceAll(s -> "" + s); + return this; + } + + @Override + public Message success() { + messages.replaceAll(s -> "" + s); + return this; + } + + @Override + public Message warning() { + messages.replaceAll(s -> "" + s); + return this; + } + + @Override + public Message error() { + messages.replaceAll(s -> "" + s); + return this; + } + + @Override + public Message applyCustomPlaceholders() { + //TODO: add ChatColorHandler support + return this; + } + + @Override + public Component buildComponent() { + String joined = String.join("\n", messages); + return MiniMessage.miniMessage().deserialize(joined, tagResolverBuilder.build()); + } + + @Override + public Message copy() { + return new MultiMessage(config, messages); + } + + public String build() { + return String.join("\n", messages); + } + + public List getRawMessages() { + return messages; + } + + public List getSimpleMessages() { + List messages = new ArrayList<>(); + for (String s : this.messages) { + messages.add(new SimpleMessage(config, s)); + } + + return messages; + } + + public MultiMessage page(int page, int messagesPerPage) { + List pageMessages = new ArrayList<>(); + int start = (page - 1) * messagesPerPage; + int end = Math.min(start + messagesPerPage, messages.size()); + + for (int i = start; i < end; i++) { + pageMessages.add(messages.get(i)); + } + + return new MultiMessage(config, pageMessages); + } + + public int getPages(int messagesPerPage) { + return (int) Math.ceil((double) messages.size() / messagesPerPage); + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/translations/message/SimpleMessage.java b/libraries/common/src/main/java/de/oliver/fancylib/translations/message/SimpleMessage.java new file mode 100644 index 00000000..96f9b177 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/translations/message/SimpleMessage.java @@ -0,0 +1,97 @@ +package de.oliver.fancylib.translations.message; + +import de.oliver.fancylib.translations.TextConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.entity.Player; + +public class SimpleMessage extends Message { + + private String message; + + public SimpleMessage(TextConfig config, String message) { + super(config); + this.message = message; + + applyColorPlaceholders(); + } + + @Override + public SimpleMessage replace(String placeholder, String replacement) { + message = message + .replace("{" + placeholder + "}", replacement) + .replace("%" + placeholder + "%", replacement); + + return this; + } + + @Override + public Message replaceStripped(String placeholder, String replacement) { + return replace(placeholder, MiniMessage.miniMessage().stripTags(replacement, tagResolverBuilder.build())); + } + + @Override + public SimpleMessage withPrefix() { + message = config.prefix() + message; + return this; + } + + @Override + public SimpleMessage primary() { + message = "" + message; + return this; + } + + @Override + public SimpleMessage secondary() { + message = "" + message; + return this; + } + + @Override + public SimpleMessage success() { + message = "" + message; + return this; + } + + @Override + public SimpleMessage warning() { + message = "" + message; + return this; + } + + @Override + public SimpleMessage error() { + message = "" + message; + return this; + } + + @Override + public SimpleMessage applyCustomPlaceholders() { + // TODO: add ChatColorHandler support + + return this; + } + + @Override + public Component buildComponent() { + return MiniMessage.miniMessage().deserialize(message, tagResolverBuilder.build()); + } + + @Override + public Message copy() { + return new SimpleMessage(config, message); + } + + public String build() { + return message; + } + + public void actionbar(Player receiver) { + receiver.sendActionBar(buildComponent()); + } + + public String getMessage() { + return message; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/HangarVersionFetcher.java b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/HangarVersionFetcher.java new file mode 100644 index 00000000..21ced68a --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/HangarVersionFetcher.java @@ -0,0 +1,32 @@ +package de.oliver.fancylib.versionFetcher; + +import org.apache.maven.artifact.versioning.ComparableVersion; + +public class HangarVersionFetcher implements VersionFetcher{ + + private final String pluginName; + private ComparableVersion newestVersion; + + public HangarVersionFetcher(String pluginName) { + this.pluginName = pluginName; + this.newestVersion = null; + } + + @Override + public ComparableVersion fetchNewestVersion() { + if(newestVersion != null) return newestVersion; + + String versionStr = VersionFetcher.getDataFromUrl("https://hangar.papermc.io/api/v1/projects/" + pluginName + "/latestrelease"); + if(versionStr == null || versionStr.isEmpty()){ + return null; + } + + newestVersion = new ComparableVersion(versionStr); + return newestVersion; + } + + @Override + public String getDownloadUrl() { + return "https://hangar.papermc.io/Oliver/" + pluginName; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/MasterVersionFetcher.java b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/MasterVersionFetcher.java new file mode 100644 index 00000000..5fdd1dcc --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/MasterVersionFetcher.java @@ -0,0 +1,36 @@ +package de.oliver.fancylib.versionFetcher; + +import org.apache.maven.artifact.versioning.ComparableVersion; + +import java.util.LinkedList; + +public class MasterVersionFetcher implements VersionFetcher{ + + private final String pluginName; + private ComparableVersion newestVersion; + private LinkedList fetchers; + + public MasterVersionFetcher(String pluginName) { + this.pluginName = pluginName; + this.fetchers = new LinkedList<>(); + fetchers.push(new ReposiliteVersionFetcher(pluginName)); + fetchers.push(new ModrinthVersionFetcher(pluginName)); + fetchers.push(new HangarVersionFetcher(pluginName)); + } + + @Override + public ComparableVersion fetchNewestVersion() { + for (VersionFetcher fetcher : fetchers) { + ComparableVersion version = fetcher.fetchNewestVersion(); + if(version == null) continue; + newestVersion = version; + return newestVersion; + } + return null; + } + + @Override + public String getDownloadUrl() { + return "https://modrinth.com/plugin/" + pluginName; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ModrinthVersionFetcher.java b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ModrinthVersionFetcher.java new file mode 100644 index 00000000..7c556b7b --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ModrinthVersionFetcher.java @@ -0,0 +1,48 @@ +package de.oliver.fancylib.versionFetcher; + +import com.google.gson.Gson; +import org.apache.maven.artifact.versioning.ComparableVersion; + +import java.util.Map; + +public class ModrinthVersionFetcher implements VersionFetcher { + + private final String pluginName; + private ComparableVersion newestVersion; + + public ModrinthVersionFetcher(String pluginName) { + this.pluginName = pluginName; + this.newestVersion = null; + } + + @Override + public ComparableVersion fetchNewestVersion() { + if (newestVersion != null) return newestVersion; + + String jsonString = de.oliver.fancylib.versionFetcher.VersionFetcher.getDataFromUrl("https://api.modrinth.com/v2/project/" + pluginName.toLowerCase() + "/version"); + if (jsonString == null || jsonString.isEmpty()) { + return null; + } + + Gson gson = new Gson(); + Map[] versions = gson.fromJson(jsonString, Map[].class); + + + for (Map version : versions) { + if (!version.get("version_type").equals("release")) { + continue; + } + + String versionNumber = (String) version.get("version_number"); + newestVersion = new ComparableVersion(versionNumber); + break; + } + + return newestVersion; + } + + @Override + public String getDownloadUrl() { + return "https://modrinth.com/plugin/" + pluginName; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ReposiliteVersionFetcher.java b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ReposiliteVersionFetcher.java new file mode 100644 index 00000000..2502f4e6 --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/ReposiliteVersionFetcher.java @@ -0,0 +1,39 @@ +package de.oliver.fancylib.versionFetcher; + +import com.google.gson.Gson; +import org.apache.maven.artifact.versioning.ComparableVersion; + +import java.util.Map; + +public class ReposiliteVersionFetcher implements VersionFetcher{ + + private final String pluginName; + private ComparableVersion newestVersion; + + public ReposiliteVersionFetcher(String pluginName) { + this.pluginName = pluginName; + this.newestVersion = null; + } + + @Override + public ComparableVersion fetchNewestVersion() { + if (newestVersion != null) return newestVersion; + + String jsonString = VersionFetcher.getDataFromUrl("https://repo.fancyplugins.de/api/maven/latest/version/releases/de/oliver/" + pluginName); + if (jsonString == null || jsonString.isEmpty()) { + return null; + } + + Gson gson = new Gson(); + Map data = gson.fromJson(jsonString, Map.class); + String versionStr = (String) data.get("version"); + + newestVersion = new ComparableVersion(versionStr); + return newestVersion; + } + + @Override + public String getDownloadUrl() { + return "https://modrinth.com/plugin/" + pluginName; + } +} diff --git a/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/VersionFetcher.java b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/VersionFetcher.java new file mode 100644 index 00000000..fe038e4c --- /dev/null +++ b/libraries/common/src/main/java/de/oliver/fancylib/versionFetcher/VersionFetcher.java @@ -0,0 +1,31 @@ +package de.oliver.fancylib.versionFetcher; + +import org.apache.maven.artifact.versioning.ComparableVersion; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +public interface VersionFetcher { + + ComparableVersion fetchNewestVersion(); + String getDownloadUrl(); + + static String getDataFromUrl(String urlString) { + try { + URL url = new URL(urlString); + URLConnection connection = url.openConnection(); + + connection.setConnectTimeout(300); + Scanner scanner = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8).useDelimiter("\\A"); + + return scanner.hasNext() ? scanner.next() : ""; + } catch (IOException e) { + e.printStackTrace(); + } + + return ""; + } +} diff --git a/libraries/common/src/test/java/de/oliver/fancylib/jdb/JDBTest.java b/libraries/common/src/test/java/de/oliver/fancylib/jdb/JDBTest.java new file mode 100644 index 00000000..32a0034e --- /dev/null +++ b/libraries/common/src/test/java/de/oliver/fancylib/jdb/JDBTest.java @@ -0,0 +1,232 @@ +package de.oliver.fancylib.jdb; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class JDBTest { + + //This class tests the get operation of the JDB class, + //which is supposed to retrieve and deserialize a JSON document + //from a given path in the file system. + + private final String basePath = "./test_files/"; + + public static void cleanUpDirectory(String path) throws IOException { + Path directory = Paths.get(path); + if (Files.exists(directory)) { + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + } + + @BeforeEach + void setUp() throws IOException { + cleanUpDirectory(basePath); + } + + @AfterEach + void tearDown() throws IOException { + cleanUpDirectory(basePath); + } + + @Test + public void testGetObject() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "test_file"; + jdb.set(path, "Test message"); + + // Act + String result = jdb.get(path, String.class); + + // Assert + assertEquals("Test message", result); + } + + @Test + public void testGetObjectNonExisting() throws IOException { + // Prepare + JDB jdb = new JDB("./test_files/"); + String path = "does_not_exist"; + + // Act + Object result = jdb.get(path, Object.class); + + // Assert + assertNull(result); + } + + @Test + public void testGetAllObjects() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "test_files"; + jdb.set(path + "/obj1", "Test message 1"); + jdb.set(path + "/obj2", "Test message 2"); + jdb.set(path + "/obj3", "Test message 3"); + + // Act + List result = jdb.getAll(path, String.class); + + // Assert + assertEquals(3, result.size()); + assertTrue(result.contains("Test message 1")); + assertTrue(result.contains("Test message 2")); + assertTrue(result.contains("Test message 3")); + } + + @Test + public void testGetAllObjectsNonExisting() throws IOException { + // Prepare + JDB jdb = new JDB("./test_files/"); + String path = "does_not_exist"; + + // Act + List result = jdb.getAll(path, Object.class); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + public void testSetNewObject() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "new_object"; + String value = "New message"; + + // Act + jdb.set(path, value); + String result = jdb.get(path, String.class); + + // Assert + assertEquals(value, result); + } + + @Test + public void testSetExistingObject() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "existing_object"; + String value = "Existing message"; + jdb.set(path, "Old message"); + + // Act + jdb.set(path, value); + String result = jdb.get(path, String.class); + + // Assert + assertEquals(value, result); + } + + @Test + public void testSetObjectNull() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "null_object"; + + // Act + jdb.set(path, null); + String result = jdb.get(path, String.class); + + // Assert + assertNull(result); + } + + @Test + public void testDeleteWhenFileExists() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "existing_file"; + String value = "Test message"; + jdb.set(path, value); + + // Act + jdb.delete(path); + String result = jdb.get(path, String.class); + + // Assert + assertNull(result); + + File file = new File(basePath + path + ".json"); + assertFalse(file.exists()); + } + + @Test + public void testDeleteWhenFileNotExists() { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "non_existing_file"; + + // Act + jdb.delete(path); + + // Assert + File file = new File(basePath + path + ".json"); + assertFalse(file.exists()); + } + + // The getDocument method in the JDB class is supposed to retrieve and deserialize a JSON document(but encapsulated in a JDocument) from a given path in the file system. + // Testing getDocument method when the file exists + @Test + public void testGetDocumentWhenFileExists() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "existing_file"; + TestObject value = new TestObject("Test message"); + jdb.set(path, value); + + // Act + JDocument document = jdb.getDocument(path); + + // Assert + assertNotNull(document); + assertEquals(value.message(), document.getString("message")); + } + + // Testing the getDocument method when the file does not exist + @Test + public void testGetDocumentWhenFileDoesNotExist() throws IOException { + // Prepare + String basePath = "./test_files/"; + JDB jdb = new JDB(basePath); + String path = "non_existing_file"; + + // Act + JDocument document = jdb.getDocument(path); + + // Assert + assertNull(document); + } + + record TestObject(String message) { + } +} diff --git a/libraries/common/src/test/java/de/oliver/fancylib/jdb/JDocumentTest.java b/libraries/common/src/test/java/de/oliver/fancylib/jdb/JDocumentTest.java new file mode 100644 index 00000000..a9bacccf --- /dev/null +++ b/libraries/common/src/test/java/de/oliver/fancylib/jdb/JDocumentTest.java @@ -0,0 +1,619 @@ +package de.oliver.fancylib.jdb; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class JDocumentTest { + + /** + * The JDocumentTest class contains unit tests for the JDocument class. + * The get method in the JDocument class is being tested here. + */ + + @Test + public void testGet_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", "value1"); + JDocument jDocument = new JDocument(data); + + Object result = jDocument.get("key1"); + + assertNotNull(result); + assertEquals("value1", result.toString()); + } + + @Test + public void testGet_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + Object result = jDocument.get("key1"); + + assertNull(result); + } + + @Test + public void testGet_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + Object result = jDocument.get("key1.key2"); + + assertNotNull(result); + assertEquals("value2", result.toString()); + } + + @Test + public void testGet_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + Object result = jDocument.get("key1.key3"); + + assertNull(result); + } + + /** + * The contains method in the JDocument class is being tested here. + * It checks whether a given key is present in the JDocument's data or not. + */ + + @Test + public void testContains_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", "value1"); + JDocument jDocument = new JDocument(data); + + boolean result = jDocument.contains("key1"); + + assertTrue(result); + } + + @Test + public void testContains_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + boolean result = jDocument.contains("key1"); + + assertFalse(result); + } + + @Test + public void testContains_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + boolean result = jDocument.contains("key1.key2"); + + assertTrue(result); + } + + @Test + public void testContains_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + boolean result = jDocument.contains("key1.key3"); + + assertFalse(result); + } + + /** + * The getKeys method in the JDocument class is being tested here. + * It retrieves the keys of the nested Map present within the data. + */ + + @Test + public void testGetKeys_Success_SingleKey() { + Map innerData = new HashMap<>(); + innerData.put("innerKey1", "value1"); + + Map data = new HashMap<>(); + data.put("key1", innerData); + + JDocument jDocument = new JDocument(data); + + Set keys = jDocument.getKeys("key1"); + + assertNotNull(keys); + assertEquals(1, keys.size()); + assertTrue(keys.contains("innerKey1")); + } + + @Test + public void testGetKeys_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + Set keys = jDocument.getKeys("key1"); + + assertNotNull(keys); + assertTrue(keys.isEmpty()); + } + + @Test + public void testGetKeys_Success_MultipleKeys() { + Map innerData = new HashMap<>(); + innerData.put("innerKey1", "value1"); + innerData.put("innerKey2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", innerData); + + JDocument jDocument = new JDocument(data); + + Set keys = jDocument.getKeys("key1"); + + assertNotNull(keys); + assertEquals(2, keys.size()); + assertTrue(keys.contains("innerKey1")); + assertTrue(keys.contains("innerKey2")); + } + + /** + * The getString method in the JDocument class is being tested here. + * It returns a String value of the specified key from the JDocument's data. + */ + + @Test + public void testGetString_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", "value1"); + JDocument jDocument = new JDocument(data); + + String result = jDocument.getString("key1"); + + assertNotNull(result); + assertEquals("value1", result); + } + + @Test + public void testGetString_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + String result = jDocument.getString("key1"); + + assertNotNull(result); + assertEquals("", result); + } + + @Test + public void testGetString_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + String result = jDocument.getString("key1.key2"); + + assertNotNull(result); + assertEquals("value2", result); + } + + @Test + public void testGetString_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", "value2"); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + String result = jDocument.getString("key1.key3"); + + assertNotNull(result); + assertEquals("", result); + } + + /** + * The getBoolean method in the JDocument class is being tested here. + * It retrieves a boolean value of the specified key from the JDocument's data. + */ + + @Test + public void testGetBoolean_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", true); + JDocument jDocument = new JDocument(data); + + boolean result = jDocument.getBoolean("key1"); + + assertTrue(result); + } + + @Test + public void testGetBoolean_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + boolean result = jDocument.getBoolean("key1"); + + assertFalse(result); + } + + @Test + public void testGetBoolean_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", true); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + boolean result = jDocument.getBoolean("key1.key2"); + + assertTrue(result); + } + + @Test + public void testGetBoolean_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", true); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + boolean result = jDocument.getBoolean("key1.key3"); + + assertFalse(result); + } + + /** + * The getByte method in the JDocument class is being tested here. + * It retrieves a byte value of the specified key from the JDocument's data. + */ + + @Test + public void testGetByte_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", (byte) 1); + JDocument jDocument = new JDocument(data); + + byte result = jDocument.getByte("key1"); + + assertEquals((byte) 1, result); + } + + @Test + public void testGetByte_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + byte result = jDocument.getByte("key1"); + + assertEquals((byte) 0, result); + } + + @Test + public void testGetByte_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", (byte) 2); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + byte result = jDocument.getByte("key1.key2"); + + assertEquals((byte) 2, result); + } + + @Test + public void testGetByte_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", (byte) 2); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + byte result = jDocument.getByte("key1.key3"); + + assertEquals((byte) 0, result); + } + + /** + * The getShort method in the JDocument class is being tested here. + * It retrieves a short value of the specified key from the JDocument's data. + */ + + @Test + public void testGetShort_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", (short) 1); + JDocument jDocument = new JDocument(data); + + short result = jDocument.getShort("key1"); + + assertEquals((short) 1, result); + } + + @Test + public void testGetShort_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + short result = jDocument.getShort("key1"); + + assertEquals((short) 0, result); + } + + @Test + public void testGetShort_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", (short) 2); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + short result = jDocument.getShort("key1.key2"); + + assertEquals((short) 2, result); + } + + @Test + public void testGetShort_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", (short) 2); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + short result = jDocument.getShort("key1.key3"); + + assertEquals((short) 0, result); + } + + /** + * The getInt method in the JDocument class is being tested here. + * It retrieves an integer value of the specified key from the JDocument's data. + */ + + @Test + public void testGetInt_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", 123); + JDocument jDocument = new JDocument(data); + + int result = jDocument.getInt("key1"); + + assertEquals(123, result); + } + + @Test + public void testGetInt_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + int result = jDocument.getInt("key1"); + + assertEquals(0, result); + } + + @Test + public void testGetInt_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 456); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + int result = jDocument.getInt("key1.key2"); + + assertEquals(456, result); + } + + @Test + public void testGetInt_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 456); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + int result = jDocument.getInt("key1.key3"); + + assertEquals(0, result); + } + + /** + * The getLong method in the JDocument class is being tested here. + * It retrieves a long value of the specified key from the JDocument's data. + */ + + @Test + public void testGetLong_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", 123L); + JDocument jDocument = new JDocument(data); + + long result = jDocument.getLong("key1"); + + assertEquals(123L, result); + } + + @Test + public void testGetLong_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + long result = jDocument.getLong("key1"); + + assertEquals(0L, result); + } + + @Test + public void testGetLong_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 456L); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + long result = jDocument.getLong("key1.key2"); + + assertEquals(456L, result); + } + + @Test + public void testGetLong_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 456L); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + long result = jDocument.getLong("key1.key3"); + + assertEquals(0L, result); + } + + /** + * The getFloat method in the JDocument class is being tested here. + * It retrieves a float value of the specified key from the JDocument's data. + */ + @Test + public void testGetFloat_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", 1.23f); + JDocument jDocument = new JDocument(data); + + float result = jDocument.getFloat("key1"); + + assertEquals(1.23f, result); + } + + @Test + public void testGetFloat_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + float result = jDocument.getFloat("key1"); + + assertEquals(0f, result); + } + + @Test + public void testGetFloat_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 4.56f); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + float result = jDocument.getFloat("key1.key2"); + + assertEquals(4.56f, result); + } + + @Test + public void testGetFloat_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 4.56f); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + float result = jDocument.getFloat("key1.key3"); + + assertEquals(0f, result); + } + + /** + * The getDouble method in the JDocument class is being tested here. + * It retrieves a double value of the specified key from the JDocument's data. + */ + + @Test + public void testGetDouble_Success_SingleKey() { + Map data = new HashMap<>(); + data.put("key1", 1.23d); + JDocument jDocument = new JDocument(data); + + double result = jDocument.getDouble("key1"); + + assertEquals(1.23d, result); + } + + @Test + public void testGetDouble_Failure_KeyNotFound() { + JDocument jDocument = new JDocument(Collections.emptyMap()); + + double result = jDocument.getDouble("key1"); + + assertEquals(0d, result); + } + + @Test + public void testGetDouble_Success_NestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 4.56d); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + double result = jDocument.getDouble("key1.key2"); + + assertEquals(4.56d, result); + } + + @Test + public void testGetDouble_Failure_NonExistentNestedKey() { + Map nestedData = new HashMap<>(); + nestedData.put("key2", 4.56d); + + Map data = new HashMap<>(); + data.put("key1", nestedData); + + JDocument jDocument = new JDocument(data); + + double result = jDocument.getDouble("key1.key3"); + + assertEquals(0d, result); + } +} diff --git a/libraries/packets/.gitignore b/libraries/packets/.gitignore new file mode 100644 index 00000000..b7642c01 --- /dev/null +++ b/libraries/packets/.gitignore @@ -0,0 +1,46 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +## run paper +**/run/ + +### IntelliJ IDEA ### +.idea/ +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/libraries/packets/README.md b/libraries/packets/README.md new file mode 100644 index 00000000..d1fac8a0 --- /dev/null +++ b/libraries/packets/README.md @@ -0,0 +1,58 @@ +![](fancysitula_title.png) + +## Using Minecraft internals made easier. + +Initially developed for FancyNpcs and FancyHolograms, FancySitula is a library that simplifies the process of +interacting with Minecraft internals. It provides a simple and easy to use API to interact with Minecraft packets and +entities. + +## Features + +- Easy to use API for creating and sending packets +- Powerful factory classes for creating packets +- Wrapper classes for Minecraft packets and entities +- Support for multiple Minecraft versions +- No third-party dependencies and works out of the box + +## Supported packets + +The following packets are currently supported: + +- ClientboundAddEntityPacket +- ClientboundPlayerInfoRemovePacket +- ClientboundPlayerInfoUpdatePacket +- ClientboundRemoveEntitiesPacket +- ClientboundRotateHeadPacket +- ClientboundSetEntityDataPacket +- ClientboundSetEquipmentPacket +- ClientboundTeleportEntityPacket + +More packets will be added when needed / requested (contributions are welcome). + +## Supported Minecraft versions + +FancySitula will support the latest Minecraft version and additional older versions. The following versions are +supported: + +- [x] 1.21.1 +- [x] 1.21 +- [x] 1.20.6 +- [x] 1.20.5 +- [ ] 1.20.4 +- [ ] 1.20.2 +- [ ] 1.20.1 + +## Missing Packets for FancyNpcs + +- [x] ClientboundPlayerInfoUpdatePacket +- [x] ClientboundAddEntityPacket +- [x] ClientboundPlayerInfoRemovePacket +- [x] ClientboundRemoveEntitiesPacket +- [x] ClientboundTeleportEntityPacket +- [x] ClientboundRotateHeadPacket +- [x] ClientboundSetEquipmentPacket +- [x] ClientboundSetEntityDataPacket +- [x] ClientboundSetPlayerTeamPacket +- [ ] ClientboundUpdateAttributesPacket +- [ ] ClientboundAnimatePacket +- [ ] ClientboundSetPassengersPacket \ No newline at end of file diff --git a/libraries/packets/api/build.gradle.kts b/libraries/packets/api/build.gradle.kts new file mode 100644 index 00000000..b92d2d31 --- /dev/null +++ b/libraries/packets/api/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("java-library") +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + compileOnly("de.oliver.FancyAnalytics:logger:0.0.4") +} + +tasks { + java { + withSourcesJar() + withJavadocJar() + } + + javadoc { + options.encoding = Charsets.UTF_8.name() + } + + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release = 21 + + } +} \ No newline at end of file diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/IFancySitula.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/IFancySitula.java new file mode 100644 index 00000000..55eff40f --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/IFancySitula.java @@ -0,0 +1,7 @@ +package de.oliver.fancysitula.api; + +import de.oliver.fancyanalytics.logger.ExtendedFancyLogger; + +public interface IFancySitula { + ExtendedFancyLogger LOGGER = new ExtendedFancyLogger("FancySitula"); +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_BlockDisplay.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_BlockDisplay.java new file mode 100644 index 00000000..9f139f4c --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_BlockDisplay.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_BlockDisplayData; +import org.bukkit.entity.EntityType; + +import java.util.List; + +public class FS_BlockDisplay extends FS_Display { + + protected FS_ClientboundSetEntityDataPacket.EntityData blockData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_BlockDisplayData.BLOCK, null); + + public FS_BlockDisplay() { + super(EntityType.BLOCK_DISPLAY); + } + + public org.bukkit.block.BlockState getBlock() { + return (org.bukkit.block.BlockState) this.blockData.getValue(); + } + + public void setBlock(org.bukkit.block.BlockState block) { + this.blockData.setValue(block); + } + + @Override + public List getEntityData() { + List entityData = super.getEntityData(); + entityData.add(this.blockData); + return entityData; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Display.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Display.java new file mode 100644 index 00000000..870cf887 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Display.java @@ -0,0 +1,214 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_DisplayData; +import org.bukkit.entity.EntityType; + +import java.util.List; + +public class FS_Display extends FS_Entity { + + protected FS_ClientboundSetEntityDataPacket.EntityData transformationInterpolationStartDeltaTicksData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS, null); + protected FS_ClientboundSetEntityDataPacket.EntityData transformationInterpolationDurationData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.TRANSFORMATION_INTERPOLATION_DURATION, null); + protected FS_ClientboundSetEntityDataPacket.EntityData posRotInterpolationDurationData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.POS_ROT_INTERPOLATION_DURATION, null); + protected FS_ClientboundSetEntityDataPacket.EntityData translationData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.TRANSLATION, null); + protected FS_ClientboundSetEntityDataPacket.EntityData scaleData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.SCALE, null); + protected FS_ClientboundSetEntityDataPacket.EntityData leftRotationData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.LEFT_ROTATION, null); + protected FS_ClientboundSetEntityDataPacket.EntityData rightRotationData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.RIGHT_ROTATION, null); + protected FS_ClientboundSetEntityDataPacket.EntityData billboardRenderConstraintsData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.BILLBOARD_RENDER_CONSTRAINTS, null); + protected FS_ClientboundSetEntityDataPacket.EntityData brightnessOverrideData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.BRIGHTNESS_OVERRIDE, null); + protected FS_ClientboundSetEntityDataPacket.EntityData viewRangeData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.VIEW_RANGE, null); + protected FS_ClientboundSetEntityDataPacket.EntityData shadowRadiusData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.SHADOW_RADIUS, null); + protected FS_ClientboundSetEntityDataPacket.EntityData shadowStrengthData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.SHADOW_STRENGTH, null); + protected FS_ClientboundSetEntityDataPacket.EntityData widthData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.WIDTH, null); + protected FS_ClientboundSetEntityDataPacket.EntityData heightData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.HEIGHT, null); + protected FS_ClientboundSetEntityDataPacket.EntityData glowColorOverrideData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_DisplayData.GLOW_COLOR_OVERRIDE, null); + + public FS_Display(EntityType type) { + super(type); + } + + public int getTransformationInterpolationStartDeltaTicks() { + return (int) this.transformationInterpolationStartDeltaTicksData.getValue(); + } + + public void setTransformationInterpolationStartDeltaTicks(int transformationInterpolationStartDeltaTicks) { + this.transformationInterpolationStartDeltaTicksData.setValue(transformationInterpolationStartDeltaTicks); + } + + public int getTransformationInterpolationDuration() { + return (int) this.transformationInterpolationDurationData.getValue(); + } + + public void setTransformationInterpolationDuration(int transformationInterpolationDuration) { + this.transformationInterpolationDurationData.setValue(transformationInterpolationDuration); + } + + public int getPosRotInterpolationDuration() { + return (int) this.posRotInterpolationDurationData.getValue(); + } + + public void setPosRotInterpolationDuration(int posRotInterpolationDuration) { + this.posRotInterpolationDurationData.setValue(posRotInterpolationDuration); + } + + public org.joml.Vector3f getTranslation() { + return (org.joml.Vector3f) this.translationData.getValue(); + } + + public void setTranslation(org.joml.Vector3f translation) { + this.translationData.setValue(translation); + } + + public org.joml.Vector3f getScale() { + return (org.joml.Vector3f) this.scaleData.getValue(); + } + + public void setScale(org.joml.Vector3f scale) { + this.scaleData.setValue(scale); + } + + public org.joml.Quaternionf getLeftRotation() { + return (org.joml.Quaternionf) this.leftRotationData.getValue(); + } + + public void setLeftRotation(org.joml.Quaternionf leftRotation) { + this.leftRotationData.setValue(leftRotation); + } + + public org.joml.Quaternionf getRightRotation() { + return (org.joml.Quaternionf) this.rightRotationData.getValue(); + } + + public void setRightRotation(org.joml.Quaternionf rightRotation) { + this.rightRotationData.setValue(rightRotation); + } + + public byte getBillboardRenderConstraints() { + return (byte) this.billboardRenderConstraintsData.getValue(); + } + + public void setBillboardRenderConstraints(byte billboardRenderConstraints) { + this.billboardRenderConstraintsData.setValue(billboardRenderConstraints); + } + + public Billboard getBillboard() { + return Billboard.getById(getBillboardRenderConstraints()); + } + + public void setBillboard(Billboard billboard) { + this.billboardRenderConstraintsData.setValue(billboard.getId()); + } + + public int getBrightnessOverride() { + return (int) this.brightnessOverrideData.getValue(); + } + + public void setBrightnessOverride(int brightnessOverride) { + this.brightnessOverrideData.setValue(brightnessOverride); + } + + public float getViewRange() { + return (float) this.viewRangeData.getValue(); + } + + public void setViewRange(float viewRange) { + this.viewRangeData.setValue(viewRange); + } + + public float getShadowRadius() { + return (float) this.shadowRadiusData.getValue(); + } + + public void setShadowRadius(float shadowRadius) { + this.shadowRadiusData.setValue(shadowRadius); + } + + public float getShadowStrength() { + return (float) this.shadowStrengthData.getValue(); + } + + public void setShadowStrength(float shadowStrength) { + this.shadowStrengthData.setValue(shadowStrength); + } + + public int getWidth() { + return (int) this.widthData.getValue(); + } + + public void setWidth(int width) { + this.widthData.setValue(width); + } + + public int getHeight() { + return (int) this.heightData.getValue(); + } + + public void setHeight(int height) { + this.heightData.setValue(height); + } + + public int getGlowColorOverride() { + return (int) this.glowColorOverrideData.getValue(); + } + + public void setGlowColorOverride(int glowColorOverride) { + this.glowColorOverrideData.setValue(glowColorOverride); + } + + @Override + public List getEntityData() { + List entityData = super.getEntityData(); + + entityData.add(this.transformationInterpolationStartDeltaTicksData); + entityData.add(this.transformationInterpolationDurationData); + entityData.add(this.posRotInterpolationDurationData); + entityData.add(this.translationData); + entityData.add(this.scaleData); + entityData.add(this.leftRotationData); + entityData.add(this.rightRotationData); + entityData.add(this.billboardRenderConstraintsData); + entityData.add(this.brightnessOverrideData); + entityData.add(this.viewRangeData); + entityData.add(this.shadowRadiusData); + entityData.add(this.shadowStrengthData); + entityData.add(this.widthData); + entityData.add(this.heightData); + entityData.add(this.glowColorOverrideData); + return entityData; + } + + public enum Billboard { + FIXED((byte) 0, "fixed"), + VERTICAL((byte) 1, "vertical"), + HORIZONTAL((byte) 2, "horizontal"), + CENTER((byte) 3, "center"), + ; + + private final byte id; + private final String name; + + Billboard(byte id, String name) { + this.id = id; + this.name = name; + } + + public static Billboard getById(byte id) { + for (Billboard value : values()) { + if (value.getId() == id) { + return value; + } + } + return null; + } + + public byte getId() { + return id; + } + + public String getName() { + return name; + } + } + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Entity.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Entity.java new file mode 100644 index 00000000..c29ba2af --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Entity.java @@ -0,0 +1,246 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_EntityData; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.entity.EntityType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class FS_Entity { + + private static int entityCount = Integer.MAX_VALUE / 2; + + protected int id = entityCount++; + protected UUID uuid = UUID.randomUUID(); + protected EntityType type; + + protected double x = 0; + protected double y = 0; + protected double z = 0; + + protected float yaw = 0; + protected float pitch = 0; + protected float headYaw = 0; + + protected int velocityX = 0; + protected int velocityY = 0; + protected int velocityZ = 0; + + protected int data = 0; + + protected FS_ClientboundSetEntityDataPacket.EntityData sharedFlagsData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.SHARED_FLAGS, null); + protected FS_ClientboundSetEntityDataPacket.EntityData airSupplyData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.AIR_SUPPLY, null); + protected FS_ClientboundSetEntityDataPacket.EntityData customNameData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.CUSTOM_NAME, null); + protected FS_ClientboundSetEntityDataPacket.EntityData customNameVisibleData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.CUSTOM_NAME_VISIBLE, null); + protected FS_ClientboundSetEntityDataPacket.EntityData silentData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.SILENT, null); + protected FS_ClientboundSetEntityDataPacket.EntityData noGravityData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.NO_GRAVITY, null); + protected FS_ClientboundSetEntityDataPacket.EntityData ticksFrozenData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_EntityData.TICKS_FROZEN, null); + + public FS_Entity(EntityType type) { + this.type = type; + } + + // Entity data + + public byte getSharedFlags() { + return (byte) sharedFlagsData.getValue(); + } + + public void setSharedFlags(byte sharedFlags) { + this.sharedFlagsData.setValue(sharedFlags); + } + + public byte getAirSupply() { + return (byte) airSupplyData.getValue(); + } + + public void setAirSupply(byte airSupply) { + this.airSupplyData.setValue(airSupply); + } + + public Optional getCustomName() { + return (Optional) customNameData.getValue(); + } + + public void setCustomName(Optional customName) { + this.customNameData.setValue(customName); + } + + public boolean getCustomNameVisible() { + return (boolean) customNameVisibleData.getValue(); + } + + public void setCustomNameVisible(boolean customNameVisible) { + this.customNameVisibleData.setValue(customNameVisible); + } + + public boolean getSilent() { + return (boolean) silentData.getValue(); + } + + public void setSilent(boolean silent) { + this.silentData.setValue(silent); + } + + public boolean getNoGravity() { + return (boolean) noGravityData.getValue(); + } + + public void setNoGravity(boolean noGravity) { + this.noGravityData.setValue(noGravity); + } + + public int getTicksFrozen() { + return (int) ticksFrozenData.getValue(); + } + + public void setTicksFrozen(int ticksFrozen) { + this.ticksFrozenData.setValue(ticksFrozen); + } + + public List getEntityData() { + List entityData = new ArrayList<>(); + + entityData.add(this.sharedFlagsData); + entityData.add(this.airSupplyData); + entityData.add(this.customNameData); + entityData.add(this.customNameVisibleData); + entityData.add(this.silentData); + entityData.add(this.noGravityData); + entityData.add(this.ticksFrozenData); + return entityData; + } + + // Getter and Setter for all fields + + public void setLocation(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void setLocation(Location location) { + setLocation(location.getX(), location.getY(), location.getZ()); + setRotation(location.getYaw(), location.getPitch()); + } + + public void setRotation(float yaw, float pitch) { + this.yaw = yaw; + this.pitch = pitch; + } + + public void setVelocity(int velocityX, int velocityY, int velocityZ) { + this.velocityX = velocityX; + this.velocityY = velocityY; + this.velocityZ = velocityZ; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + public EntityType getType() { + return type; + } + + public void setType(EntityType type) { + this.type = type; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public float getPitch() { + return pitch; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + public float getHeadYaw() { + return headYaw; + } + + public void setHeadYaw(float headYaw) { + this.headYaw = headYaw; + } + + public int getVelocityX() { + return velocityX; + } + + public void setVelocityX(int velocityX) { + this.velocityX = velocityX; + } + + public int getVelocityY() { + return velocityY; + } + + public void setVelocityY(int velocityY) { + this.velocityY = velocityY; + } + + public int getVelocityZ() { + return velocityZ; + } + + public void setVelocityZ(int velocityZ) { + this.velocityZ = velocityZ; + } + + public int getData() { + return data; + } + + public void setData(int data) { + this.data = data; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_ItemDisplay.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_ItemDisplay.java new file mode 100644 index 00000000..ba1ebae9 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_ItemDisplay.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_ItemDisplayData; +import org.bukkit.entity.EntityType; + +import java.util.List; + +public class FS_ItemDisplay extends FS_Display { + + protected FS_ClientboundSetEntityDataPacket.EntityData itemData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_ItemDisplayData.ITEM, null); + + public FS_ItemDisplay() { + super(EntityType.ITEM_DISPLAY); + } + + public org.bukkit.inventory.ItemStack getItem() { + return (org.bukkit.inventory.ItemStack) this.itemData.getValue(); + } + + public void setItem(org.bukkit.inventory.ItemStack item) { + this.itemData.setValue(item); + } + + @Override + public List getEntityData() { + List entityData = super.getEntityData(); + entityData.add(this.itemData); + return entityData; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Player.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Player.java new file mode 100644 index 00000000..4eff544d --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_Player.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class FS_Player extends FS_Entity { + + protected Map equipment; + + public FS_Player() { + super(EntityType.PLAYER); + + this.equipment = new ConcurrentHashMap<>(); + } + + public Map getEquipment() { + return equipment; + } + + public void setEquipment(Map equipment) { + this.equipment = equipment; + } + + public void setEquipment(FS_EquipmentSlot slot, ItemStack item) { + this.equipment.put(slot, item); + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_RealPlayer.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_RealPlayer.java new file mode 100644 index 00000000..3ced4551 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_RealPlayer.java @@ -0,0 +1,40 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.packets.FS_ClientboundPacket; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a real player + */ +public class FS_RealPlayer { + + @NotNull + private final Player bukkitPlayer; + + /** + * Creates a new FS_RealPlayer instance + * Must have a real bukkit player instance + * Used for sending packets to the player + * + * @param bukkitPlayer the bukkit player instance + */ + public FS_RealPlayer(@NotNull Player bukkitPlayer) { + this.bukkitPlayer = bukkitPlayer; + } + + /** + * Sends a packet to the player + * Must have a real bukkit player instance + * + * @param packet the packet to send + */ + public void sendPacket(FS_ClientboundPacket packet) { + packet.send(this); + } + + public @NotNull Player getBukkitPlayer() { + return bukkitPlayer; + } + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_TextDisplay.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_TextDisplay.java new file mode 100644 index 00000000..af710a41 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/entities/FS_TextDisplay.java @@ -0,0 +1,123 @@ +package de.oliver.fancysitula.api.entities; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_TextDisplayData; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.EntityType; + +import java.util.List; + +public class FS_TextDisplay extends FS_Display { + + protected FS_ClientboundSetEntityDataPacket.EntityData textData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_TextDisplayData.TEXT, null); + protected FS_ClientboundSetEntityDataPacket.EntityData lineWidthData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_TextDisplayData.LINE_WIDTH, null); + protected FS_ClientboundSetEntityDataPacket.EntityData backgroundData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_TextDisplayData.BACKGROUND, null); + protected FS_ClientboundSetEntityDataPacket.EntityData textOpacityData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_TextDisplayData.TEXT_OPACITY, null); + protected FS_ClientboundSetEntityDataPacket.EntityData styleFlagsData = new FS_ClientboundSetEntityDataPacket.EntityData(FS_TextDisplayData.STYLE_FLAGS, null); + + public FS_TextDisplay() { + super(EntityType.TEXT_DISPLAY); + } + + public Component getText() { + return (Component) this.textData.getValue(); + } + + public void setText(Component text) { + this.textData.setValue(text); + } + + public int getLineWidth() { + return (int) this.lineWidthData.getValue(); + } + + public void setLineWidth(int lineWidth) { + this.lineWidthData.setValue(lineWidth); + } + + public int getBackground() { + return (int) this.backgroundData.getValue(); + } + + public void setBackground(int background) { + this.backgroundData.setValue(background); + } + + public byte getTextOpacity() { + return (byte) this.textOpacityData.getValue(); + } + + public void setTextOpacity(byte textOpacity) { + this.textOpacityData.setValue(textOpacity); + } + + public byte getStyleFlags() { + return (byte) this.styleFlagsData.getValue(); + } + + public void setStyleFlags(byte styleFlags) { + this.styleFlagsData.setValue(styleFlags); + } + + public void setStyleFlag(byte flag, boolean value) { + byte styleFlags = getStyleFlags(); + if (value) { + this.styleFlagsData.setValue((byte) (styleFlags | flag)); + } else { + this.styleFlagsData.setValue((byte) (styleFlags & ~flag)); + } + } + + public boolean hasStyleFlag(byte flag) { + return (getStyleFlags() & flag) == flag; + } + + public void setShadow(boolean shadow) { + setStyleFlag((byte) 1, shadow); + } + + public boolean hasShadow() { + return hasStyleFlag((byte) 1); + } + + public boolean isSeeThrough() { + return hasStyleFlag((byte) 2); + } + + public void setSeeThrough(boolean seeThrough) { + setStyleFlag((byte) 2, seeThrough); + } + + public void setUseDefaultBackground(boolean defaultBackground) { + setStyleFlag((byte) 4, defaultBackground); + } + + public boolean isUsingDefaultBackground() { + return hasStyleFlag((byte) 4); + } + + public boolean isAlignLeft() { + return hasStyleFlag((byte) 8); + } + + public void setAlignLeft(boolean alignLeft) { + setStyleFlag((byte) 8, alignLeft); + } + + public void setAlignRight(boolean alignRight) { + setStyleFlag((byte) 16, alignRight); + } + + @Override + public List getEntityData() { + List entityData = super.getEntityData(); + + entityData.add(this.textData); + entityData.add(this.lineWidthData); + entityData.add(this.backgroundData); + entityData.add(this.textOpacityData); + entityData.add(this.styleFlagsData); + return entityData; + } +} + diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundAddEntityPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundAddEntityPacket.java new file mode 100644 index 00000000..ef9ac54e --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundAddEntityPacket.java @@ -0,0 +1,166 @@ +package de.oliver.fancysitula.api.packets; + +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +/** + * Used to add an entity to the client's world. + */ +public abstract class FS_ClientboundAddEntityPacket extends FS_ClientboundPacket { + + protected int entityId; + protected UUID entityUUID; + protected EntityType entityType; + + protected double x; + protected double y; + protected double z; + + protected float yaw; + protected float pitch; + protected float headYaw; + + protected int velocityX; + protected int velocityY; + protected int velocityZ; + + protected int data; + + /** + * @param pitch in degrees (0 - 360) + * @param headYaw in degrees (0 - 360) + */ + public FS_ClientboundAddEntityPacket( + int entityId, + UUID entityUUID, + EntityType entityType, + double x, + double y, + double z, + float yaw, + float pitch, + float headYaw, + int velocityX, + int velocityY, + int velocityZ, + int data) { + this.entityId = entityId; + this.entityUUID = entityUUID; + this.entityType = entityType; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.headYaw = headYaw; + this.velocityX = velocityX; + this.velocityY = velocityY; + this.velocityZ = velocityZ; + this.data = data; + } + + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public UUID getEntityUUID() { + return entityUUID; + } + + public void setEntityUUID(UUID entityUUID) { + this.entityUUID = entityUUID; + } + + public EntityType getEntityType() { + return entityType; + } + + public void setEntityType(EntityType entityType) { + this.entityType = entityType; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public float getPitch() { + return pitch; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + public float getHeadYaw() { + return headYaw; + } + + public void setHeadYaw(float headYaw) { + this.headYaw = headYaw; + } + + public int getVelocityX() { + return velocityX; + } + + public void setVelocityX(int velocityX) { + this.velocityX = velocityX; + } + + public int getVelocityY() { + return velocityY; + } + + public void setVelocityY(int velocityY) { + this.velocityY = velocityY; + } + + public int getVelocityZ() { + return velocityZ; + } + + public void setVelocityZ(int velocityZ) { + this.velocityZ = velocityZ; + } + + public int getData() { + return data; + } + + public void setData(int data) { + this.data = data; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundCreateOrUpdateTeamPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundCreateOrUpdateTeamPacket.java new file mode 100644 index 00000000..9a9bf1e7 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundCreateOrUpdateTeamPacket.java @@ -0,0 +1,324 @@ +package de.oliver.fancysitula.api.packets; + +import de.oliver.fancysitula.api.teams.FS_CollisionRule; +import de.oliver.fancysitula.api.teams.FS_NameTagVisibility; +import net.kyori.adventure.text.Component; + +import java.util.List; + +public abstract class FS_ClientboundCreateOrUpdateTeamPacket extends FS_ClientboundPacket { + + protected Method method; + protected String teamName; + + protected CreateTeam createTeam; + protected RemoveTeam removeTeam; + protected UpdateTeam updateTeam; + protected AddEntity addEntity; + protected RemoveEntity removeEntity; + + public FS_ClientboundCreateOrUpdateTeamPacket(String teamName, CreateTeam createTeam) { + this.method = Method.CREATE_TEAM; + this.teamName = teamName; + this.createTeam = createTeam; + } + + public FS_ClientboundCreateOrUpdateTeamPacket(String teamName, RemoveTeam removeTeam) { + this.method = Method.REMOVE_TEAM; + this.teamName = teamName; + this.removeTeam = removeTeam; + } + + public FS_ClientboundCreateOrUpdateTeamPacket(String teamName, UpdateTeam updateTeam) { + this.method = Method.UPDATE_TEAM; + this.teamName = teamName; + this.updateTeam = updateTeam; + } + + public FS_ClientboundCreateOrUpdateTeamPacket(String teamName, AddEntity addEntity) { + this.method = Method.ADD_ENTITY; + this.teamName = teamName; + this.addEntity = addEntity; + } + + public FS_ClientboundCreateOrUpdateTeamPacket(String teamName, RemoveEntity removeEntity) { + this.method = Method.REMOVE_ENTITY; + this.teamName = teamName; + this.removeEntity = removeEntity; + } + + public Method getMethod() { + return method; + } + + public void setMethod(Method method) { + this.method = method; + } + + public String getTeamName() { + return teamName; + } + + public void setTeamName(String teamName) { + this.teamName = teamName; + } + + public CreateTeam getCreateTeam() { + return createTeam; + } + + public void setCreateTeam(CreateTeam createTeam) { + this.createTeam = createTeam; + } + + public UpdateTeam getUpdateTeam() { + return updateTeam; + } + + public void setUpdateTeam(UpdateTeam updateTeam) { + this.updateTeam = updateTeam; + } + + public AddEntity getAddEntity() { + return addEntity; + } + + public void setAddEntity(AddEntity addEntity) { + this.addEntity = addEntity; + } + + public RemoveEntity getRemoveEntity() { + return removeEntity; + } + + public void setRemoveEntity(RemoveEntity removeEntity) { + this.removeEntity = removeEntity; + } + + public enum Method { + CREATE_TEAM, + REMOVE_TEAM, + UPDATE_TEAM, + ADD_ENTITY, + REMOVE_ENTITY; + } + + public static class CreateTeam { + private Component displayName; + private boolean allowFriendlyFire; + private boolean canSeeFriendlyInvisibles; + private FS_NameTagVisibility nameTagVisibility; + private FS_CollisionRule collisionRule; + private FS_Color color; + private Component prefix; + private Component suffix; + private List entities; + + public CreateTeam(Component displayName, boolean allowFriendlyFire, boolean canSeeFriendlyInvisibles, FS_NameTagVisibility nameTagVisibility, FS_CollisionRule collisionRule, FS_Color color, Component prefix, Component suffix, List entities) { + this.displayName = displayName; + this.allowFriendlyFire = allowFriendlyFire; + this.canSeeFriendlyInvisibles = canSeeFriendlyInvisibles; + this.nameTagVisibility = nameTagVisibility; + this.collisionRule = collisionRule; + this.color = color; + this.prefix = prefix; + this.suffix = suffix; + this.entities = entities; + } + + public Component getDisplayName() { + return displayName; + } + + public void setDisplayName(Component displayName) { + this.displayName = displayName; + } + + public boolean isAllowFriendlyFire() { + return allowFriendlyFire; + } + + public void setAllowFriendlyFire(boolean allowFriendlyFire) { + this.allowFriendlyFire = allowFriendlyFire; + } + + public boolean isCanSeeFriendlyInvisibles() { + return canSeeFriendlyInvisibles; + } + + public void setCanSeeFriendlyInvisibles(boolean canSeeFriendlyInvisibles) { + this.canSeeFriendlyInvisibles = canSeeFriendlyInvisibles; + } + + public FS_NameTagVisibility getNameTagVisibility() { + return nameTagVisibility; + } + + public void setNameTagVisibility(FS_NameTagVisibility nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility; + } + + public FS_CollisionRule getCollisionRule() { + return collisionRule; + } + + public void setCollisionRule(FS_CollisionRule collisionRule) { + this.collisionRule = collisionRule; + } + + public FS_Color getColor() { + return color; + } + + public void setColor(FS_Color color) { + this.color = color; + } + + public Component getPrefix() { + return prefix; + } + + public void setPrefix(Component prefix) { + this.prefix = prefix; + } + + public Component getSuffix() { + return suffix; + } + + public void setSuffix(Component suffix) { + this.suffix = suffix; + } + + public List getEntities() { + return entities; + } + + public void setEntities(List entities) { + this.entities = entities; + } + } + + public static class RemoveTeam { + + } + + public static class UpdateTeam { + private Component displayName; + private boolean allowFriendlyFire; + private boolean canSeeFriendlyInvisibles; + private FS_NameTagVisibility nameTagVisibility; + private FS_CollisionRule collisionRule; + private FS_Color color; + private Component prefix; + private Component suffix; + + public UpdateTeam(Component displayName, boolean allowFriendlyFire, boolean canSeeFriendlyInvisibles, FS_NameTagVisibility nameTagVisibility, FS_CollisionRule collisionRule, FS_Color color, Component prefix, Component suffix) { + this.displayName = displayName; + this.allowFriendlyFire = allowFriendlyFire; + this.canSeeFriendlyInvisibles = canSeeFriendlyInvisibles; + this.nameTagVisibility = nameTagVisibility; + this.collisionRule = collisionRule; + this.color = color; + this.prefix = prefix; + this.suffix = suffix; + } + + public Component getDisplayName() { + return displayName; + } + + public void setDisplayName(Component displayName) { + this.displayName = displayName; + } + + public boolean isAllowFriendlyFire() { + return allowFriendlyFire; + } + + public void setAllowFriendlyFire(boolean allowFriendlyFire) { + this.allowFriendlyFire = allowFriendlyFire; + } + + public boolean isCanSeeFriendlyInvisibles() { + return canSeeFriendlyInvisibles; + } + + public void setCanSeeFriendlyInvisibles(boolean canSeeFriendlyInvisibles) { + this.canSeeFriendlyInvisibles = canSeeFriendlyInvisibles; + } + + public FS_NameTagVisibility getNameTagVisibility() { + return nameTagVisibility; + } + + public void setNameTagVisibility(FS_NameTagVisibility nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility; + } + + public FS_CollisionRule getCollisionRule() { + return collisionRule; + } + + public void setCollisionRule(FS_CollisionRule collisionRule) { + this.collisionRule = collisionRule; + } + + public FS_Color getColor() { + return color; + } + + public void setColor(FS_Color color) { + this.color = color; + } + + public Component getPrefix() { + return prefix; + } + + public void setPrefix(Component prefix) { + this.prefix = prefix; + } + + public Component getSuffix() { + return suffix; + } + + public void setSuffix(Component suffix) { + this.suffix = suffix; + } + } + + public static class AddEntity { + private List entities; + + public AddEntity(List entities) { + this.entities = entities; + } + + public List getEntities() { + return entities; + } + + public void setEntities(List entities) { + this.entities = entities; + } + } + + public static class RemoveEntity { + private List entities; + + public RemoveEntity(List entities) { + this.entities = entities; + } + + public List getEntities() { + return entities; + } + + public void setEntities(List entities) { + this.entities = entities; + } + } + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPacket.java new file mode 100644 index 00000000..e8950514 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPacket.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.api.packets; + +import de.oliver.fancysitula.api.IFancySitula; +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import org.jetbrains.annotations.ApiStatus; + +public abstract class FS_ClientboundPacket { + + /** + * Creates the packet object. + * For internal use only. + */ + @ApiStatus.Internal + public abstract Object createPacket(); + + /** + * Sends the packet to the player. + * For internal use only. + */ + @ApiStatus.Internal + protected abstract void sendPacketTo(FS_RealPlayer player); + + /** + * Sends the packet to the player. + */ + public final void send(FS_RealPlayer player) { + IFancySitula.LOGGER.debug("Sending packet '" + this.getClass().getSimpleName() + "' to " + player.getBukkitPlayer().getName()); + + sendPacketTo(player); + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoRemovePacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoRemovePacket.java new file mode 100644 index 00000000..359c0dbc --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoRemovePacket.java @@ -0,0 +1,24 @@ +package de.oliver.fancysitula.api.packets; + +import java.util.List; +import java.util.UUID; + +/** + * Used to remove players from the client's player list. + */ +public abstract class FS_ClientboundPlayerInfoRemovePacket extends FS_ClientboundPacket { + + protected List uuids; + + public FS_ClientboundPlayerInfoRemovePacket(List uuids) { + this.uuids = uuids; + } + + public List getUuids() { + return uuids; + } + + public void setUuids(List uuids) { + this.uuids = uuids; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoUpdatePacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoUpdatePacket.java new file mode 100644 index 00000000..4e15b7e3 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundPlayerInfoUpdatePacket.java @@ -0,0 +1,59 @@ +package de.oliver.fancysitula.api.packets; + +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import net.kyori.adventure.text.Component; + +import javax.annotation.Nullable; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +/** + * Used to update the player list of the client. + */ +public abstract class FS_ClientboundPlayerInfoUpdatePacket extends FS_ClientboundPacket { + + protected EnumSet actions; + protected List entries; + + public FS_ClientboundPlayerInfoUpdatePacket(EnumSet actions, List entries) { + this.actions = actions; + this.entries = entries; + } + + public EnumSet getActions() { + return actions; + } + + public void setActions(EnumSet actions) { + this.actions = actions; + } + + public List getEntries() { + return entries; + } + + public void setEntries(List entries) { + this.entries = entries; + } + + public enum Action { + ADD_PLAYER, + INITIALIZE_CHAT, + UPDATE_GAME_MODE, + UPDATE_LISTED, + UPDATE_LATENCY, + UPDATE_DISPLAY_NAME, + } + + public record Entry(UUID uuid, + FS_GameProfile profile, + boolean listed, + int latency, + FS_GameType gameMode, + @Nullable Component displayName) { + + } + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRemoveEntitiesPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRemoveEntitiesPacket.java new file mode 100644 index 00000000..b307c102 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRemoveEntitiesPacket.java @@ -0,0 +1,26 @@ +package de.oliver.fancysitula.api.packets; + +import java.util.List; + +/** + * Used to remove entities from the client's world. + */ +public abstract class FS_ClientboundRemoveEntitiesPacket extends FS_ClientboundPacket { + + protected List entityIds; + + /** + * @param entityIds IDs of the entities to remove + */ + public FS_ClientboundRemoveEntitiesPacket(List entityIds) { + this.entityIds = entityIds; + } + + public List getEntityIds() { + return entityIds; + } + + public void setEntityIds(List entityIds) { + this.entityIds = entityIds; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRotateHeadPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRotateHeadPacket.java new file mode 100644 index 00000000..95634821 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundRotateHeadPacket.java @@ -0,0 +1,28 @@ +package de.oliver.fancysitula.api.packets; + +public abstract class FS_ClientboundRotateHeadPacket extends FS_ClientboundPacket { + + protected int entityId; + protected float headYaw; + + public FS_ClientboundRotateHeadPacket(int entityId, float headYaw) { + this.entityId = entityId; + this.headYaw = headYaw; + } + + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public float getHeadYaw() { + return headYaw; + } + + public void setHeadYaw(float headYaw) { + this.headYaw = headYaw; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEntityDataPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEntityDataPacket.java new file mode 100644 index 00000000..a433475d --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEntityDataPacket.java @@ -0,0 +1,67 @@ +package de.oliver.fancysitula.api.packets; + +import java.util.List; + +public abstract class FS_ClientboundSetEntityDataPacket extends FS_ClientboundPacket { + + protected int entityId; + protected List entityData; + + public FS_ClientboundSetEntityDataPacket(int entityId, List entityData) { + this.entityId = entityId; + this.entityData = entityData; + } + + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public List getEntityData() { + return entityData; + } + + public void setEntityData(List entityData) { + this.entityData = entityData; + } + + public static class EntityData { + private EntityDataAccessor accessor; + private Object value; + + /** + * @param accessor can be found in {@link de.oliver.fancysitula.api.utils.entityData} + * @param value must be the correct type for the accessor (see accessor javadoc) + */ + public EntityData(EntityDataAccessor accessor, Object value) { + this.accessor = accessor; + this.value = value; + } + + public EntityDataAccessor getAccessor() { + return accessor; + } + + public void setAccessor(EntityDataAccessor accessor) { + this.accessor = accessor; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + } + + /** + * @param entityClassName the class name of the entity (e.g. "net.minecraft.world.entity.Display$TextDisplay") + * @param accessorFieldName the field name of the accessor (typically starts with "DATA_" and ends with "_ID") + */ + public record EntityDataAccessor(String entityClassName, String accessorFieldName) { + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEquipmentPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEquipmentPacket.java new file mode 100644 index 00000000..8cad16cf --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetEquipmentPacket.java @@ -0,0 +1,33 @@ +package de.oliver.fancysitula.api.packets; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +public abstract class FS_ClientboundSetEquipmentPacket extends FS_ClientboundPacket { + + protected int entityId; + protected Map equipment; + + public FS_ClientboundSetEquipmentPacket(int entityId, Map equipment) { + this.entityId = entityId; + this.equipment = equipment; + } + + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public Map getEquipment() { + return equipment; + } + + public void setEquipment(Map equipment) { + this.equipment = equipment; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetPassengersPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetPassengersPacket.java new file mode 100644 index 00000000..34d61971 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundSetPassengersPacket.java @@ -0,0 +1,30 @@ +package de.oliver.fancysitula.api.packets; + +import java.util.List; + +public abstract class FS_ClientboundSetPassengersPacket extends FS_ClientboundPacket { + + protected int entityId; + protected List passengers; + + public FS_ClientboundSetPassengersPacket(int entityId, List passengers) { + this.entityId = entityId; + this.passengers = passengers; + } + + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public List getPassengers() { + return passengers; + } + + public void setPassengers(List passengers) { + this.passengers = passengers; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundTeleportEntityPacket.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundTeleportEntityPacket.java new file mode 100644 index 00000000..d9495ae7 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_ClientboundTeleportEntityPacket.java @@ -0,0 +1,78 @@ +package de.oliver.fancysitula.api.packets; + +public abstract class FS_ClientboundTeleportEntityPacket extends FS_ClientboundPacket { + + protected int entityId; + protected double x; + protected double y; + protected double z; + protected float yaw; + protected float pitch; + protected boolean onGround; + + public FS_ClientboundTeleportEntityPacket(int entityId, double x, double y, double z, float yaw, float pitch, boolean onGround) { + this.entityId = entityId; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.onGround = onGround; + } + + public int getEntityId() { + return entityId; + } + + public void setEntityId(int entityId) { + this.entityId = entityId; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public float getPitch() { + return pitch; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + public boolean isOnGround() { + return onGround; + } + + public void setOnGround(boolean onGround) { + this.onGround = onGround; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_Color.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_Color.java new file mode 100644 index 00000000..7fc2d9f9 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/packets/FS_Color.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.api.packets; + +public enum FS_Color { + BLACK(0), + DARK_BLUE(1), + DARK_GREEN(2), + DARK_AQUA(3), + DARK_RED(4), + DARK_PURPLE(5), + GOLD(6), + GRAY(7), + DARK_GRAY(8), + BLUE(9), + GREEN(10), + AQUA(11), + RED(12), + LIGHT_PURPLE(13), + YELLOW(14), + WHITE(15); + + + private final int id; + + FS_Color(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_CollisionRule.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_CollisionRule.java new file mode 100644 index 00000000..d64ec068 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_CollisionRule.java @@ -0,0 +1,18 @@ +package de.oliver.fancysitula.api.teams; + +public enum FS_CollisionRule { + ALWAYS("always"), + NEVER("never"), + PUSH_OTHER_TEAMS("pushOtherTeams"), + PUSH_OWN_TEAM("pushOwnTeam"); + + private final String name; + + FS_CollisionRule(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_NameTagVisibility.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_NameTagVisibility.java new file mode 100644 index 00000000..355026c1 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_NameTagVisibility.java @@ -0,0 +1,18 @@ +package de.oliver.fancysitula.api.teams; + +public enum FS_NameTagVisibility { + ALWAYS("always"), + NEVER("never"), + HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"), + HIDE_FOR_OWN_TEAM("hideForOwnTeam"); + + private final String name; + + FS_NameTagVisibility(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_Team.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_Team.java new file mode 100644 index 00000000..9302d3bb --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/teams/FS_Team.java @@ -0,0 +1,113 @@ +package de.oliver.fancysitula.api.teams; + +import de.oliver.fancysitula.api.packets.FS_Color; +import net.kyori.adventure.text.Component; + +import java.util.List; + +public class FS_Team { + + private String teamName; + private Component displayName; + private boolean allowFriendlyFire; + private boolean canSeeFriendlyInvisibles; + private FS_NameTagVisibility nameTagVisibility; + private FS_CollisionRule collisionRule; + private FS_Color color; + private Component prefix; + private Component suffix; + private List entities; + + public FS_Team(String teamName, Component displayName, boolean allowFriendlyFire, boolean canSeeFriendlyInvisibles, FS_NameTagVisibility nameTagVisibility, FS_CollisionRule collisionRule, FS_Color color, Component prefix, Component suffix, List entities) { + this.teamName = teamName; + this.displayName = displayName; + this.allowFriendlyFire = allowFriendlyFire; + this.canSeeFriendlyInvisibles = canSeeFriendlyInvisibles; + this.nameTagVisibility = nameTagVisibility; + this.collisionRule = collisionRule; + this.color = color; + this.prefix = prefix; + this.suffix = suffix; + this.entities = entities; + } + + public String getTeamName() { + return teamName; + } + + public void setTeamName(String teamName) { + this.teamName = teamName; + } + + public Component getDisplayName() { + return displayName; + } + + public void setDisplayName(Component displayName) { + this.displayName = displayName; + } + + public boolean isAllowFriendlyFire() { + return allowFriendlyFire; + } + + public void setAllowFriendlyFire(boolean allowFriendlyFire) { + this.allowFriendlyFire = allowFriendlyFire; + } + + public boolean isCanSeeFriendlyInvisibles() { + return canSeeFriendlyInvisibles; + } + + public void setCanSeeFriendlyInvisibles(boolean canSeeFriendlyInvisibles) { + this.canSeeFriendlyInvisibles = canSeeFriendlyInvisibles; + } + + public FS_NameTagVisibility getNameTagVisibility() { + return nameTagVisibility; + } + + public void setNameTagVisibility(FS_NameTagVisibility nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility; + } + + public FS_CollisionRule getCollisionRule() { + return collisionRule; + } + + public void setCollisionRule(FS_CollisionRule collisionRule) { + this.collisionRule = collisionRule; + } + + public FS_Color getColor() { + return color; + } + + public void setColor(FS_Color color) { + this.color = color; + } + + public Component getPrefix() { + return prefix; + } + + public void setPrefix(Component prefix) { + this.prefix = prefix; + } + + public Component getSuffix() { + return suffix; + } + + public void setSuffix(Component suffix) { + this.suffix = suffix; + } + + public List getEntities() { + return entities; + } + + public void setEntities(List entities) { + this.entities = entities; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/AngelConverter.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/AngelConverter.java new file mode 100644 index 00000000..21bc110e --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/AngelConverter.java @@ -0,0 +1,9 @@ +package de.oliver.fancysitula.api.utils; + +public class AngelConverter { + + public static byte degreesToVanillaByte(float degrees) { + return (byte) Math.floor(degrees * 256.0F / 360.0F); + } + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_EquipmentSlot.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_EquipmentSlot.java new file mode 100644 index 00000000..88e8d474 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_EquipmentSlot.java @@ -0,0 +1,24 @@ +package de.oliver.fancysitula.api.utils; + +public enum FS_EquipmentSlot { + MAINHAND, + OFFHAND, + FEET, + LEGS, + CHEST, + HEAD, + BODY, + ; + + public static FS_EquipmentSlot fromBukkit(org.bukkit.inventory.EquipmentSlot equipmentSlot) { + return switch (equipmentSlot) { + case HAND -> MAINHAND; + case OFF_HAND -> OFFHAND; + case FEET -> FEET; + case LEGS -> LEGS; + case CHEST -> CHEST; + case HEAD -> HEAD; + case BODY -> BODY; + }; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameProfile.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameProfile.java new file mode 100644 index 00000000..e6be6298 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameProfile.java @@ -0,0 +1,62 @@ +package de.oliver.fancysitula.api.utils; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.destroystokyo.paper.profile.ProfileProperty; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class FS_GameProfile { + + private UUID uuid; + private String name; + private Map properties; + + public FS_GameProfile(UUID uuid, String name, Map properties) { + this.uuid = uuid; + this.name = name; + this.properties = properties; + } + + public FS_GameProfile(UUID uuid, String name) { + this(uuid, name, new HashMap<>()); + } + + public static FS_GameProfile fromBukkit(PlayerProfile gameProfile) { + FS_GameProfile fsGameProfile = new FS_GameProfile(gameProfile.getId(), gameProfile.getName()); + + for (ProfileProperty property : gameProfile.getProperties()) { + fsGameProfile.getProperties().put(property.getName(), new FS_GameProfile.Property(property.getName(), property.getValue(), property.getSignature())); + } + + return fsGameProfile; + } + + public UUID getUUID() { + return uuid; + } + + public void setUUID(UUID uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public record Property(String name, String value, String signature) { + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameType.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameType.java new file mode 100644 index 00000000..d48b9e2e --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/FS_GameType.java @@ -0,0 +1,24 @@ +package de.oliver.fancysitula.api.utils; + +public enum FS_GameType { + SURVIVAL(0, "survival"), + CREATIVE(1, "creative"), + ADVENTURE(2, "adventure"), + SPECTATOR(3, "spectator"); + + private final int id; + private final String name; + + FS_GameType(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/ServerVersion.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/ServerVersion.java new file mode 100644 index 00000000..c3fe97be --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/ServerVersion.java @@ -0,0 +1,70 @@ +package de.oliver.fancysitula.api.utils; + +import org.bukkit.Bukkit; + +import java.util.Arrays; +import java.util.List; + +public enum ServerVersion { + + v1_21_4("1.21.4", 769), + v1_21_3("1.21.3", 768), + v1_21_1("1.21.1", 767), + v1_21("1.21", 767), + v1_20_6("1.20.6", 766), + v1_20_5("1.20.5", 766), + ; + + private final String version; + private final int protocolVersion; + + ServerVersion(String version, int protocolVersion) { + this.version = version; + this.protocolVersion = protocolVersion; + } + + public static ServerVersion getByProtocolVersion(int protocolVersion) { + for (ServerVersion version : values()) { + if (version.getProtocolVersion() == protocolVersion) { + return version; + } + } + + return null; + } + + public static ServerVersion getByVersion(String version) { + for (ServerVersion serverVersion : values()) { + if (serverVersion.getVersion().equals(version)) { + return serverVersion; + } + } + + return null; + } + + public static List getSupportedVersions() { + return Arrays.stream(values()) + .map(ServerVersion::getVersion) + .toList(); + } + + public static boolean isVersionSupported(String version) { + return getByVersion(version) != null; + } + + /** + * @return the current server version of the server the plugin is running on + */ + public static ServerVersion getCurrentVersion() { + return getByVersion(Bukkit.getMinecraftVersion()); + } + + public String getVersion() { + return version; + } + + public int getProtocolVersion() { + return protocolVersion; + } +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_BlockDisplayData.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_BlockDisplayData.java new file mode 100644 index 00000000..50a5c740 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_BlockDisplayData.java @@ -0,0 +1,12 @@ +package de.oliver.fancysitula.api.utils.entityData; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; + +public class FS_BlockDisplayData { + + /** + * Use {@link org.bukkit.block.Block} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor BLOCK = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$BlockDisplay", "DATA_BLOCK_STATE_ID"); + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_DisplayData.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_DisplayData.java new file mode 100644 index 00000000..b9469654 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_DisplayData.java @@ -0,0 +1,82 @@ +package de.oliver.fancysitula.api.utils.entityData; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; + +public class FS_DisplayData { + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor TRANSFORMATION_INTERPOLATION_DURATION = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor POS_ROT_INTERPOLATION_DURATION = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_POS_ROT_INTERPOLATION_DURATION_ID"); + + /** + * Use {@link org.joml.Vector3f} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor TRANSLATION = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_TRANSLATION_ID"); + + /** + * Use {@link org.joml.Vector3f} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor SCALE = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_SCALE_ID"); + + /** + * Use {@link org.joml.Quaternionf} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor LEFT_ROTATION = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_LEFT_ROTATION_ID"); + + /** + * Use {@link org.joml.Quaternionf} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor RIGHT_ROTATION = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_RIGHT_ROTATION_ID"); + + /** + * Use {@link Byte} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor BILLBOARD_RENDER_CONSTRAINTS = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_BILLBOARD_RENDER_CONSTRAINTS_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor BRIGHTNESS_OVERRIDE = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_BRIGHTNESS_OVERRIDE_ID"); + + /** + * Use {@link Float} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor VIEW_RANGE = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_VIEW_RANGE_ID"); + + /** + * Use {@link Float} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor SHADOW_RADIUS = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_SHADOW_RADIUS_ID"); + + /** + * Use {@link Float} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor SHADOW_STRENGTH = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_SHADOW_STRENGTH_ID"); + + /** + * Use {@link Float} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor WIDTH = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_WIDTH_ID"); + + /** + * Use {@link Float} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor HEIGHT = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_HEIGHT_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor GLOW_COLOR_OVERRIDE = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display", "DATA_GLOW_COLOR_OVERRIDE_ID"); + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_EntityData.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_EntityData.java new file mode 100644 index 00000000..79463d31 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_EntityData.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.api.utils.entityData; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; + +public class FS_EntityData { + + /** + * Use {@link Byte} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor SHARED_FLAGS = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_SHARED_FLAGS_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor AIR_SUPPLY = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_AIR_SUPPLY_ID"); + + /** + * Use {@link java.util.Optional} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor CUSTOM_NAME = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_CUSTOM_NAME"); + + /** + * Use {@link Boolean} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor CUSTOM_NAME_VISIBLE = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_CUSTOM_NAME_VISIBLE"); + + /** + * Use {@link Boolean} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor SILENT = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_SILENT"); + + /** + * Use {@link Boolean} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor NO_GRAVITY = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_NO_GRAVITY"); + + // TODO: Add DATA_POSE + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor TICKS_FROZEN = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Entity", "DATA_TICKS_FROZEN"); + +} + diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_ItemDisplayData.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_ItemDisplayData.java new file mode 100644 index 00000000..012cf46b --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_ItemDisplayData.java @@ -0,0 +1,12 @@ +package de.oliver.fancysitula.api.utils.entityData; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; + +public class FS_ItemDisplayData { + + /** + * Use {@link org.bukkit.inventory.ItemStack} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor ITEM = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$ItemDisplay", "DATA_ITEM_STACK_ID"); + +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_TextDisplayData.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_TextDisplayData.java new file mode 100644 index 00000000..7c7ecb30 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/entityData/FS_TextDisplayData.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.api.utils.entityData; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; + +public class FS_TextDisplayData { + + /** + * Use {@link net.kyori.adventure.text.Component} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor TEXT = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$TextDisplay", "DATA_TEXT_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor LINE_WIDTH = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$TextDisplay", "DATA_LINE_WIDTH_ID"); + + /** + * Use {@link Integer} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor BACKGROUND = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$TextDisplay", "DATA_BACKGROUND_COLOR_ID"); + + /** + * Use {@link Byte} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor TEXT_OPACITY = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$TextDisplay", "DATA_TEXT_OPACITY_ID"); + + /** + * Use {@link Byte} as value + */ + public static final FS_ClientboundSetEntityDataPacket.EntityDataAccessor STYLE_FLAGS = new FS_ClientboundSetEntityDataPacket.EntityDataAccessor("net.minecraft.world.entity.Display$TextDisplay", "DATA_STYLE_FLAGS_ID"); +} diff --git a/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/reflections/ReflectionUtils.java b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/reflections/ReflectionUtils.java new file mode 100644 index 00000000..a708eda0 --- /dev/null +++ b/libraries/packets/api/src/main/java/de/oliver/fancysitula/api/utils/reflections/ReflectionUtils.java @@ -0,0 +1,72 @@ +package de.oliver.fancysitula.api.utils.reflections; + +import sun.misc.Unsafe; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +public class ReflectionUtils { + + private static Unsafe getUnsafe() throws Exception { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } + + public static T createUnsafeInstance(Class clazz) throws Exception { + Unsafe unsafe = getUnsafe(); + return (T) unsafe.allocateInstance(clazz); + } + + public static T createInstance(Class clazz) throws Exception { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + + public static Object getField(Object object, String fieldName) throws Exception { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } + + public static void setField(Object object, String fieldName, Object value) throws Exception { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(object, value); + } + + public static void setFinalField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + + // Use Unsafe to modify the final field + Unsafe unsafe = getUnsafe(); + long offset = unsafe.objectFieldOffset(field); + if (field.getType() == int.class) { + unsafe.putInt(target, offset, (int) value); + } else if (field.getType() == long.class) { + unsafe.putLong(target, offset, (long) value); + } else if (field.getType() == double.class) { + unsafe.putDouble(target, offset, (double) value); + } else if (field.getType() == float.class) { + unsafe.putFloat(target, offset, (float) value); + } else if (field.getType() == boolean.class) { + unsafe.putBoolean(target, offset, (boolean) value); + } else if (field.getType() == byte.class) { + unsafe.putByte(target, offset, (byte) value); + } else if (field.getType() == short.class) { + unsafe.putShort(target, offset, (short) value); + } else if (field.getType() == char.class) { + unsafe.putChar(target, offset, (char) value); + } else { + unsafe.putObject(target, offset, value); + } + } + + public static T getStaticField(Class clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(null); + } +} diff --git a/libraries/packets/build.gradle.kts b/libraries/packets/build.gradle.kts new file mode 100644 index 00000000..711f4ddf --- /dev/null +++ b/libraries/packets/build.gradle.kts @@ -0,0 +1,87 @@ +plugins { + id("java-library") + id("maven-publish") + id("com.gradleup.shadow") +} + +allprojects { + group = "de.oliver" + version = "0.0.13" + description = "Simple, lightweight and fast library for minecraft internals" + + repositories { + mavenLocal() + mavenCentral() + maven(url = "https://repo.papermc.io/repository/maven-public/") + maven(url = "https://repo.fancyplugins.de/releases") + } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + + implementation(project(":libraries:packets:api")) + implementation(project(":libraries:packets:factories")) + implementation(project(":libraries:packets:implementations:1_21_4")) + implementation(project(":libraries:packets:implementations:1_21_3")) + implementation(project(":libraries:packets:implementations:1_20_6")) + implementation("de.oliver.FancyAnalytics:logger:0.0.4") +} + +tasks { + shadowJar { + archiveClassifier.set("") + configurations = listOf(project.configurations["runtimeClasspath"]) + dependencies { + include(dependency("de.oliver:.*")) + } + } + + publishing { + repositories { + maven { + name = "fancypluginsReleases" + url = uri("https://repo.fancyplugins.de/releases") + credentials(PasswordCredentials::class) + authentication { + isAllowInsecureProtocol = true + create("basic") + } + } + + maven { + name = "fancypluginsSnapshots" + url = uri("https://repo.fancyplugins.de/snapshots") + credentials(PasswordCredentials::class) + authentication { + isAllowInsecureProtocol = true + create("basic") + } + } + } + publications { + create("shadow") { + groupId = project.group.toString() + version = project.version.toString() + artifact(shadowJar) + } + } + } + + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release = 21 + } + + javadoc { + options.encoding = Charsets.UTF_8.name() + } + + processResources { + filteringCharset = Charsets.UTF_8.name() + } +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} \ No newline at end of file diff --git a/libraries/packets/factories/build.gradle.kts b/libraries/packets/factories/build.gradle.kts new file mode 100644 index 00000000..a3d118a1 --- /dev/null +++ b/libraries/packets/factories/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("java-library") +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + compileOnly(project(":libraries:packets:api")) + compileOnly(project(":libraries:packets:implementations:1_20_6")) + compileOnly(project(":libraries:packets:implementations:1_21_3")) + compileOnly(project(":libraries:packets:implementations:1_21_4")) +} + +tasks { + java { + withSourcesJar() + withJavadocJar() + } + + javadoc { + options.encoding = Charsets.UTF_8.name() + } + + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release = 21 + + } +} \ No newline at end of file diff --git a/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/EntityFactory.java b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/EntityFactory.java new file mode 100644 index 00000000..c6f91d54 --- /dev/null +++ b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/EntityFactory.java @@ -0,0 +1,72 @@ +package de.oliver.fancysitula.factories; + +import de.oliver.fancysitula.api.entities.FS_Entity; +import de.oliver.fancysitula.api.entities.FS_Player; +import de.oliver.fancysitula.api.entities.FS_RealPlayer; + +import java.util.List; + +public class EntityFactory { + + public void spawnEntityFor(FS_RealPlayer player, FS_Entity entity) { + if (entity == null) { + return; + } + + FancySitula.PACKET_FACTORY.createAddEntityPacket( + entity.getId(), + entity.getUuid(), + entity.getType(), + entity.getX(), + entity.getY(), + entity.getZ(), + entity.getYaw(), + entity.getPitch(), + entity.getHeadYaw(), + entity.getVelocityX(), + entity.getVelocityY(), + entity.getVelocityZ(), + entity.getData() + ).send(player); + + setEntityDataFor(player, entity); + } + + public void despawnEntityFor(FS_RealPlayer player, FS_Entity entity) { + if (entity == null) { + return; + } + + FancySitula.PACKET_FACTORY.createRemoveEntitiesPacket( + List.of(entity.getId()) + ) + .send(player); + } + + public void setEntityDataFor(FS_RealPlayer player, FS_Entity entity) { + if (entity == null) { + return; + } + + if (entity.getEntityData().isEmpty()) { + return; + } + + FancySitula.PACKET_FACTORY.createSetEntityDataPacket( + entity.getId(), + entity.getEntityData() + ).send(player); + } + + public void setEntityEquipmentFor(FS_RealPlayer player, FS_Player entity) { + if (entity == null) { + return; + } + + FancySitula.PACKET_FACTORY.createSetEquipmentPacket( + entity.getId(), + entity.getEquipment() + ).send(player); + } + +} diff --git a/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/FancySitula.java b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/FancySitula.java new file mode 100644 index 00000000..e16f4bd3 --- /dev/null +++ b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/FancySitula.java @@ -0,0 +1,8 @@ +package de.oliver.fancysitula.factories; + +public class FancySitula { + + public static final PacketFactory PACKET_FACTORY = new PacketFactory(); + public static final EntityFactory ENTITY_FACTORY = new EntityFactory(); + public static final TeamFactory TEAM_FACTORY = new TeamFactory(); +} diff --git a/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/PacketFactory.java b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/PacketFactory.java new file mode 100644 index 00000000..75bdc266 --- /dev/null +++ b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/PacketFactory.java @@ -0,0 +1,549 @@ +package de.oliver.fancysitula.factories; + +import de.oliver.fancysitula.api.packets.*; +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.api.utils.ServerVersion; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Factory class for creating packet instances based on the server version + */ +public class PacketFactory { + + /** + * Creates a new FS_ClientboundPlayerInfoUpdatePacket instance based on the server version + * + * @param actions EnumSet of {@link FS_ClientboundPlayerInfoUpdatePacket.Action} to perform + * @param entries List of {@link FS_ClientboundPlayerInfoUpdatePacket.Entry} to update + */ + public FS_ClientboundPlayerInfoUpdatePacket createPlayerInfoUpdatePacket( + ServerVersion serverVersion, EnumSet actions, + List entries) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundPlayerInfoUpdatePacket instance based on the current server version + * + * @param actions EnumSet of {@link FS_ClientboundPlayerInfoUpdatePacket.Action} to perform + * @param entries List of {@link FS_ClientboundPlayerInfoUpdatePacket.Entry} to update + */ + public FS_ClientboundPlayerInfoUpdatePacket createPlayerInfoUpdatePacket( + EnumSet actions, + List entries) { + return createPlayerInfoUpdatePacket(ServerVersion.getCurrentVersion(), actions, entries); + } + + /** + * Creates a new FS_ClientboundAddEntityPacket instance based on the server version + * + * @param yaw in degrees (0 - 360) + * @param pitch in degrees (0 - 360) + * @param headYaw in degrees (0 - 360) + */ + public FS_ClientboundAddEntityPacket createAddEntityPacket( + ServerVersion serverVersion, + int entityId, + UUID entityUUID, + EntityType entityType, + double x, + double y, + double z, + float yaw, + float pitch, + float headYaw, + int velocityX, + int velocityY, + int velocityZ, + int data) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundAddEntityPacketImpl(entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundAddEntityPacketImpl(entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundAddEntityPacketImpl(entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundAddEntityPacket instance based on the current server version + * + * @param yaw in degrees (0 - 360) + * @param pitch in degrees (0 - 360) + * @param headYaw in degrees (0 - 360) + */ + public FS_ClientboundAddEntityPacket createAddEntityPacket( + int entityId, + UUID entityUUID, + EntityType entityType, + double x, + double y, + double z, + float yaw, + float pitch, + float headYaw, + int velocityX, + int velocityY, + int velocityZ, + int data) { + return createAddEntityPacket(ServerVersion.getCurrentVersion(), entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + + /** + * Creates a new FS_ClientboundPlayerInfoRemovePacket instance based on the server version + * + * @param uuids UUIDs of the players to remove + */ + public FS_ClientboundPlayerInfoRemovePacket createPlayerInfoRemovePacket(ServerVersion serverVersion, List uuids) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundPlayerInfoRemovePacketImpl(uuids); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundPlayerInfoRemovePacketImpl(uuids); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundPlayerInfoRemovePacketImpl(uuids); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundPlayerInfoRemovePacket instance based on the current server version + * + * @param uuids UUIDs of the players to remove + */ + public FS_ClientboundPlayerInfoRemovePacket createPlayerInfoRemovePacket(List uuids) { + return createPlayerInfoRemovePacket(ServerVersion.getCurrentVersion(), uuids); + } + + /** + * Creates a new FS_ClientboundPlayerInfoRemovePacket instance based on the server version + * + * @param uuid UUID of the player to remove + */ + public FS_ClientboundPlayerInfoRemovePacket createPlayerInfoRemovePacket(ServerVersion serverVersion, UUID uuid) { + return createPlayerInfoRemovePacket(serverVersion, List.of(uuid)); + } + + /** + * Creates a new FS_ClientboundPlayerInfoRemovePacket instance based on the current server version + * + * @param uuid UUID of the player to remove + */ + public FS_ClientboundPlayerInfoRemovePacket createPlayerInfoRemovePacket(UUID uuid) { + return createPlayerInfoRemovePacket(ServerVersion.getCurrentVersion(), uuid); + } + + /** + * Creates a new FS_ClientboundRemoveEntitiesPacket instance based on the server version + * + * @param entityIds IDs of the entities to remove + */ + public FS_ClientboundRemoveEntitiesPacket createRemoveEntitiesPacket(ServerVersion serverVersion, List entityIds) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundRemoveEntitiesPacketImpl(entityIds); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundRemoveEntitiesPacketImpl(entityIds); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundRemoveEntitiesPacketImpl(entityIds); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundRemoveEntitiesPacket instance based on the current server version + * + * @param entityIds IDs of the entities to remove + */ + public FS_ClientboundRemoveEntitiesPacket createRemoveEntitiesPacket(List entityIds) { + return createRemoveEntitiesPacket(ServerVersion.getCurrentVersion(), entityIds); + } + + /** + * Creates a new FS_ClientboundTeleportEntityPacket instance based on the server version + * + * @param entityId ID of the entity to teleport + * @param x X coordinate + * @param y Y coordinate + * @param z Z coordinate + * @param yaw Yaw in degrees (0 - 360) + * @param pitch Pitch in degrees (0 - 360) + * @param onGround Whether the entity is on the ground + */ + public FS_ClientboundTeleportEntityPacket createTeleportEntityPacket( + ServerVersion serverVersion, + int entityId, + double x, + double y, + double z, + float yaw, + float pitch, + boolean onGround + ) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundTeleportEntityPacket instance based on the current server version + * + * @param entityId ID of the entity to teleport + * @param x X coordinate + * @param y Y coordinate + * @param z Z coordinate + * @param yaw Yaw in degrees (0 - 360) + * @param pitch Pitch in degrees (0 - 360) + * @param onGround Whether the entity is on the ground + */ + public FS_ClientboundTeleportEntityPacket createTeleportEntityPacket( + int entityId, + double x, + double y, + double z, + float yaw, + float pitch, + boolean onGround + ) { + return createTeleportEntityPacket(ServerVersion.getCurrentVersion(), entityId, x, y, z, yaw, pitch, onGround); + } + + /** + * Creates a new FS_ClientboundRotateHeadPacket instance based on the server version + * + * @param entityId ID of the entity to rotate the head of + * @param headYaw Yaw of the head in degrees (0 - 360) + */ + public FS_ClientboundRotateHeadPacket createRotateHeadPacket(ServerVersion serverVersion, int entityId, float headYaw) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundRotateHeadPacketImpl(entityId, headYaw); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundRotateHeadPacketImpl(entityId, headYaw); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundRotateHeadPacketImpl(entityId, headYaw); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundRotateHeadPacket instance based on the current server version + * + * @param entityId ID of the entity to rotate the head of + * @param headYaw Yaw of the head in degrees (0 - 360) + */ + public FS_ClientboundRotateHeadPacket createRotateHeadPacket(int entityId, float headYaw) { + return createRotateHeadPacket(ServerVersion.getCurrentVersion(), entityId, headYaw); + } + + /** + * Creates a new FS_ClientboundSetEntityDataPacket instance based on the server version + * + * @param entityId ID of the entity to set the data of + * @param entityData List of {@link FS_ClientboundSetEntityDataPacket.EntityData} to set + */ + public FS_ClientboundSetEntityDataPacket createSetEntityDataPacket( + ServerVersion serverVersion, int entityId, List entityData) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundSetEntityDataPacketImpl(entityId, entityData); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundSetEntityDataPacketImpl(entityId, entityData); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetEntityDataPacketImpl(entityId, entityData); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundSetEntityDataPacket instance based on the current server version + * + * @param entityId ID of the entity to set the data of + * @param entityData List of {@link FS_ClientboundSetEntityDataPacket.EntityData} to set + */ + public FS_ClientboundSetEntityDataPacket createSetEntityDataPacket(int entityId, List entityData) { + return createSetEntityDataPacket(ServerVersion.getCurrentVersion(), entityId, entityData); + } + + /** + * Creates a new FS_ClientboundSetEquipmentPacket instance based on the server version + * + * @param entityId ID of the entity to set the equipment of + * @param equipment Map of {@link org.bukkit.inventory.EquipmentSlot} and {@link org.bukkit.inventory.ItemStack} to set + */ + public FS_ClientboundSetEquipmentPacket createSetEquipmentPacket(ServerVersion serverVersion, int entityId, Map equipment) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundSetEquipmentPacketImpl(entityId, equipment); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundSetEquipmentPacketImpl(entityId, equipment); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetEquipmentPacketImpl(entityId, equipment); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundSetEquipmentPacket instance based on the current server version + * + * @param entityId ID of the entity to set the equipment of + * @param equipment Map of {@link org.bukkit.inventory.EquipmentSlot} and {@link org.bukkit.inventory.ItemStack} to set + */ + public FS_ClientboundSetEquipmentPacket createSetEquipmentPacket(int entityId, Map equipment) { + return createSetEquipmentPacket(ServerVersion.getCurrentVersion(), entityId, equipment); + } + + /** + * Creates a new FS_ClientboundSetPassengersPacket instance based on the server version + * + * @param entityId ID of the vehicle entity + * @param passengers List of entity IDs to set as passengers + */ + public FS_ClientboundSetPassengersPacket createSetPassengersPacket( + ServerVersion serverVersion, int entityId, List passengers) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundSetPassengersPacketImpl(entityId, passengers); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundSetPassengersPacketImpl(entityId, passengers); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetPassengersPacketImpl(entityId, passengers); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundSetPassengersPacket instance based on the current server version + * + * @param entityId ID of the vehicle entity + * @param passengers List of entity IDs to set as passengers + */ + public FS_ClientboundSetPassengersPacket createSetPassengersPacket(int entityId, List passengers) { + return createSetPassengersPacket(ServerVersion.getCurrentVersion(), entityId, passengers); + } + + + /** + * Creates and returns a FS_ClientboundCreateOrUpdateTeamPacket based on the given server version and team information. + * + * @param serverVersion the version of the server for which the packet is to be created + * @param teamName the name of the team to be created or updated + * @param createTeam an instance of FS_ClientboundCreateOrUpdateTeamPacket.CreateTeam containing the team creation or update details + * @return a FS_ClientboundCreateOrUpdateTeamPacket instance corresponding to the specified server version and team details + * @throws IllegalArgumentException if the provided server version is not supported + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(ServerVersion serverVersion, String teamName, FS_ClientboundCreateOrUpdateTeamPacket.CreateTeam createTeam) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, createTeam); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, createTeam); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, createTeam); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates an FS_ClientboundCreateOrUpdateTeamPacket for the given team name and creation details. + * + * @param teamName The name of the team to create or update. + * @param createTeam The details of the team creation or update. + * @return An instance of FS_ClientboundCreateOrUpdateTeamPacket containing the creation or update details. + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(String teamName, FS_ClientboundCreateOrUpdateTeamPacket.CreateTeam createTeam) { + return createCreateOrUpdateTeamPacket(ServerVersion.getCurrentVersion(), teamName, createTeam); + } + + /** + * Creates a packet for creating or updating a team based on the specified server version. + * + * @param serverVersion The version of the server. + * @param teamName The name of the team. + * @param removeTeam Information about whether to remove the team. + * @return The packet for creating or updating the team. + * @throws IllegalArgumentException if the server version is unsupported. + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(ServerVersion serverVersion, String teamName, FS_ClientboundCreateOrUpdateTeamPacket.RemoveTeam removeTeam) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, removeTeam); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, removeTeam); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, removeTeam); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a packet to create or update a team with the specified name and removal flag. + * + * @param teamName the name of the team to create or update + * @param removeTeam the flag indicating whether to remove the team + * @return a packet for creating or updating the team + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(String teamName, FS_ClientboundCreateOrUpdateTeamPacket.RemoveTeam removeTeam) { + return createCreateOrUpdateTeamPacket(ServerVersion.getCurrentVersion(), teamName, removeTeam); + } + + /** + * Creates an instance of FS_ClientboundCreateOrUpdateTeamPacket based on the provided server version. + * + * @param serverVersion The server version for which the packet should be created. + * @param teamName The name of the team that is being created or updated. + * @param updateTeam The update team details which contain information about the team. + * @return A new instance of FS_ClientboundCreateOrUpdateTeamPacket tailored for the specified server version. + * @throws IllegalArgumentException If the provided server version is not supported. + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(ServerVersion serverVersion, String teamName, FS_ClientboundCreateOrUpdateTeamPacket.UpdateTeam updateTeam) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, updateTeam); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, updateTeam); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, updateTeam); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a new FS_ClientboundCreateOrUpdateTeamPacket for creating or updating a team. + * + * @param teamName the name of the team to be created or updated + * @param updateTeam the update information for the team + * @return a new instance of FS_ClientboundCreateOrUpdateTeamPacket + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(String teamName, FS_ClientboundCreateOrUpdateTeamPacket.UpdateTeam updateTeam) { + return createCreateOrUpdateTeamPacket(ServerVersion.getCurrentVersion(), teamName, updateTeam); + } + + /** + * Creates a new instance of FS_ClientboundCreateOrUpdateTeamPacket based on the given server version, team name, and addEntity parameters. + * + * @param serverVersion the version of the server for which the packet will be created + * @param teamName the name of the team to be created or updated + * @param addEntity the add entity information needed for packet creation + * @return an instance of FS_ClientboundCreateOrUpdateTeamPacket appropriate for the specified server version + * @throws IllegalArgumentException if the server version is not supported + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(ServerVersion serverVersion, String teamName, FS_ClientboundCreateOrUpdateTeamPacket.AddEntity addEntity) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, addEntity); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, addEntity); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, addEntity); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a packet for creating or updating a team with the specified name and entity. + * + * @param teamName the name of the team to create or update + * @param addEntity the entity representing the addition details for the team + * @return the packet representing the create or update operation on the team + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(String teamName, FS_ClientboundCreateOrUpdateTeamPacket.AddEntity addEntity) { + return createCreateOrUpdateTeamPacket(ServerVersion.getCurrentVersion(), teamName, addEntity); + } + + /** + * Creates an instance of FS_ClientboundCreateOrUpdateTeamPacket based on the server version. + * + * @param serverVersion The version of the server. + * @param teamName The name of the team to create or update. + * @param removeEntity The entity removal configuration for the packet. + * @return A new instance of FS_ClientboundCreateOrUpdateTeamPacket for the specified server version. + * @throws IllegalArgumentException If the server version is unsupported. + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(ServerVersion serverVersion, String teamName, FS_ClientboundCreateOrUpdateTeamPacket.RemoveEntity removeEntity) { + switch (serverVersion) { + case v1_21_4 -> { + return new de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, removeEntity); + } + case v1_21_3 -> { + return new de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, removeEntity); + } + case v1_20_5, v1_20_6, v1_21, v1_21_1 -> { + return new de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundCreateOrUpdateTeamPacketImpl(teamName, removeEntity); + } + default -> throw new IllegalArgumentException("Unsupported server version: " + serverVersion.getVersion()); + } + } + + /** + * Creates a packet for creating or updating a team with the specified name and entity removal configuration. + * + * @param teamName the name of the team to create or update + * @param removeEntity the entity removal configuration for the team + * @return the packet for creating or updating the team + */ + public FS_ClientboundCreateOrUpdateTeamPacket createCreateOrUpdateTeamPacket(String teamName, FS_ClientboundCreateOrUpdateTeamPacket.RemoveEntity removeEntity) { + return createCreateOrUpdateTeamPacket(ServerVersion.getCurrentVersion(), teamName, removeEntity); + } +} diff --git a/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/TeamFactory.java b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/TeamFactory.java new file mode 100644 index 00000000..7141507b --- /dev/null +++ b/libraries/packets/factories/src/main/java/de/oliver/fancysitula/factories/TeamFactory.java @@ -0,0 +1,77 @@ +package de.oliver.fancysitula.factories; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundCreateOrUpdateTeamPacket; +import de.oliver.fancysitula.api.teams.FS_Team; + +import java.util.List; + +public class TeamFactory { + + public void createTeamFor(FS_RealPlayer player, FS_Team team) { + FS_ClientboundCreateOrUpdateTeamPacket packet = FancySitula.PACKET_FACTORY.createCreateOrUpdateTeamPacket( + team.getTeamName(), + new FS_ClientboundCreateOrUpdateTeamPacket.CreateTeam( + team.getDisplayName(), + team.isAllowFriendlyFire(), + team.isCanSeeFriendlyInvisibles(), + team.getNameTagVisibility(), + team.getCollisionRule(), + team.getColor(), + team.getPrefix(), + team.getSuffix(), + team.getEntities() + ) + ); + + player.sendPacket(packet); + } + + public void removeTeamFor(FS_RealPlayer player, FS_Team team) { + FS_ClientboundCreateOrUpdateTeamPacket packet = FancySitula.PACKET_FACTORY.createCreateOrUpdateTeamPacket( + team.getTeamName(), + new FS_ClientboundCreateOrUpdateTeamPacket.RemoveTeam() + ); + + player.sendPacket(packet); + } + + /** + * Updates the team for the player. But does not add or remove entities. + */ + public void updateTeamFor(FS_RealPlayer player, FS_Team team) { + FS_ClientboundCreateOrUpdateTeamPacket packet = FancySitula.PACKET_FACTORY.createCreateOrUpdateTeamPacket( + team.getTeamName(), + new FS_ClientboundCreateOrUpdateTeamPacket.UpdateTeam( + team.getDisplayName(), + team.isAllowFriendlyFire(), + team.isCanSeeFriendlyInvisibles(), + team.getNameTagVisibility(), + team.getCollisionRule(), + team.getColor(), + team.getPrefix(), + team.getSuffix() + ) + ); + + player.sendPacket(packet); + } + + public void addEntitiesToTeamFor(FS_RealPlayer player, FS_Team team, List entities) { + FS_ClientboundCreateOrUpdateTeamPacket packet = FancySitula.PACKET_FACTORY.createCreateOrUpdateTeamPacket( + team.getTeamName(), + new FS_ClientboundCreateOrUpdateTeamPacket.AddEntity(entities) + ); + + player.sendPacket(packet); + } + + public void removeEntitiesFromTeamFor(FS_RealPlayer player, FS_Team team, List entities) { + FS_ClientboundCreateOrUpdateTeamPacket packet = FancySitula.PACKET_FACTORY.createCreateOrUpdateTeamPacket( + team.getTeamName(), + new FS_ClientboundCreateOrUpdateTeamPacket.RemoveEntity(entities) + ); + + player.sendPacket(packet); + } +} diff --git a/libraries/packets/fancysitula_title.png b/libraries/packets/fancysitula_title.png new file mode 100644 index 0000000000000000000000000000000000000000..dd52e7046c0f6218076cabddcffce02bdbd3eb77 GIT binary patch literal 176618 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%0A^W>wz^EX71qm}vx(3q)^l&!>($oTl< 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 G^Zx^vfqGp4 literal 0 HcmV?d00001 diff --git a/libraries/packets/gradle.properties b/libraries/packets/gradle.properties new file mode 100644 index 00000000..94918500 --- /dev/null +++ b/libraries/packets/gradle.properties @@ -0,0 +1,2 @@ +minecraftVersion=1.21.3 +fancyLoggerVersion=0.0.4 \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/build.gradle.kts b/libraries/packets/implementations/1_20_6/build.gradle.kts new file mode 100644 index 00000000..7aefac23 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("java-library") + id("io.papermc.paperweight.userdev") +} + +val minecraftVersion = "1.20.6" + +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +dependencies { + paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT") + compileOnly(project(":libraries:packets:api")) + + testImplementation(project(":libraries:packets:api")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") +} + +tasks { + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImpl.java new file mode 100644 index 00000000..c73996c6 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImpl.java @@ -0,0 +1,48 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundAddEntityPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +public class ClientboundAddEntityPacketImpl extends FS_ClientboundAddEntityPacket { + + public ClientboundAddEntityPacketImpl(int entityId, UUID entityUUID, EntityType entityType, double x, double y, double z, float yaw, float pitch, float headYaw, int velocityX, int velocityY, int velocityZ, int data) { + super(entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + + @Override + public Object createPacket() { + net.minecraft.world.entity.EntityType vanillaType = BuiltInRegistries.ENTITY_TYPE.get(CraftNamespacedKey.toMinecraft(entityType.getKey())); + + return new ClientboundAddEntityPacket( + entityId, + entityUUID, + x, + y, + z, + AngelConverter.degreesToVanillaByte(pitch), + AngelConverter.degreesToVanillaByte(yaw), + vanillaType, + data, + new Vec3(velocityX, velocityY, velocityZ), + AngelConverter.degreesToVanillaByte(headYaw) + ); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundAddEntityPacket packet = (ClientboundAddEntityPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundCreateOrUpdateTeamPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundCreateOrUpdateTeamPacketImpl.java new file mode 100644 index 00000000..04aa823a --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundCreateOrUpdateTeamPacketImpl.java @@ -0,0 +1,128 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundCreateOrUpdateTeamPacket; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.ChatFormatting; +import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.Team; + +public class ClientboundCreateOrUpdateTeamPacketImpl extends FS_ClientboundCreateOrUpdateTeamPacket { + + private static final Scoreboard SCOREBOARD = new Scoreboard(); + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, CreateTeam createTeam) { + super(teamName, createTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, RemoveTeam removeTeam) { + super(teamName, removeTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, UpdateTeam updateTeam) { + super(teamName, updateTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, AddEntity addEntity) { + super(teamName, addEntity); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, RemoveEntity removeEntity) { + super(teamName, removeEntity); + } + + @Override + public Object createPacket() { + return switch (method) { + case CREATE_TEAM -> createCreateTeamPacket(); + case REMOVE_TEAM -> createRemoveTeamPacket(); + case UPDATE_TEAM -> createUpdateTeamPacket(); + case ADD_ENTITY -> createAddEntityPacket(); + case REMOVE_ENTITY -> createRemoveEntityPacket(); + }; + } + + private Object createCreateTeamPacket() { + if (createTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + playerTeam.setDisplayName(PaperAdventure.asVanilla(createTeam.getDisplayName())); + playerTeam.setAllowFriendlyFire(createTeam.isAllowFriendlyFire()); + playerTeam.setSeeFriendlyInvisibles(createTeam.isCanSeeFriendlyInvisibles()); + playerTeam.setNameTagVisibility(Team.Visibility.byName(createTeam.getNameTagVisibility().getName())); + playerTeam.setCollisionRule(PlayerTeam.CollisionRule.byName(createTeam.getCollisionRule().getName())); + playerTeam.setColor(ChatFormatting.getById(createTeam.getColor().getId())); + playerTeam.setPlayerPrefix(PaperAdventure.asVanilla(createTeam.getPrefix())); + playerTeam.setPlayerSuffix(PaperAdventure.asVanilla(createTeam.getSuffix())); + for (String entity : createTeam.getEntities()) { + playerTeam.getPlayers().add(entity); + } + + return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true); + } + + private Object createRemoveTeamPacket() { + if (removeTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + return ClientboundSetPlayerTeamPacket.createRemovePacket(playerTeam); + } + + private Object createUpdateTeamPacket() { + if (updateTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + playerTeam.setDisplayName(PaperAdventure.asVanilla(updateTeam.getDisplayName())); + playerTeam.setAllowFriendlyFire(updateTeam.isAllowFriendlyFire()); + playerTeam.setSeeFriendlyInvisibles(updateTeam.isCanSeeFriendlyInvisibles()); + playerTeam.setNameTagVisibility(Team.Visibility.byName(updateTeam.getNameTagVisibility().getName())); + playerTeam.setCollisionRule(PlayerTeam.CollisionRule.byName(updateTeam.getCollisionRule().getName())); + playerTeam.setColor(ChatFormatting.getById(updateTeam.getColor().getId())); + playerTeam.setPlayerPrefix(PaperAdventure.asVanilla(updateTeam.getPrefix())); + playerTeam.setPlayerSuffix(PaperAdventure.asVanilla(updateTeam.getSuffix())); + + return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true); + } + + private Object createAddEntityPacket() { + if (addEntity == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + for (String entity : addEntity.getEntities()) { + playerTeam.getPlayers().add(entity); + } + return ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(playerTeam, addEntity.getEntities(), ClientboundSetPlayerTeamPacket.Action.ADD); + } + + private Object createRemoveEntityPacket() { + if (removeEntity == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + for (String entity : removeEntity.getEntities()) { + playerTeam.getPlayers().add(entity); + } + return ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(playerTeam, removeEntity.getEntities(), ClientboundSetPlayerTeamPacket.Action.REMOVE); + } + + @Override + protected void sendPacketTo(FS_RealPlayer player) { + ClientboundSetPlayerTeamPacket packet = (ClientboundSetPlayerTeamPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImpl.java new file mode 100644 index 00000000..8a4626f4 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImpl.java @@ -0,0 +1,30 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoRemovePacket; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.UUID; + +public class ClientboundPlayerInfoRemovePacketImpl extends FS_ClientboundPlayerInfoRemovePacket { + + public ClientboundPlayerInfoRemovePacketImpl(List uuids) { + super(uuids); + } + + @Override + public Object createPacket() { + return new ClientboundPlayerInfoRemovePacket(uuids); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundPlayerInfoRemovePacket packet = (ClientboundPlayerInfoRemovePacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImpl.java new file mode 100644 index 00000000..b49c9854 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImpl.java @@ -0,0 +1,52 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.versions.v1_20_6.utils.GameProfileImpl; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class ClientboundPlayerInfoUpdatePacketImpl extends FS_ClientboundPlayerInfoUpdatePacket { + + public ClientboundPlayerInfoUpdatePacketImpl(EnumSet actions, List entries) { + super(actions, entries); + } + + @Override + public Object createPacket() { + EnumSet vanillaActions = EnumSet.noneOf(ClientboundPlayerInfoUpdatePacket.Action.class); + for (FS_ClientboundPlayerInfoUpdatePacket.Action action : actions) { + vanillaActions.add(ClientboundPlayerInfoUpdatePacket.Action.valueOf(action.name())); + } + + List entries = new ArrayList<>(); + for (Entry entry : this.entries) { + entries.add(new ClientboundPlayerInfoUpdatePacket.Entry( + entry.uuid(), + GameProfileImpl.asVanilla(entry.profile()), + entry.listed(), + entry.latency(), + GameType.byId(entry.gameMode().getId()), + PaperAdventure.asVanilla(entry.displayName()), + null // TODO: Add ChatSession support + )); + } + + return new ClientboundPlayerInfoUpdatePacket(vanillaActions, entries); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundPlayerInfoUpdatePacket packet = (ClientboundPlayerInfoUpdatePacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImpl.java new file mode 100644 index 00000000..7b3c35a1 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImpl.java @@ -0,0 +1,37 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundRemoveEntitiesPacket; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class ClientboundRemoveEntitiesPacketImpl extends FS_ClientboundRemoveEntitiesPacket { + + /** + * @param entityIds IDs of the entities to remove + */ + public ClientboundRemoveEntitiesPacketImpl(List entityIds) { + super(entityIds); + } + + @Override + public Object createPacket() { + int[] ids = new int[this.entityIds.size()]; + for (int i = 0; i < this.entityIds.size(); i++) { + ids[i] = this.entityIds.get(i); + } + + return new ClientboundRemoveEntitiesPacket(ids); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundRemoveEntitiesPacket packet = (ClientboundRemoveEntitiesPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImpl.java new file mode 100644 index 00000000..8d1a8c05 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImpl.java @@ -0,0 +1,38 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundRotateHeadPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.server.level.ServerPlayer; + +public class ClientboundRotateHeadPacketImpl extends FS_ClientboundRotateHeadPacket { + + public ClientboundRotateHeadPacketImpl(int entityId, float headYaw) { + super(entityId, headYaw); + } + + @Override + public Object createPacket() { + ClientboundRotateHeadPacket packet = null; + + try { + packet = ReflectionUtils.createUnsafeInstance(ClientboundRotateHeadPacket.class); + ReflectionUtils.setFinalField(packet, "entityId", entityId); + ReflectionUtils.setFinalField(packet, "yHeadRot", AngelConverter.degreesToVanillaByte(headYaw)); + } catch (Exception e) { + e.printStackTrace(); + } + return packet; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundRotateHeadPacket packet = (ClientboundRotateHeadPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImpl.java new file mode 100644 index 00000000..573d7e8d --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImpl.java @@ -0,0 +1,67 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.block.BlockState; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class ClientboundSetEntityDataPacketImpl extends FS_ClientboundSetEntityDataPacket { + + public ClientboundSetEntityDataPacketImpl(int entityId, List entityData) { + super(entityId, entityData); + } + + @Override + public Object createPacket() { + List> dataValues = new ArrayList<>(); + for (EntityData data : entityData) { + try { + Class entityClass = Class.forName(data.getAccessor().entityClassName()); + net.minecraft.network.syncher.EntityDataAccessor accessor = ReflectionUtils.getStaticField(entityClass, data.getAccessor().accessorFieldName()); + + Object vanillaValue = data.getValue(); + + if (data.getValue() == null) { + continue; + } + + if (data.getValue() instanceof Component c) { + vanillaValue = PaperAdventure.asVanilla(c); + } + + if (data.getValue() instanceof ItemStack i) { + vanillaValue = net.minecraft.world.item.ItemStack.fromBukkitCopy(i); + } + + if (data.getValue() instanceof BlockState b) { + vanillaValue = ((CraftBlockState) b).getHandle(); + } + + dataValues.add(SynchedEntityData.DataValue.create(accessor, vanillaValue)); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + return new ClientboundSetEntityDataPacket(entityId, dataValues); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetEntityDataPacket packet = (ClientboundSetEntityDataPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImpl.java new file mode 100644 index 00000000..e25b481e --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImpl.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import com.mojang.datafixers.util.Pair; +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEquipmentPacket; +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ClientboundSetEquipmentPacketImpl extends FS_ClientboundSetEquipmentPacket { + + public ClientboundSetEquipmentPacketImpl(int entityId, Map equipment) { + super(entityId, equipment); + } + + @Override + public Object createPacket() { + List> slots = new ArrayList<>(); + + for (Map.Entry entry : equipment.entrySet()) { + EquipmentSlot equipmentSlot = net.minecraft.world.entity.EquipmentSlot.byName(entry.getKey().name().toLowerCase()); + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.asNMSCopy(entry.getValue()); + + slots.add(Pair.of(equipmentSlot, itemStack)); + } + + return new ClientboundSetEquipmentPacket(entityId, slots); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetEquipmentPacket packet = (ClientboundSetEquipmentPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImpl.java new file mode 100644 index 00000000..1b201810 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImpl.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetPassengersPacket; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class ClientboundSetPassengersPacketImpl extends FS_ClientboundSetPassengersPacket { + + public ClientboundSetPassengersPacketImpl(int entityId, List passengers) { + super(entityId, passengers); + } + + + @Override + public Object createPacket() { + int[] passengers = new int[this.passengers.size()]; + for (int i = 0; i < this.passengers.size(); i++) { + passengers[i] = this.passengers.get(i); + } + + try { + ClientboundSetPassengersPacket packet = ReflectionUtils.createUnsafeInstance(ClientboundSetPassengersPacket.class); + ReflectionUtils.setFinalField(packet, "vehicle", entityId); + ReflectionUtils.setFinalField(packet, "passengers", passengers); + return packet; + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetPassengersPacket packet = (ClientboundSetPassengersPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImpl.java new file mode 100644 index 00000000..9221e15d --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImpl.java @@ -0,0 +1,43 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundTeleportEntityPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_20_6.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.server.level.ServerPlayer; + +public class ClientboundTeleportEntityPacketImpl extends FS_ClientboundTeleportEntityPacket { + + public ClientboundTeleportEntityPacketImpl(int entityId, double x, double y, double z, float yaw, float pitch, boolean onGround) { + super(entityId, x, y, z, yaw, pitch, onGround); + } + + @Override + public Object createPacket() { + ClientboundTeleportEntityPacket packet = null; + + try { + packet = ReflectionUtils.createUnsafeInstance(ClientboundTeleportEntityPacket.class); + ReflectionUtils.setFinalField(packet, "id", entityId); + ReflectionUtils.setFinalField(packet, "x", x); + ReflectionUtils.setFinalField(packet, "y", y); + ReflectionUtils.setFinalField(packet, "z", z); + ReflectionUtils.setFinalField(packet, "yRot", AngelConverter.degreesToVanillaByte(yaw)); + ReflectionUtils.setFinalField(packet, "xRot", AngelConverter.degreesToVanillaByte(pitch)); + ReflectionUtils.setFinalField(packet, "onGround", onGround); + } catch (Exception e) { + e.printStackTrace(); + } + return packet; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundTeleportEntityPacket packet = (ClientboundTeleportEntityPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/GameProfileImpl.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/GameProfileImpl.java new file mode 100644 index 00000000..2fc36021 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/GameProfileImpl.java @@ -0,0 +1,33 @@ +package de.oliver.fancysitula.versions.v1_20_6.utils; + +import com.mojang.authlib.GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameProfile; + +import java.util.Map; + +public class GameProfileImpl { + + public static GameProfile asVanilla(FS_GameProfile gameProfile) { + GameProfile gf = new GameProfile(gameProfile.getUUID(), gameProfile.getName()); + + for (Map.Entry entry : gameProfile.getProperties().entrySet()) { + FS_GameProfile.Property property = entry.getValue(); + + gf.getProperties().put(entry.getKey(), new com.mojang.authlib.properties.Property(property.name(), property.value(), property.signature())); + } + + return gf; + } + + public static FS_GameProfile fromVanilla(GameProfile gameProfile) { + FS_GameProfile fsGameProfile = new FS_GameProfile(gameProfile.getId(), gameProfile.getName()); + + for (Map.Entry entry : gameProfile.getProperties().entries()) { + com.mojang.authlib.properties.Property property = entry.getValue(); + + fsGameProfile.getProperties().put(entry.getKey(), new FS_GameProfile.Property(property.name(), property.value(), property.signature())); + } + + return fsGameProfile; + } +} diff --git a/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/VanillaPlayerAdapter.java b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/VanillaPlayerAdapter.java new file mode 100644 index 00000000..40bae3df --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/main/java/de/oliver/fancysitula/versions/v1_20_6/utils/VanillaPlayerAdapter.java @@ -0,0 +1,12 @@ +package de.oliver.fancysitula.versions.v1_20_6.utils; + +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; + +public class VanillaPlayerAdapter { + + public static ServerPlayer asVanilla(Player p) { + return ((CraftPlayer) p).getHandle(); + } +} diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImplTest.java new file mode 100644 index 00000000..90779a77 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundAddEntityPacketImplTest.java @@ -0,0 +1,64 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +class ClientboundAddEntityPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + int entityId = 10000; + UUID entityUUID = UUID.randomUUID(); + EntityType entityType = EntityType.PIG; + + double x = 5; + double y = 57; + double z = 203; + + float yaw = 142; + float pitch = 247; + float headYaw = 90; + + int velocityX = 0; + int velocityY = 0; + int velocityZ = 0; + + int data = 0; + + ClientboundAddEntityPacketImpl packet = new ClientboundAddEntityPacketImpl( + entityId, + entityUUID, + entityType, + x, + y, + z, + yaw, + pitch, + headYaw, + velocityX, + velocityY, + velocityZ, + data + ); + + ClientboundAddEntityPacket createdPacket = (ClientboundAddEntityPacket) packet.createPacket(); + + assert createdPacket.getId() == entityId; + assert createdPacket.getUUID().equals(entityUUID); + assert createdPacket.getType().getDescriptionId().equals(entityType.getKey().getKey()); + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getYRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getXRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + assert createdPacket.getXa() == velocityX; + assert createdPacket.getYa() == velocityY; + assert createdPacket.getZa() == velocityZ; + assert createdPacket.getData() == data; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImplTest.java new file mode 100644 index 00000000..c741c052 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoRemovePacketImplTest.java @@ -0,0 +1,22 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoRemovePacketImplTest { + + @Test + void createPacket() { + List uuids = List.of(UUID.randomUUID(), UUID.randomUUID()); + + ClientboundPlayerInfoRemovePacketImpl packet = new ClientboundPlayerInfoRemovePacketImpl(uuids); + ClientboundPlayerInfoRemovePacket vanillaPacket = (ClientboundPlayerInfoRemovePacket) packet.createPacket(); + + for (UUID uuid : uuids) { + assert vanillaPacket.profileIds().contains(uuid); + } + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImplTest.java new file mode 100644 index 00000000..d5e830cb --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundPlayerInfoUpdatePacketImplTest.java @@ -0,0 +1,61 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoUpdatePacketImplTest { + + @Test + void createPacket() { + // Setup packet + EnumSet actions = EnumSet.noneOf(FS_ClientboundPlayerInfoUpdatePacket.Action.class); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + + FS_GameProfile gameProfile = new FS_GameProfile(UUID.randomUUID(), "Test name"); + boolean listed = true; + int latency = 42; + FS_GameType gameMode = FS_GameType.SURVIVAL; + Component displayName = Component.text("Test displayname"); + + List entries = new ArrayList<>(); + entries.add(new FS_ClientboundPlayerInfoUpdatePacket.Entry( + gameProfile.getUUID(), + gameProfile, + listed, + latency, + gameMode, + displayName + )); + + ClientboundPlayerInfoUpdatePacketImpl packet = new ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + + ClientboundPlayerInfoUpdatePacket createdPacket = (ClientboundPlayerInfoUpdatePacket) packet.createPacket(); + + assert createdPacket.entries().size() == 1; + assert createdPacket.actions().size() == 3; + + // check entry + ClientboundPlayerInfoUpdatePacket.Entry entry = createdPacket.entries().getFirst(); + assert entry.profile().getId().equals(gameProfile.getUUID()); + assert entry.profile().getName().equals(gameProfile.getName()); + assert entry.listed() == listed; + assert entry.latency() == latency; + assert entry.gameMode().getId() == gameMode.getId(); + + // check actions + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImplTest.java new file mode 100644 index 00000000..1fadcfd1 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRemoveEntitiesPacketImplTest.java @@ -0,0 +1,20 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ClientboundRemoveEntitiesPacketImplTest { + + @Test + void createPacket() { + List entityIds = List.of(95, 120, 154, 187); + + ClientboundRemoveEntitiesPacketImpl packet = new ClientboundRemoveEntitiesPacketImpl(entityIds); + ClientboundRemoveEntitiesPacket createdPacket = (ClientboundRemoveEntitiesPacket) packet.createPacket(); + + assert createdPacket.getEntityIds().size() == entityIds.size(); + assert createdPacket.getEntityIds().containsAll(entityIds); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImplTest.java new file mode 100644 index 00000000..632cc6db --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundRotateHeadPacketImplTest.java @@ -0,0 +1,21 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import org.junit.jupiter.api.Test; + +class ClientboundRotateHeadPacketImplTest { + + @Test + void createPacket() throws Exception { + int entityId = 184; + float headYaw = 45; + + ClientboundRotateHeadPacketImpl packet = new ClientboundRotateHeadPacketImpl(entityId, (byte) headYaw); + ClientboundRotateHeadPacket createdPacket = (ClientboundRotateHeadPacket) packet.createPacket(); + + assert ReflectionUtils.getField(createdPacket, "entityId").equals(entityId); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImplTest.java new file mode 100644 index 00000000..96c8a24b --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEntityDataPacketImplTest.java @@ -0,0 +1,28 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_TextDisplayData; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; + +import java.util.List; + +class ClientboundSetEntityDataPacketImplTest { + + //TODO: Fix this test (using registry) +// @Test + void createPacket() { + int entityId = 712; + List entityData = List.of( + new FS_ClientboundSetEntityDataPacket.EntityData( + FS_TextDisplayData.TEXT, + "Hello, World!" + ) + ); + + ClientboundSetEntityDataPacketImpl packet = new ClientboundSetEntityDataPacketImpl(entityId, entityData); + ClientboundSetEntityDataPacket createdPacket = (ClientboundSetEntityDataPacket) packet.createPacket(); + + assert createdPacket.id() == entityId; + assert createdPacket.packedItems().size() == 1; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImplTest.java new file mode 100644 index 00000000..4206614a --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetEquipmentPacketImplTest.java @@ -0,0 +1,28 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +class ClientboundSetEquipmentPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + // Setup packet + Map equipment = Map.of( + FS_EquipmentSlot.MAINHAND, new ItemStack(Material.DIAMOND_SWORD), + FS_EquipmentSlot.OFFHAND, new ItemStack(Material.SHIELD), + FS_EquipmentSlot.HEAD, new ItemStack(Material.DIAMOND_HELMET) + ); + + ClientboundSetEquipmentPacketImpl packet = new ClientboundSetEquipmentPacketImpl(42, equipment); + ClientboundSetEquipmentPacket createdPacket = (ClientboundSetEquipmentPacket) packet.createPacket(); + + assert createdPacket.getEntity() == 42; + assert createdPacket.getSlots().size() == 3; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImplTest.java new file mode 100644 index 00000000..91c2aa96 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundSetPassengersPacketImplTest.java @@ -0,0 +1,28 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ClientboundSetPassengersPacketImplTest { + + @Test + void createPacket() { + // Setup packet + int vehicleID = 712; + List passengers = new ArrayList<>(); + passengers.add(571); + passengers.add(572); + + ClientboundSetPassengersPacketImpl packet = new ClientboundSetPassengersPacketImpl(vehicleID, passengers); + ClientboundSetPassengersPacket createdPacket = (ClientboundSetPassengersPacket) packet.createPacket(); + + // Check packet + assert createdPacket.getVehicle() == vehicleID; + assert createdPacket.getPassengers().length == 2; + assert createdPacket.getPassengers()[0] == 571; + assert createdPacket.getPassengers()[1] == 572; + } +} diff --git a/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImplTest.java b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImplTest.java new file mode 100644 index 00000000..84991ce9 --- /dev/null +++ b/libraries/packets/implementations/1_20_6/src/test/java/de/oliver/fancysitula/versions/v1_20_6/packets/ClientboundTeleportEntityPacketImplTest.java @@ -0,0 +1,31 @@ +package de.oliver.fancysitula.versions.v1_20_6.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import org.junit.jupiter.api.Test; + +class ClientboundTeleportEntityPacketImplTest { + + @Test + void createPacket() { + int entityId = 4313; + double x = 15.0; + double y = 57.0; + double z = -27.0; + float yaw = 90.0f; + float pitch = 45.0f; + boolean onGround = true; + + ClientboundTeleportEntityPacketImpl packet = new ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + ClientboundTeleportEntityPacket createdPacket = (ClientboundTeleportEntityPacket) packet.createPacket(); + + assert createdPacket != null; + assert createdPacket.getId() == entityId; + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getyRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getxRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.isOnGround() == onGround; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/build.gradle.kts b/libraries/packets/implementations/1_21/build.gradle.kts new file mode 100644 index 00000000..82bfe0e9 --- /dev/null +++ b/libraries/packets/implementations/1_21/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("java-library") + id("io.papermc.paperweight.userdev") +} + +val minecraftVersion = "1.21" + +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +dependencies { + paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT") + compileOnly(project(":libraries:packets:api")) + + testImplementation(project(":libraries:packets:api")) + testImplementation(project(":libraries:packets:implementations:1_20_6")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") +} + +tasks { + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundAddEntityPacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundAddEntityPacketImplTest.java new file mode 100644 index 00000000..49b7449a --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundAddEntityPacketImplTest.java @@ -0,0 +1,65 @@ +package packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundAddEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +class ClientboundAddEntityPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + int entityId = 10000; + UUID entityUUID = UUID.randomUUID(); + EntityType entityType = EntityType.PIG; + + double x = 5; + double y = 57; + double z = 203; + + float yaw = 142; + float pitch = 247; + float headYaw = 90; + + int velocityX = 0; + int velocityY = 0; + int velocityZ = 0; + + int data = 0; + + ClientboundAddEntityPacketImpl packet = new ClientboundAddEntityPacketImpl( + entityId, + entityUUID, + entityType, + x, + y, + z, + yaw, + pitch, + headYaw, + velocityX, + velocityY, + velocityZ, + data + ); + + ClientboundAddEntityPacket createdPacket = (ClientboundAddEntityPacket) packet.createPacket(); + + assert createdPacket.getId() == entityId; + assert createdPacket.getUUID().equals(entityUUID); + assert createdPacket.getType().getDescriptionId().equals(entityType.getKey().getKey()); + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getYRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getXRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + assert createdPacket.getXa() == velocityX; + assert createdPacket.getYa() == velocityY; + assert createdPacket.getZa() == velocityZ; + assert createdPacket.getData() == data; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java new file mode 100644 index 00000000..e938af2f --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java @@ -0,0 +1,23 @@ +package packets; + +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundPlayerInfoRemovePacketImpl; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoRemovePacketImplTest { + + @Test + void createPacket() { + List uuids = List.of(UUID.randomUUID(), UUID.randomUUID()); + + ClientboundPlayerInfoRemovePacketImpl packet = new ClientboundPlayerInfoRemovePacketImpl(uuids); + ClientboundPlayerInfoRemovePacket vanillaPacket = (ClientboundPlayerInfoRemovePacket) packet.createPacket(); + + for (UUID uuid : uuids) { + assert vanillaPacket.profileIds().contains(uuid); + } + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java new file mode 100644 index 00000000..440e284b --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java @@ -0,0 +1,62 @@ +package packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundPlayerInfoUpdatePacketImpl; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoUpdatePacketImplTest { + + @Test + void createPacket() { + // Setup packet + EnumSet actions = EnumSet.noneOf(FS_ClientboundPlayerInfoUpdatePacket.Action.class); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + + FS_GameProfile gameProfile = new FS_GameProfile(UUID.randomUUID(), "Test name"); + boolean listed = true; + int latency = 42; + FS_GameType gameMode = FS_GameType.SURVIVAL; + Component displayName = Component.text("Test displayname"); + + List entries = new ArrayList<>(); + entries.add(new FS_ClientboundPlayerInfoUpdatePacket.Entry( + gameProfile.getUUID(), + gameProfile, + listed, + latency, + gameMode, + displayName + )); + + ClientboundPlayerInfoUpdatePacketImpl packet = new ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + + ClientboundPlayerInfoUpdatePacket createdPacket = (ClientboundPlayerInfoUpdatePacket) packet.createPacket(); + + assert createdPacket.entries().size() == 1; + assert createdPacket.actions().size() == 3; + + // check entry + ClientboundPlayerInfoUpdatePacket.Entry entry = createdPacket.entries().getFirst(); + assert entry.profile().getId().equals(gameProfile.getUUID()); + assert entry.profile().getName().equals(gameProfile.getName()); + assert entry.listed() == listed; + assert entry.latency() == latency; + assert entry.gameMode().getId() == gameMode.getId(); + + // check actions + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java new file mode 100644 index 00000000..3a8833df --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java @@ -0,0 +1,21 @@ +package packets; + +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundRemoveEntitiesPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ClientboundRemoveEntitiesPacketImplTest { + + @Test + void createPacket() { + List entityIds = List.of(95, 120, 154, 187); + + ClientboundRemoveEntitiesPacketImpl packet = new ClientboundRemoveEntitiesPacketImpl(entityIds); + ClientboundRemoveEntitiesPacket createdPacket = (ClientboundRemoveEntitiesPacket) packet.createPacket(); + + assert createdPacket.getEntityIds().size() == entityIds.size(); + assert createdPacket.getEntityIds().containsAll(entityIds); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java new file mode 100644 index 00000000..630897c1 --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java @@ -0,0 +1,22 @@ +package packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundRotateHeadPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import org.junit.jupiter.api.Test; + +class ClientboundRotateHeadPacketImplTest { + + @Test + void createPacket() throws Exception { + int entityId = 184; + float headYaw = 45; + + ClientboundRotateHeadPacketImpl packet = new ClientboundRotateHeadPacketImpl(entityId, (byte) headYaw); + ClientboundRotateHeadPacket createdPacket = (ClientboundRotateHeadPacket) packet.createPacket(); + + assert ReflectionUtils.getField(createdPacket, "entityId").equals(entityId); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java new file mode 100644 index 00000000..c92cf6f6 --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java @@ -0,0 +1,29 @@ +package packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_TextDisplayData; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetEntityDataPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; + +import java.util.List; + +class ClientboundSetEntityDataPacketImplTest { + + //TODO: Fix this test (using registry) +// @Test + void createPacket() { + int entityId = 712; + List entityData = List.of( + new FS_ClientboundSetEntityDataPacket.EntityData( + FS_TextDisplayData.TEXT, + "Hello, World!" + ) + ); + + ClientboundSetEntityDataPacketImpl packet = new ClientboundSetEntityDataPacketImpl(entityId, entityData); + ClientboundSetEntityDataPacket createdPacket = (ClientboundSetEntityDataPacket) packet.createPacket(); + + assert createdPacket.id() == entityId; + assert createdPacket.packedItems().size() == 1; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java new file mode 100644 index 00000000..0f8f151d --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java @@ -0,0 +1,29 @@ +package packets; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetEquipmentPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +class ClientboundSetEquipmentPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + // Setup packet + Map equipment = Map.of( + FS_EquipmentSlot.MAINHAND, new ItemStack(Material.DIAMOND_SWORD), + FS_EquipmentSlot.OFFHAND, new ItemStack(Material.SHIELD), + FS_EquipmentSlot.HEAD, new ItemStack(Material.DIAMOND_HELMET) + ); + + ClientboundSetEquipmentPacketImpl packet = new ClientboundSetEquipmentPacketImpl(42, equipment); + ClientboundSetEquipmentPacket createdPacket = (ClientboundSetEquipmentPacket) packet.createPacket(); + + assert createdPacket.getEntity() == 42; + assert createdPacket.getSlots().size() == 3; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java new file mode 100644 index 00000000..e7ee990d --- /dev/null +++ b/libraries/packets/implementations/1_21/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java @@ -0,0 +1,32 @@ +package packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundTeleportEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import org.junit.jupiter.api.Test; + +class ClientboundTeleportEntityPacketImplTest { + + @Test + void createPacket() { + int entityId = 4313; + double x = 15.0; + double y = 57.0; + double z = -27.0; + float yaw = 90.0f; + float pitch = 45.0f; + boolean onGround = true; + + ClientboundTeleportEntityPacketImpl packet = new ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + ClientboundTeleportEntityPacket createdPacket = (ClientboundTeleportEntityPacket) packet.createPacket(); + + assert createdPacket != null; + assert createdPacket.getId() == entityId; + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getyRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getxRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.isOnGround() == onGround; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/build.gradle.kts b/libraries/packets/implementations/1_21_1/build.gradle.kts new file mode 100644 index 00000000..579f7752 --- /dev/null +++ b/libraries/packets/implementations/1_21_1/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("java-library") + id("io.papermc.paperweight.userdev") +} + +val minecraftVersion = "1.21.1" + +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +dependencies { + paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT") + compileOnly(project(":libraries:packets:api")) + + testImplementation(project(":libraries:packets:api")) + testImplementation(project(":libraries:packets:implementations:1_20_6")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") +} + +tasks { + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundAddEntityPacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundAddEntityPacketImplTest.java new file mode 100644 index 00000000..49b7449a --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundAddEntityPacketImplTest.java @@ -0,0 +1,65 @@ +package packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundAddEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +class ClientboundAddEntityPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + int entityId = 10000; + UUID entityUUID = UUID.randomUUID(); + EntityType entityType = EntityType.PIG; + + double x = 5; + double y = 57; + double z = 203; + + float yaw = 142; + float pitch = 247; + float headYaw = 90; + + int velocityX = 0; + int velocityY = 0; + int velocityZ = 0; + + int data = 0; + + ClientboundAddEntityPacketImpl packet = new ClientboundAddEntityPacketImpl( + entityId, + entityUUID, + entityType, + x, + y, + z, + yaw, + pitch, + headYaw, + velocityX, + velocityY, + velocityZ, + data + ); + + ClientboundAddEntityPacket createdPacket = (ClientboundAddEntityPacket) packet.createPacket(); + + assert createdPacket.getId() == entityId; + assert createdPacket.getUUID().equals(entityUUID); + assert createdPacket.getType().getDescriptionId().equals(entityType.getKey().getKey()); + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getYRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getXRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + assert createdPacket.getXa() == velocityX; + assert createdPacket.getYa() == velocityY; + assert createdPacket.getZa() == velocityZ; + assert createdPacket.getData() == data; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java new file mode 100644 index 00000000..e938af2f --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoRemovePacketImplTest.java @@ -0,0 +1,23 @@ +package packets; + +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundPlayerInfoRemovePacketImpl; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoRemovePacketImplTest { + + @Test + void createPacket() { + List uuids = List.of(UUID.randomUUID(), UUID.randomUUID()); + + ClientboundPlayerInfoRemovePacketImpl packet = new ClientboundPlayerInfoRemovePacketImpl(uuids); + ClientboundPlayerInfoRemovePacket vanillaPacket = (ClientboundPlayerInfoRemovePacket) packet.createPacket(); + + for (UUID uuid : uuids) { + assert vanillaPacket.profileIds().contains(uuid); + } + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java new file mode 100644 index 00000000..440e284b --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundPlayerInfoUpdatePacketImplTest.java @@ -0,0 +1,62 @@ +package packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundPlayerInfoUpdatePacketImpl; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoUpdatePacketImplTest { + + @Test + void createPacket() { + // Setup packet + EnumSet actions = EnumSet.noneOf(FS_ClientboundPlayerInfoUpdatePacket.Action.class); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + + FS_GameProfile gameProfile = new FS_GameProfile(UUID.randomUUID(), "Test name"); + boolean listed = true; + int latency = 42; + FS_GameType gameMode = FS_GameType.SURVIVAL; + Component displayName = Component.text("Test displayname"); + + List entries = new ArrayList<>(); + entries.add(new FS_ClientboundPlayerInfoUpdatePacket.Entry( + gameProfile.getUUID(), + gameProfile, + listed, + latency, + gameMode, + displayName + )); + + ClientboundPlayerInfoUpdatePacketImpl packet = new ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + + ClientboundPlayerInfoUpdatePacket createdPacket = (ClientboundPlayerInfoUpdatePacket) packet.createPacket(); + + assert createdPacket.entries().size() == 1; + assert createdPacket.actions().size() == 3; + + // check entry + ClientboundPlayerInfoUpdatePacket.Entry entry = createdPacket.entries().getFirst(); + assert entry.profile().getId().equals(gameProfile.getUUID()); + assert entry.profile().getName().equals(gameProfile.getName()); + assert entry.listed() == listed; + assert entry.latency() == latency; + assert entry.gameMode().getId() == gameMode.getId(); + + // check actions + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java new file mode 100644 index 00000000..3a8833df --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRemoveEntitiesPacketImplTest.java @@ -0,0 +1,21 @@ +package packets; + +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundRemoveEntitiesPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ClientboundRemoveEntitiesPacketImplTest { + + @Test + void createPacket() { + List entityIds = List.of(95, 120, 154, 187); + + ClientboundRemoveEntitiesPacketImpl packet = new ClientboundRemoveEntitiesPacketImpl(entityIds); + ClientboundRemoveEntitiesPacket createdPacket = (ClientboundRemoveEntitiesPacket) packet.createPacket(); + + assert createdPacket.getEntityIds().size() == entityIds.size(); + assert createdPacket.getEntityIds().containsAll(entityIds); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java new file mode 100644 index 00000000..630897c1 --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundRotateHeadPacketImplTest.java @@ -0,0 +1,22 @@ +package packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundRotateHeadPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import org.junit.jupiter.api.Test; + +class ClientboundRotateHeadPacketImplTest { + + @Test + void createPacket() throws Exception { + int entityId = 184; + float headYaw = 45; + + ClientboundRotateHeadPacketImpl packet = new ClientboundRotateHeadPacketImpl(entityId, (byte) headYaw); + ClientboundRotateHeadPacket createdPacket = (ClientboundRotateHeadPacket) packet.createPacket(); + + assert ReflectionUtils.getField(createdPacket, "entityId").equals(entityId); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java new file mode 100644 index 00000000..c92cf6f6 --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEntityDataPacketImplTest.java @@ -0,0 +1,29 @@ +package packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_TextDisplayData; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetEntityDataPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; + +import java.util.List; + +class ClientboundSetEntityDataPacketImplTest { + + //TODO: Fix this test (using registry) +// @Test + void createPacket() { + int entityId = 712; + List entityData = List.of( + new FS_ClientboundSetEntityDataPacket.EntityData( + FS_TextDisplayData.TEXT, + "Hello, World!" + ) + ); + + ClientboundSetEntityDataPacketImpl packet = new ClientboundSetEntityDataPacketImpl(entityId, entityData); + ClientboundSetEntityDataPacket createdPacket = (ClientboundSetEntityDataPacket) packet.createPacket(); + + assert createdPacket.id() == entityId; + assert createdPacket.packedItems().size() == 1; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java new file mode 100644 index 00000000..0f8f151d --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundSetEquipmentPacketImplTest.java @@ -0,0 +1,29 @@ +package packets; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundSetEquipmentPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +class ClientboundSetEquipmentPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + // Setup packet + Map equipment = Map.of( + FS_EquipmentSlot.MAINHAND, new ItemStack(Material.DIAMOND_SWORD), + FS_EquipmentSlot.OFFHAND, new ItemStack(Material.SHIELD), + FS_EquipmentSlot.HEAD, new ItemStack(Material.DIAMOND_HELMET) + ); + + ClientboundSetEquipmentPacketImpl packet = new ClientboundSetEquipmentPacketImpl(42, equipment); + ClientboundSetEquipmentPacket createdPacket = (ClientboundSetEquipmentPacket) packet.createPacket(); + + assert createdPacket.getEntity() == 42; + assert createdPacket.getSlots().size() == 3; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java new file mode 100644 index 00000000..e7ee990d --- /dev/null +++ b/libraries/packets/implementations/1_21_1/src/test/java/packets/ClientboundTeleportEntityPacketImplTest.java @@ -0,0 +1,32 @@ +package packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_20_6.packets.ClientboundTeleportEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import org.junit.jupiter.api.Test; + +class ClientboundTeleportEntityPacketImplTest { + + @Test + void createPacket() { + int entityId = 4313; + double x = 15.0; + double y = 57.0; + double z = -27.0; + float yaw = 90.0f; + float pitch = 45.0f; + boolean onGround = true; + + ClientboundTeleportEntityPacketImpl packet = new ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + ClientboundTeleportEntityPacket createdPacket = (ClientboundTeleportEntityPacket) packet.createPacket(); + + assert createdPacket != null; + assert createdPacket.getId() == entityId; + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getyRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getxRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.isOnGround() == onGround; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/build.gradle.kts b/libraries/packets/implementations/1_21_3/build.gradle.kts new file mode 100644 index 00000000..551ef7ca --- /dev/null +++ b/libraries/packets/implementations/1_21_3/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("java-library") + id("io.papermc.paperweight.userdev") +} + +val minecraftVersion = "1.21.3" + +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +dependencies { + paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT") + compileOnly(project(":libraries:packets:api")) + + testImplementation(project(":libraries:packets:api")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") +} + +tasks { + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImpl.java new file mode 100644 index 00000000..44572a02 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImpl.java @@ -0,0 +1,48 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundAddEntityPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +public class ClientboundAddEntityPacketImpl extends FS_ClientboundAddEntityPacket { + + public ClientboundAddEntityPacketImpl(int entityId, UUID entityUUID, EntityType entityType, double x, double y, double z, float yaw, float pitch, float headYaw, int velocityX, int velocityY, int velocityZ, int data) { + super(entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + + @Override + public Object createPacket() { + net.minecraft.world.entity.EntityType vanillaType = BuiltInRegistries.ENTITY_TYPE.getValue(CraftNamespacedKey.toMinecraft(entityType.getKey())); + + return new ClientboundAddEntityPacket( + entityId, + entityUUID, + x, + y, + z, + AngelConverter.degreesToVanillaByte(pitch), + AngelConverter.degreesToVanillaByte(yaw), + vanillaType, + data, + new Vec3(velocityX, velocityY, velocityZ), + AngelConverter.degreesToVanillaByte(headYaw) + ); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundAddEntityPacket packet = (ClientboundAddEntityPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundCreateOrUpdateTeamPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundCreateOrUpdateTeamPacketImpl.java new file mode 100644 index 00000000..39201c61 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundCreateOrUpdateTeamPacketImpl.java @@ -0,0 +1,128 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundCreateOrUpdateTeamPacket; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.ChatFormatting; +import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.Team; + +public class ClientboundCreateOrUpdateTeamPacketImpl extends FS_ClientboundCreateOrUpdateTeamPacket { + + private static final Scoreboard SCOREBOARD = new Scoreboard(); + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, CreateTeam createTeam) { + super(teamName, createTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, RemoveTeam removeTeam) { + super(teamName, removeTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, UpdateTeam updateTeam) { + super(teamName, updateTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, AddEntity addEntity) { + super(teamName, addEntity); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, RemoveEntity removeEntity) { + super(teamName, removeEntity); + } + + @Override + public Object createPacket() { + return switch (method) { + case CREATE_TEAM -> createCreateTeamPacket(); + case REMOVE_TEAM -> createRemoveTeamPacket(); + case UPDATE_TEAM -> createUpdateTeamPacket(); + case ADD_ENTITY -> createAddEntityPacket(); + case REMOVE_ENTITY -> createRemoveEntityPacket(); + }; + } + + private Object createCreateTeamPacket() { + if (createTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + playerTeam.setDisplayName(PaperAdventure.asVanilla(createTeam.getDisplayName())); + playerTeam.setAllowFriendlyFire(createTeam.isAllowFriendlyFire()); + playerTeam.setSeeFriendlyInvisibles(createTeam.isCanSeeFriendlyInvisibles()); + playerTeam.setNameTagVisibility(Team.Visibility.byName(createTeam.getNameTagVisibility().getName())); + playerTeam.setCollisionRule(PlayerTeam.CollisionRule.byName(createTeam.getCollisionRule().getName())); + playerTeam.setColor(ChatFormatting.getById(createTeam.getColor().getId())); + playerTeam.setPlayerPrefix(PaperAdventure.asVanilla(createTeam.getPrefix())); + playerTeam.setPlayerSuffix(PaperAdventure.asVanilla(createTeam.getSuffix())); + for (String entity : createTeam.getEntities()) { + playerTeam.getPlayers().add(entity); + } + + return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true); + } + + private Object createRemoveTeamPacket() { + if (removeTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + return ClientboundSetPlayerTeamPacket.createRemovePacket(playerTeam); + } + + private Object createUpdateTeamPacket() { + if (updateTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + playerTeam.setDisplayName(PaperAdventure.asVanilla(updateTeam.getDisplayName())); + playerTeam.setAllowFriendlyFire(updateTeam.isAllowFriendlyFire()); + playerTeam.setSeeFriendlyInvisibles(updateTeam.isCanSeeFriendlyInvisibles()); + playerTeam.setNameTagVisibility(Team.Visibility.byName(updateTeam.getNameTagVisibility().getName())); + playerTeam.setCollisionRule(PlayerTeam.CollisionRule.byName(updateTeam.getCollisionRule().getName())); + playerTeam.setColor(ChatFormatting.getById(updateTeam.getColor().getId())); + playerTeam.setPlayerPrefix(PaperAdventure.asVanilla(updateTeam.getPrefix())); + playerTeam.setPlayerSuffix(PaperAdventure.asVanilla(updateTeam.getSuffix())); + + return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true); + } + + private Object createAddEntityPacket() { + if (addEntity == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + for (String entity : addEntity.getEntities()) { + playerTeam.getPlayers().add(entity); + } + return ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(playerTeam, addEntity.getEntities(), ClientboundSetPlayerTeamPacket.Action.ADD); + } + + private Object createRemoveEntityPacket() { + if (removeEntity == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + for (String entity : removeEntity.getEntities()) { + playerTeam.getPlayers().add(entity); + } + return ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(playerTeam, removeEntity.getEntities(), ClientboundSetPlayerTeamPacket.Action.REMOVE); + } + + @Override + protected void sendPacketTo(FS_RealPlayer player) { + ClientboundSetPlayerTeamPacket packet = (ClientboundSetPlayerTeamPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImpl.java new file mode 100644 index 00000000..b8fe8066 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImpl.java @@ -0,0 +1,30 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoRemovePacket; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.UUID; + +public class ClientboundPlayerInfoRemovePacketImpl extends FS_ClientboundPlayerInfoRemovePacket { + + public ClientboundPlayerInfoRemovePacketImpl(List uuids) { + super(uuids); + } + + @Override + public Object createPacket() { + return new ClientboundPlayerInfoRemovePacket(uuids); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundPlayerInfoRemovePacket packet = (ClientboundPlayerInfoRemovePacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImpl.java new file mode 100644 index 00000000..fe4d6c13 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImpl.java @@ -0,0 +1,53 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.versions.v1_21_3.utils.GameProfileImpl; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class ClientboundPlayerInfoUpdatePacketImpl extends FS_ClientboundPlayerInfoUpdatePacket { + + public ClientboundPlayerInfoUpdatePacketImpl(EnumSet actions, List entries) { + super(actions, entries); + } + + @Override + public Object createPacket() { + EnumSet vanillaActions = EnumSet.noneOf(ClientboundPlayerInfoUpdatePacket.Action.class); + for (FS_ClientboundPlayerInfoUpdatePacket.Action action : actions) { + vanillaActions.add(ClientboundPlayerInfoUpdatePacket.Action.valueOf(action.name())); + } + + List entries = new ArrayList<>(); + for (Entry entry : this.entries) { + entries.add(new ClientboundPlayerInfoUpdatePacket.Entry( + entry.uuid(), + GameProfileImpl.asVanilla(entry.profile()), + entry.listed(), + entry.latency(), + GameType.byId(entry.gameMode().getId()), + PaperAdventure.asVanilla(entry.displayName()), + -1, + null // TODO: Add ChatSession support + )); + } + + return new ClientboundPlayerInfoUpdatePacket(vanillaActions, entries); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundPlayerInfoUpdatePacket packet = (ClientboundPlayerInfoUpdatePacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImpl.java new file mode 100644 index 00000000..90671fd0 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImpl.java @@ -0,0 +1,37 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundRemoveEntitiesPacket; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class ClientboundRemoveEntitiesPacketImpl extends FS_ClientboundRemoveEntitiesPacket { + + /** + * @param entityIds IDs of the entities to remove + */ + public ClientboundRemoveEntitiesPacketImpl(List entityIds) { + super(entityIds); + } + + @Override + public Object createPacket() { + int[] ids = new int[this.entityIds.size()]; + for (int i = 0; i < this.entityIds.size(); i++) { + ids[i] = this.entityIds.get(i); + } + + return new ClientboundRemoveEntitiesPacket(ids); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundRemoveEntitiesPacket packet = (ClientboundRemoveEntitiesPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImpl.java new file mode 100644 index 00000000..0ccd04e5 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImpl.java @@ -0,0 +1,38 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundRotateHeadPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.server.level.ServerPlayer; + +public class ClientboundRotateHeadPacketImpl extends FS_ClientboundRotateHeadPacket { + + public ClientboundRotateHeadPacketImpl(int entityId, float headYaw) { + super(entityId, headYaw); + } + + @Override + public Object createPacket() { + ClientboundRotateHeadPacket packet = null; + + try { + packet = ReflectionUtils.createUnsafeInstance(ClientboundRotateHeadPacket.class); + ReflectionUtils.setFinalField(packet, "entityId", entityId); + ReflectionUtils.setFinalField(packet, "yHeadRot", AngelConverter.degreesToVanillaByte(headYaw)); + } catch (Exception e) { + e.printStackTrace(); + } + return packet; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundRotateHeadPacket packet = (ClientboundRotateHeadPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImpl.java new file mode 100644 index 00000000..09667e6f --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImpl.java @@ -0,0 +1,67 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.block.BlockState; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class ClientboundSetEntityDataPacketImpl extends FS_ClientboundSetEntityDataPacket { + + public ClientboundSetEntityDataPacketImpl(int entityId, List entityData) { + super(entityId, entityData); + } + + @Override + public Object createPacket() { + List> dataValues = new ArrayList<>(); + for (EntityData data : entityData) { + try { + Class entityClass = Class.forName(data.getAccessor().entityClassName()); + net.minecraft.network.syncher.EntityDataAccessor accessor = ReflectionUtils.getStaticField(entityClass, data.getAccessor().accessorFieldName()); + + Object vanillaValue = data.getValue(); + + if (data.getValue() == null) { + continue; + } + + if (data.getValue() instanceof Component c) { + vanillaValue = PaperAdventure.asVanilla(c); + } + + if (data.getValue() instanceof ItemStack i) { + vanillaValue = net.minecraft.world.item.ItemStack.fromBukkitCopy(i); + } + + if (data.getValue() instanceof BlockState b) { + vanillaValue = ((CraftBlockState) b).getHandle(); + } + + dataValues.add(SynchedEntityData.DataValue.create(accessor, vanillaValue)); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + return new ClientboundSetEntityDataPacket(entityId, dataValues); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetEntityDataPacket packet = (ClientboundSetEntityDataPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImpl.java new file mode 100644 index 00000000..cee9a61b --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImpl.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import com.mojang.datafixers.util.Pair; +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEquipmentPacket; +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ClientboundSetEquipmentPacketImpl extends FS_ClientboundSetEquipmentPacket { + + public ClientboundSetEquipmentPacketImpl(int entityId, Map equipment) { + super(entityId, equipment); + } + + @Override + public Object createPacket() { + List> slots = new ArrayList<>(); + + for (Map.Entry entry : equipment.entrySet()) { + EquipmentSlot equipmentSlot = net.minecraft.world.entity.EquipmentSlot.byName(entry.getKey().name().toLowerCase()); + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.asNMSCopy(entry.getValue()); + + slots.add(Pair.of(equipmentSlot, itemStack)); + } + + return new ClientboundSetEquipmentPacket(entityId, slots); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetEquipmentPacket packet = (ClientboundSetEquipmentPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImpl.java new file mode 100644 index 00000000..20b84206 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImpl.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetPassengersPacket; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class ClientboundSetPassengersPacketImpl extends FS_ClientboundSetPassengersPacket { + + public ClientboundSetPassengersPacketImpl(int entityId, List passengers) { + super(entityId, passengers); + } + + + @Override + public Object createPacket() { + int[] passengers = new int[this.passengers.size()]; + for (int i = 0; i < this.passengers.size(); i++) { + passengers[i] = this.passengers.get(i); + } + + try { + ClientboundSetPassengersPacket packet = ReflectionUtils.createUnsafeInstance(ClientboundSetPassengersPacket.class); + ReflectionUtils.setFinalField(packet, "vehicle", entityId); + ReflectionUtils.setFinalField(packet, "passengers", passengers); + return packet; + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetPassengersPacket packet = (ClientboundSetPassengersPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImpl.java new file mode 100644 index 00000000..390fe39a --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImpl.java @@ -0,0 +1,43 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundTeleportEntityPacket; +import de.oliver.fancysitula.versions.v1_21_3.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.PositionMoveRotation; +import net.minecraft.world.phys.Vec3; + +import java.util.Set; + +public class ClientboundTeleportEntityPacketImpl extends FS_ClientboundTeleportEntityPacket { + + public ClientboundTeleportEntityPacketImpl(int entityId, double x, double y, double z, float yaw, float pitch, boolean onGround) { + super(entityId, x, y, z, yaw, pitch, onGround); + } + + @Override + public Object createPacket() { + ClientboundTeleportEntityPacket packet = new ClientboundTeleportEntityPacket( + entityId, + new PositionMoveRotation( + new Vec3(x, y, z), + Vec3.ZERO, + yaw, + pitch + ), + Set.of(), + onGround + ); + + return packet; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundTeleportEntityPacket packet = (ClientboundTeleportEntityPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/GameProfileImpl.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/GameProfileImpl.java new file mode 100644 index 00000000..0cc5fb32 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/GameProfileImpl.java @@ -0,0 +1,33 @@ +package de.oliver.fancysitula.versions.v1_21_3.utils; + +import com.mojang.authlib.GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameProfile; + +import java.util.Map; + +public class GameProfileImpl { + + public static GameProfile asVanilla(FS_GameProfile gameProfile) { + GameProfile gf = new GameProfile(gameProfile.getUUID(), gameProfile.getName()); + + for (Map.Entry entry : gameProfile.getProperties().entrySet()) { + FS_GameProfile.Property property = entry.getValue(); + + gf.getProperties().put(entry.getKey(), new com.mojang.authlib.properties.Property(property.name(), property.value(), property.signature())); + } + + return gf; + } + + public static FS_GameProfile fromVanilla(GameProfile gameProfile) { + FS_GameProfile fsGameProfile = new FS_GameProfile(gameProfile.getId(), gameProfile.getName()); + + for (Map.Entry entry : gameProfile.getProperties().entries()) { + com.mojang.authlib.properties.Property property = entry.getValue(); + + fsGameProfile.getProperties().put(entry.getKey(), new FS_GameProfile.Property(property.name(), property.value(), property.signature())); + } + + return fsGameProfile; + } +} diff --git a/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/VanillaPlayerAdapter.java b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/VanillaPlayerAdapter.java new file mode 100644 index 00000000..9d6f0007 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/main/java/de/oliver/fancysitula/versions/v1_21_3/utils/VanillaPlayerAdapter.java @@ -0,0 +1,12 @@ +package de.oliver.fancysitula.versions.v1_21_3.utils; + +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; + +public class VanillaPlayerAdapter { + + public static ServerPlayer asVanilla(Player p) { + return ((CraftPlayer) p).getHandle(); + } +} diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImplTest.java new file mode 100644 index 00000000..1cb95bb3 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundAddEntityPacketImplTest.java @@ -0,0 +1,65 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundAddEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +class ClientboundAddEntityPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + int entityId = 10000; + UUID entityUUID = UUID.randomUUID(); + EntityType entityType = EntityType.PIG; + + double x = 5; + double y = 57; + double z = 203; + + float yaw = 142; + float pitch = 247; + float headYaw = 90; + + int velocityX = 0; + int velocityY = 0; + int velocityZ = 0; + + int data = 0; + + ClientboundAddEntityPacketImpl packet = new ClientboundAddEntityPacketImpl( + entityId, + entityUUID, + entityType, + x, + y, + z, + yaw, + pitch, + headYaw, + velocityX, + velocityY, + velocityZ, + data + ); + + ClientboundAddEntityPacket createdPacket = (ClientboundAddEntityPacket) packet.createPacket(); + + assert createdPacket.getId() == entityId; + assert createdPacket.getUUID().equals(entityUUID); + assert createdPacket.getType().getDescriptionId().equals(entityType.getKey().getKey()); + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getYRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getXRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + assert createdPacket.getXa() == velocityX; + assert createdPacket.getYa() == velocityY; + assert createdPacket.getZa() == velocityZ; + assert createdPacket.getData() == data; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImplTest.java new file mode 100644 index 00000000..cc94a4d5 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoRemovePacketImplTest.java @@ -0,0 +1,23 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundPlayerInfoRemovePacketImpl; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoRemovePacketImplTest { + + @Test + void createPacket() { + List uuids = List.of(UUID.randomUUID(), UUID.randomUUID()); + + ClientboundPlayerInfoRemovePacketImpl packet = new ClientboundPlayerInfoRemovePacketImpl(uuids); + ClientboundPlayerInfoRemovePacket vanillaPacket = (ClientboundPlayerInfoRemovePacket) packet.createPacket(); + + for (UUID uuid : uuids) { + assert vanillaPacket.profileIds().contains(uuid); + } + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImplTest.java new file mode 100644 index 00000000..a6ad9e34 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundPlayerInfoUpdatePacketImplTest.java @@ -0,0 +1,62 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundPlayerInfoUpdatePacketImpl; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoUpdatePacketImplTest { + + @Test + void createPacket() { + // Setup packet + EnumSet actions = EnumSet.noneOf(FS_ClientboundPlayerInfoUpdatePacket.Action.class); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + + FS_GameProfile gameProfile = new FS_GameProfile(UUID.randomUUID(), "Test name"); + boolean listed = true; + int latency = 42; + FS_GameType gameMode = FS_GameType.SURVIVAL; + Component displayName = Component.text("Test displayname"); + + List entries = new ArrayList<>(); + entries.add(new FS_ClientboundPlayerInfoUpdatePacket.Entry( + gameProfile.getUUID(), + gameProfile, + listed, + latency, + gameMode, + displayName + )); + + ClientboundPlayerInfoUpdatePacketImpl packet = new ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + + ClientboundPlayerInfoUpdatePacket createdPacket = (ClientboundPlayerInfoUpdatePacket) packet.createPacket(); + + assert createdPacket.entries().size() == 1; + assert createdPacket.actions().size() == 3; + + // check entry + ClientboundPlayerInfoUpdatePacket.Entry entry = createdPacket.entries().getFirst(); + assert entry.profile().getId().equals(gameProfile.getUUID()); + assert entry.profile().getName().equals(gameProfile.getName()); + assert entry.listed() == listed; + assert entry.latency() == latency; + assert entry.gameMode().getId() == gameMode.getId(); + + // check actions + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImplTest.java new file mode 100644 index 00000000..bb71e5c8 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRemoveEntitiesPacketImplTest.java @@ -0,0 +1,21 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundRemoveEntitiesPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ClientboundRemoveEntitiesPacketImplTest { + + @Test + void createPacket() { + List entityIds = List.of(95, 120, 154, 187); + + ClientboundRemoveEntitiesPacketImpl packet = new ClientboundRemoveEntitiesPacketImpl(entityIds); + ClientboundRemoveEntitiesPacket createdPacket = (ClientboundRemoveEntitiesPacket) packet.createPacket(); + + assert createdPacket.getEntityIds().size() == entityIds.size(); + assert createdPacket.getEntityIds().containsAll(entityIds); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImplTest.java new file mode 100644 index 00000000..cf0106d9 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundRotateHeadPacketImplTest.java @@ -0,0 +1,22 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundRotateHeadPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import org.junit.jupiter.api.Test; + +class ClientboundRotateHeadPacketImplTest { + + @Test + void createPacket() throws Exception { + int entityId = 184; + float headYaw = 45; + + ClientboundRotateHeadPacketImpl packet = new ClientboundRotateHeadPacketImpl(entityId, (byte) headYaw); + ClientboundRotateHeadPacket createdPacket = (ClientboundRotateHeadPacket) packet.createPacket(); + + assert ReflectionUtils.getField(createdPacket, "entityId").equals(entityId); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImplTest.java new file mode 100644 index 00000000..97bdaaa5 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEntityDataPacketImplTest.java @@ -0,0 +1,29 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_TextDisplayData; +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundSetEntityDataPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; + +import java.util.List; + +class ClientboundSetEntityDataPacketImplTest { + + //TODO: Fix this test (using registry) +// @Test + void createPacket() { + int entityId = 712; + List entityData = List.of( + new FS_ClientboundSetEntityDataPacket.EntityData( + FS_TextDisplayData.TEXT, + "Hello, World!" + ) + ); + + ClientboundSetEntityDataPacketImpl packet = new ClientboundSetEntityDataPacketImpl(entityId, entityData); + ClientboundSetEntityDataPacket createdPacket = (ClientboundSetEntityDataPacket) packet.createPacket(); + + assert createdPacket.id() == entityId; + assert createdPacket.packedItems().size() == 1; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImplTest.java new file mode 100644 index 00000000..943c9cc7 --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetEquipmentPacketImplTest.java @@ -0,0 +1,29 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundSetEquipmentPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +class ClientboundSetEquipmentPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + // Setup packet + Map equipment = Map.of( + FS_EquipmentSlot.MAINHAND, new ItemStack(Material.DIAMOND_SWORD), + FS_EquipmentSlot.OFFHAND, new ItemStack(Material.SHIELD), + FS_EquipmentSlot.HEAD, new ItemStack(Material.DIAMOND_HELMET) + ); + + ClientboundSetEquipmentPacketImpl packet = new ClientboundSetEquipmentPacketImpl(42, equipment); + ClientboundSetEquipmentPacket createdPacket = (ClientboundSetEquipmentPacket) packet.createPacket(); + + assert createdPacket.getEntity() == 42; + assert createdPacket.getSlots().size() == 3; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImplTest.java new file mode 100644 index 00000000..34bc26ee --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundSetPassengersPacketImplTest.java @@ -0,0 +1,29 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundSetPassengersPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ClientboundSetPassengersPacketImplTest { + + @Test + void createPacket() { + // Setup packet + int vehicleID = 712; + List passengers = new ArrayList<>(); + passengers.add(571); + passengers.add(572); + + ClientboundSetPassengersPacketImpl packet = new ClientboundSetPassengersPacketImpl(vehicleID, passengers); + ClientboundSetPassengersPacket createdPacket = (ClientboundSetPassengersPacket) packet.createPacket(); + + // Check packet + assert createdPacket.getVehicle() == vehicleID; + assert createdPacket.getPassengers().length == 2; + assert createdPacket.getPassengers()[0] == 571; + assert createdPacket.getPassengers()[1] == 572; + } +} diff --git a/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImplTest.java b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImplTest.java new file mode 100644 index 00000000..758e55ce --- /dev/null +++ b/libraries/packets/implementations/1_21_3/src/test/java/de/oliver/fancysitula/versions/v1_21_3/packets/ClientboundTeleportEntityPacketImplTest.java @@ -0,0 +1,32 @@ +package de.oliver.fancysitula.versions.v1_21_3.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_21_3.packets.ClientboundTeleportEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import org.junit.jupiter.api.Test; + +class ClientboundTeleportEntityPacketImplTest { + + @Test + void createPacket() { + int entityId = 4313; + double x = 15.0; + double y = 57.0; + double z = -27.0; + float yaw = 90.0f; + float pitch = 45.0f; + boolean onGround = true; + + ClientboundTeleportEntityPacketImpl packet = new ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + ClientboundTeleportEntityPacket createdPacket = (ClientboundTeleportEntityPacket) packet.createPacket(); + + assert createdPacket != null; + assert createdPacket.getId() == entityId; + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getyRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getxRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.isOnGround() == onGround; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/build.gradle.kts b/libraries/packets/implementations/1_21_4/build.gradle.kts new file mode 100644 index 00000000..966293a8 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("java-library") + id("io.papermc.paperweight.userdev") +} + +val minecraftVersion = "1.21.4" + +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + +dependencies { + paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT") + compileOnly(project(":libraries:packets:api")) + + testImplementation(project(":libraries:packets:api")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") +} + +tasks { + test { + useJUnitPlatform() + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImpl.java new file mode 100644 index 00000000..62136d8b --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImpl.java @@ -0,0 +1,48 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundAddEntityPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +public class ClientboundAddEntityPacketImpl extends FS_ClientboundAddEntityPacket { + + public ClientboundAddEntityPacketImpl(int entityId, UUID entityUUID, EntityType entityType, double x, double y, double z, float yaw, float pitch, float headYaw, int velocityX, int velocityY, int velocityZ, int data) { + super(entityId, entityUUID, entityType, x, y, z, yaw, pitch, headYaw, velocityX, velocityY, velocityZ, data); + } + + @Override + public Object createPacket() { + net.minecraft.world.entity.EntityType vanillaType = BuiltInRegistries.ENTITY_TYPE.getValue(CraftNamespacedKey.toMinecraft(entityType.getKey())); + + return new ClientboundAddEntityPacket( + entityId, + entityUUID, + x, + y, + z, + AngelConverter.degreesToVanillaByte(pitch), + AngelConverter.degreesToVanillaByte(yaw), + vanillaType, + data, + new Vec3(velocityX, velocityY, velocityZ), + AngelConverter.degreesToVanillaByte(headYaw) + ); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundAddEntityPacket packet = (ClientboundAddEntityPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundCreateOrUpdateTeamPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundCreateOrUpdateTeamPacketImpl.java new file mode 100644 index 00000000..3a54a586 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundCreateOrUpdateTeamPacketImpl.java @@ -0,0 +1,128 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundCreateOrUpdateTeamPacket; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.ChatFormatting; +import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.Team; + +public class ClientboundCreateOrUpdateTeamPacketImpl extends FS_ClientboundCreateOrUpdateTeamPacket { + + private static final Scoreboard SCOREBOARD = new Scoreboard(); + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, CreateTeam createTeam) { + super(teamName, createTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, RemoveTeam removeTeam) { + super(teamName, removeTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, UpdateTeam updateTeam) { + super(teamName, updateTeam); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, AddEntity addEntity) { + super(teamName, addEntity); + } + + public ClientboundCreateOrUpdateTeamPacketImpl(String teamName, RemoveEntity removeEntity) { + super(teamName, removeEntity); + } + + @Override + public Object createPacket() { + return switch (method) { + case CREATE_TEAM -> createCreateTeamPacket(); + case REMOVE_TEAM -> createRemoveTeamPacket(); + case UPDATE_TEAM -> createUpdateTeamPacket(); + case ADD_ENTITY -> createAddEntityPacket(); + case REMOVE_ENTITY -> createRemoveEntityPacket(); + }; + } + + private Object createCreateTeamPacket() { + if (createTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + playerTeam.setDisplayName(PaperAdventure.asVanilla(createTeam.getDisplayName())); + playerTeam.setAllowFriendlyFire(createTeam.isAllowFriendlyFire()); + playerTeam.setSeeFriendlyInvisibles(createTeam.isCanSeeFriendlyInvisibles()); + playerTeam.setNameTagVisibility(Team.Visibility.byName(createTeam.getNameTagVisibility().getName())); + playerTeam.setCollisionRule(PlayerTeam.CollisionRule.byName(createTeam.getCollisionRule().getName())); + playerTeam.setColor(ChatFormatting.getById(createTeam.getColor().getId())); + playerTeam.setPlayerPrefix(PaperAdventure.asVanilla(createTeam.getPrefix())); + playerTeam.setPlayerSuffix(PaperAdventure.asVanilla(createTeam.getSuffix())); + for (String entity : createTeam.getEntities()) { + playerTeam.getPlayers().add(entity); + } + + return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true); + } + + private Object createRemoveTeamPacket() { + if (removeTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + return ClientboundSetPlayerTeamPacket.createRemovePacket(playerTeam); + } + + private Object createUpdateTeamPacket() { + if (updateTeam == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + playerTeam.setDisplayName(PaperAdventure.asVanilla(updateTeam.getDisplayName())); + playerTeam.setAllowFriendlyFire(updateTeam.isAllowFriendlyFire()); + playerTeam.setSeeFriendlyInvisibles(updateTeam.isCanSeeFriendlyInvisibles()); + playerTeam.setNameTagVisibility(Team.Visibility.byName(updateTeam.getNameTagVisibility().getName())); + playerTeam.setCollisionRule(PlayerTeam.CollisionRule.byName(updateTeam.getCollisionRule().getName())); + playerTeam.setColor(ChatFormatting.getById(updateTeam.getColor().getId())); + playerTeam.setPlayerPrefix(PaperAdventure.asVanilla(updateTeam.getPrefix())); + playerTeam.setPlayerSuffix(PaperAdventure.asVanilla(updateTeam.getSuffix())); + + return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerTeam, true); + } + + private Object createAddEntityPacket() { + if (addEntity == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + for (String entity : addEntity.getEntities()) { + playerTeam.getPlayers().add(entity); + } + return ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(playerTeam, addEntity.getEntities(), ClientboundSetPlayerTeamPacket.Action.ADD); + } + + private Object createRemoveEntityPacket() { + if (removeEntity == null) { + return null; + } + + PlayerTeam playerTeam = new PlayerTeam(SCOREBOARD, teamName); + for (String entity : removeEntity.getEntities()) { + playerTeam.getPlayers().add(entity); + } + return ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(playerTeam, removeEntity.getEntities(), ClientboundSetPlayerTeamPacket.Action.REMOVE); + } + + @Override + protected void sendPacketTo(FS_RealPlayer player) { + ClientboundSetPlayerTeamPacket packet = (ClientboundSetPlayerTeamPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImpl.java new file mode 100644 index 00000000..7a758046 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImpl.java @@ -0,0 +1,30 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoRemovePacket; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.UUID; + +public class ClientboundPlayerInfoRemovePacketImpl extends FS_ClientboundPlayerInfoRemovePacket { + + public ClientboundPlayerInfoRemovePacketImpl(List uuids) { + super(uuids); + } + + @Override + public Object createPacket() { + return new ClientboundPlayerInfoRemovePacket(uuids); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundPlayerInfoRemovePacket packet = (ClientboundPlayerInfoRemovePacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImpl.java new file mode 100644 index 00000000..3143b436 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImpl.java @@ -0,0 +1,54 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.versions.v1_21_4.utils.GameProfileImpl; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class ClientboundPlayerInfoUpdatePacketImpl extends FS_ClientboundPlayerInfoUpdatePacket { + + public ClientboundPlayerInfoUpdatePacketImpl(EnumSet actions, List entries) { + super(actions, entries); + } + + @Override + public Object createPacket() { + EnumSet vanillaActions = EnumSet.noneOf(ClientboundPlayerInfoUpdatePacket.Action.class); + for (FS_ClientboundPlayerInfoUpdatePacket.Action action : actions) { + vanillaActions.add(ClientboundPlayerInfoUpdatePacket.Action.valueOf(action.name())); + } + + List entries = new ArrayList<>(); + for (Entry entry : this.entries) { + entries.add(new ClientboundPlayerInfoUpdatePacket.Entry( + entry.uuid(), + GameProfileImpl.asVanilla(entry.profile()), + entry.listed(), + entry.latency(), + GameType.byId(entry.gameMode().getId()), + PaperAdventure.asVanilla(entry.displayName()), + true, + -1, + null // TODO: Add ChatSession support + )); + } + + return new ClientboundPlayerInfoUpdatePacket(vanillaActions, entries); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundPlayerInfoUpdatePacket packet = (ClientboundPlayerInfoUpdatePacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImpl.java new file mode 100644 index 00000000..04f0ebec --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImpl.java @@ -0,0 +1,37 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundRemoveEntitiesPacket; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class ClientboundRemoveEntitiesPacketImpl extends FS_ClientboundRemoveEntitiesPacket { + + /** + * @param entityIds IDs of the entities to remove + */ + public ClientboundRemoveEntitiesPacketImpl(List entityIds) { + super(entityIds); + } + + @Override + public Object createPacket() { + int[] ids = new int[this.entityIds.size()]; + for (int i = 0; i < this.entityIds.size(); i++) { + ids[i] = this.entityIds.get(i); + } + + return new ClientboundRemoveEntitiesPacket(ids); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundRemoveEntitiesPacket packet = (ClientboundRemoveEntitiesPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImpl.java new file mode 100644 index 00000000..a37a77c0 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImpl.java @@ -0,0 +1,38 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundRotateHeadPacket; +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.server.level.ServerPlayer; + +public class ClientboundRotateHeadPacketImpl extends FS_ClientboundRotateHeadPacket { + + public ClientboundRotateHeadPacketImpl(int entityId, float headYaw) { + super(entityId, headYaw); + } + + @Override + public Object createPacket() { + ClientboundRotateHeadPacket packet = null; + + try { + packet = ReflectionUtils.createUnsafeInstance(ClientboundRotateHeadPacket.class); + ReflectionUtils.setFinalField(packet, "entityId", entityId); + ReflectionUtils.setFinalField(packet, "yHeadRot", AngelConverter.degreesToVanillaByte(headYaw)); + } catch (Exception e) { + e.printStackTrace(); + } + return packet; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundRotateHeadPacket packet = (ClientboundRotateHeadPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImpl.java new file mode 100644 index 00000000..98e346d5 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImpl.java @@ -0,0 +1,67 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.block.BlockState; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class ClientboundSetEntityDataPacketImpl extends FS_ClientboundSetEntityDataPacket { + + public ClientboundSetEntityDataPacketImpl(int entityId, List entityData) { + super(entityId, entityData); + } + + @Override + public Object createPacket() { + List> dataValues = new ArrayList<>(); + for (EntityData data : entityData) { + try { + Class entityClass = Class.forName(data.getAccessor().entityClassName()); + net.minecraft.network.syncher.EntityDataAccessor accessor = ReflectionUtils.getStaticField(entityClass, data.getAccessor().accessorFieldName()); + + Object vanillaValue = data.getValue(); + + if (data.getValue() == null) { + continue; + } + + if (data.getValue() instanceof Component c) { + vanillaValue = PaperAdventure.asVanilla(c); + } + + if (data.getValue() instanceof ItemStack i) { + vanillaValue = net.minecraft.world.item.ItemStack.fromBukkitCopy(i); + } + + if (data.getValue() instanceof BlockState b) { + vanillaValue = ((CraftBlockState) b).getHandle(); + } + + dataValues.add(SynchedEntityData.DataValue.create(accessor, vanillaValue)); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + return new ClientboundSetEntityDataPacket(entityId, dataValues); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetEntityDataPacket packet = (ClientboundSetEntityDataPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImpl.java new file mode 100644 index 00000000..21d6acfd --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImpl.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import com.mojang.datafixers.util.Pair; +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEquipmentPacket; +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ClientboundSetEquipmentPacketImpl extends FS_ClientboundSetEquipmentPacket { + + public ClientboundSetEquipmentPacketImpl(int entityId, Map equipment) { + super(entityId, equipment); + } + + @Override + public Object createPacket() { + List> slots = new ArrayList<>(); + + for (Map.Entry entry : equipment.entrySet()) { + EquipmentSlot equipmentSlot = net.minecraft.world.entity.EquipmentSlot.byName(entry.getKey().name().toLowerCase()); + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.asNMSCopy(entry.getValue()); + + slots.add(Pair.of(equipmentSlot, itemStack)); + } + + return new ClientboundSetEquipmentPacket(entityId, slots); + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetEquipmentPacket packet = (ClientboundSetEquipmentPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImpl.java new file mode 100644 index 00000000..152690fd --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImpl.java @@ -0,0 +1,45 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundSetPassengersPacket; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class ClientboundSetPassengersPacketImpl extends FS_ClientboundSetPassengersPacket { + + public ClientboundSetPassengersPacketImpl(int entityId, List passengers) { + super(entityId, passengers); + } + + + @Override + public Object createPacket() { + int[] passengers = new int[this.passengers.size()]; + for (int i = 0; i < this.passengers.size(); i++) { + passengers[i] = this.passengers.get(i); + } + + try { + ClientboundSetPassengersPacket packet = ReflectionUtils.createUnsafeInstance(ClientboundSetPassengersPacket.class); + ReflectionUtils.setFinalField(packet, "vehicle", entityId); + ReflectionUtils.setFinalField(packet, "passengers", passengers); + return packet; + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundSetPassengersPacket packet = (ClientboundSetPassengersPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImpl.java new file mode 100644 index 00000000..0c2853cc --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImpl.java @@ -0,0 +1,43 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundTeleportEntityPacket; +import de.oliver.fancysitula.versions.v1_21_4.utils.VanillaPlayerAdapter; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.PositionMoveRotation; +import net.minecraft.world.phys.Vec3; + +import java.util.Set; + +public class ClientboundTeleportEntityPacketImpl extends FS_ClientboundTeleportEntityPacket { + + public ClientboundTeleportEntityPacketImpl(int entityId, double x, double y, double z, float yaw, float pitch, boolean onGround) { + super(entityId, x, y, z, yaw, pitch, onGround); + } + + @Override + public Object createPacket() { + ClientboundTeleportEntityPacket packet = new ClientboundTeleportEntityPacket( + entityId, + new PositionMoveRotation( + new Vec3(x, y, z), + Vec3.ZERO, + yaw, + pitch + ), + Set.of(), + onGround + ); + + return packet; + } + + @Override + public void sendPacketTo(FS_RealPlayer player) { + ClientboundTeleportEntityPacket packet = (ClientboundTeleportEntityPacket) createPacket(); + + ServerPlayer vanillaPlayer = VanillaPlayerAdapter.asVanilla(player.getBukkitPlayer()); + vanillaPlayer.connection.send(packet); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/GameProfileImpl.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/GameProfileImpl.java new file mode 100644 index 00000000..fda827e6 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/GameProfileImpl.java @@ -0,0 +1,33 @@ +package de.oliver.fancysitula.versions.v1_21_4.utils; + +import com.mojang.authlib.GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameProfile; + +import java.util.Map; + +public class GameProfileImpl { + + public static GameProfile asVanilla(FS_GameProfile gameProfile) { + GameProfile gf = new GameProfile(gameProfile.getUUID(), gameProfile.getName()); + + for (Map.Entry entry : gameProfile.getProperties().entrySet()) { + FS_GameProfile.Property property = entry.getValue(); + + gf.getProperties().put(entry.getKey(), new com.mojang.authlib.properties.Property(property.name(), property.value(), property.signature())); + } + + return gf; + } + + public static FS_GameProfile fromVanilla(GameProfile gameProfile) { + FS_GameProfile fsGameProfile = new FS_GameProfile(gameProfile.getId(), gameProfile.getName()); + + for (Map.Entry entry : gameProfile.getProperties().entries()) { + com.mojang.authlib.properties.Property property = entry.getValue(); + + fsGameProfile.getProperties().put(entry.getKey(), new FS_GameProfile.Property(property.name(), property.value(), property.signature())); + } + + return fsGameProfile; + } +} diff --git a/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/VanillaPlayerAdapter.java b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/VanillaPlayerAdapter.java new file mode 100644 index 00000000..c8036600 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/main/java/de/oliver/fancysitula/versions/v1_21_4/utils/VanillaPlayerAdapter.java @@ -0,0 +1,12 @@ +package de.oliver.fancysitula.versions.v1_21_4.utils; + +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; + +public class VanillaPlayerAdapter { + + public static ServerPlayer asVanilla(Player p) { + return ((CraftPlayer) p).getHandle(); + } +} diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImplTest.java new file mode 100644 index 00000000..66b15bf5 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundAddEntityPacketImplTest.java @@ -0,0 +1,65 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundAddEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import org.bukkit.entity.EntityType; + +import java.util.UUID; + +class ClientboundAddEntityPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + int entityId = 10000; + UUID entityUUID = UUID.randomUUID(); + EntityType entityType = EntityType.PIG; + + double x = 5; + double y = 57; + double z = 203; + + float yaw = 142; + float pitch = 247; + float headYaw = 90; + + int velocityX = 0; + int velocityY = 0; + int velocityZ = 0; + + int data = 0; + + ClientboundAddEntityPacketImpl packet = new ClientboundAddEntityPacketImpl( + entityId, + entityUUID, + entityType, + x, + y, + z, + yaw, + pitch, + headYaw, + velocityX, + velocityY, + velocityZ, + data + ); + + ClientboundAddEntityPacket createdPacket = (ClientboundAddEntityPacket) packet.createPacket(); + + assert createdPacket.getId() == entityId; + assert createdPacket.getUUID().equals(entityUUID); + assert createdPacket.getType().getDescriptionId().equals(entityType.getKey().getKey()); + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getYRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getXRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + assert createdPacket.getXa() == velocityX; + assert createdPacket.getYa() == velocityY; + assert createdPacket.getZa() == velocityZ; + assert createdPacket.getData() == data; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImplTest.java new file mode 100644 index 00000000..b5af37c6 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoRemovePacketImplTest.java @@ -0,0 +1,23 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundPlayerInfoRemovePacketImpl; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoRemovePacketImplTest { + + @Test + void createPacket() { + List uuids = List.of(UUID.randomUUID(), UUID.randomUUID()); + + ClientboundPlayerInfoRemovePacketImpl packet = new ClientboundPlayerInfoRemovePacketImpl(uuids); + ClientboundPlayerInfoRemovePacket vanillaPacket = (ClientboundPlayerInfoRemovePacket) packet.createPacket(); + + for (UUID uuid : uuids) { + assert vanillaPacket.profileIds().contains(uuid); + } + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImplTest.java new file mode 100644 index 00000000..2bb28e13 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundPlayerInfoUpdatePacketImplTest.java @@ -0,0 +1,62 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundPlayerInfoUpdatePacketImpl; +import net.kyori.adventure.text.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +class ClientboundPlayerInfoUpdatePacketImplTest { + + @Test + void createPacket() { + // Setup packet + EnumSet actions = EnumSet.noneOf(FS_ClientboundPlayerInfoUpdatePacket.Action.class); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + actions.add(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + + FS_GameProfile gameProfile = new FS_GameProfile(UUID.randomUUID(), "Test name"); + boolean listed = true; + int latency = 42; + FS_GameType gameMode = FS_GameType.SURVIVAL; + Component displayName = Component.text("Test displayname"); + + List entries = new ArrayList<>(); + entries.add(new FS_ClientboundPlayerInfoUpdatePacket.Entry( + gameProfile.getUUID(), + gameProfile, + listed, + latency, + gameMode, + displayName + )); + + ClientboundPlayerInfoUpdatePacketImpl packet = new ClientboundPlayerInfoUpdatePacketImpl(actions, entries); + + ClientboundPlayerInfoUpdatePacket createdPacket = (ClientboundPlayerInfoUpdatePacket) packet.createPacket(); + + assert createdPacket.entries().size() == 1; + assert createdPacket.actions().size() == 3; + + // check entry + ClientboundPlayerInfoUpdatePacket.Entry entry = createdPacket.entries().getFirst(); + assert entry.profile().getId().equals(gameProfile.getUUID()); + assert entry.profile().getName().equals(gameProfile.getName()); + assert entry.listed() == listed; + assert entry.latency() == latency; + assert entry.gameMode().getId() == gameMode.getId(); + + // check actions + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + assert createdPacket.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImplTest.java new file mode 100644 index 00000000..6a32fc05 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRemoveEntitiesPacketImplTest.java @@ -0,0 +1,21 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundRemoveEntitiesPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ClientboundRemoveEntitiesPacketImplTest { + + @Test + void createPacket() { + List entityIds = List.of(95, 120, 154, 187); + + ClientboundRemoveEntitiesPacketImpl packet = new ClientboundRemoveEntitiesPacketImpl(entityIds); + ClientboundRemoveEntitiesPacket createdPacket = (ClientboundRemoveEntitiesPacket) packet.createPacket(); + + assert createdPacket.getEntityIds().size() == entityIds.size(); + assert createdPacket.getEntityIds().containsAll(entityIds); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImplTest.java new file mode 100644 index 00000000..8084414d --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundRotateHeadPacketImplTest.java @@ -0,0 +1,22 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.api.utils.reflections.ReflectionUtils; +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundRotateHeadPacketImpl; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import org.junit.jupiter.api.Test; + +class ClientboundRotateHeadPacketImplTest { + + @Test + void createPacket() throws Exception { + int entityId = 184; + float headYaw = 45; + + ClientboundRotateHeadPacketImpl packet = new ClientboundRotateHeadPacketImpl(entityId, (byte) headYaw); + ClientboundRotateHeadPacket createdPacket = (ClientboundRotateHeadPacket) packet.createPacket(); + + assert ReflectionUtils.getField(createdPacket, "entityId").equals(entityId); + assert createdPacket.getYHeadRot() == AngelConverter.degreesToVanillaByte(headYaw); + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImplTest.java new file mode 100644 index 00000000..9ada3736 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEntityDataPacketImplTest.java @@ -0,0 +1,29 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.packets.FS_ClientboundSetEntityDataPacket; +import de.oliver.fancysitula.api.utils.entityData.FS_TextDisplayData; +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundSetEntityDataPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; + +import java.util.List; + +class ClientboundSetEntityDataPacketImplTest { + + //TODO: Fix this test (using registry) +// @Test + void createPacket() { + int entityId = 712; + List entityData = List.of( + new FS_ClientboundSetEntityDataPacket.EntityData( + FS_TextDisplayData.TEXT, + "Hello, World!" + ) + ); + + ClientboundSetEntityDataPacketImpl packet = new ClientboundSetEntityDataPacketImpl(entityId, entityData); + ClientboundSetEntityDataPacket createdPacket = (ClientboundSetEntityDataPacket) packet.createPacket(); + + assert createdPacket.id() == entityId; + assert createdPacket.packedItems().size() == 1; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImplTest.java new file mode 100644 index 00000000..e14c3c3b --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetEquipmentPacketImplTest.java @@ -0,0 +1,29 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.utils.FS_EquipmentSlot; +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundSetEquipmentPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +class ClientboundSetEquipmentPacketImplTest { + + //TODO: Fix this test (registry problems) + // @Test + void createPacket() { + // Setup packet + Map equipment = Map.of( + FS_EquipmentSlot.MAINHAND, new ItemStack(Material.DIAMOND_SWORD), + FS_EquipmentSlot.OFFHAND, new ItemStack(Material.SHIELD), + FS_EquipmentSlot.HEAD, new ItemStack(Material.DIAMOND_HELMET) + ); + + ClientboundSetEquipmentPacketImpl packet = new ClientboundSetEquipmentPacketImpl(42, equipment); + ClientboundSetEquipmentPacket createdPacket = (ClientboundSetEquipmentPacket) packet.createPacket(); + + assert createdPacket.getEntity() == 42; + assert createdPacket.getSlots().size() == 3; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImplTest.java new file mode 100644 index 00000000..daca61a7 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundSetPassengersPacketImplTest.java @@ -0,0 +1,29 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundSetPassengersPacketImpl; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ClientboundSetPassengersPacketImplTest { + + @Test + void createPacket() { + // Setup packet + int vehicleID = 712; + List passengers = new ArrayList<>(); + passengers.add(571); + passengers.add(572); + + ClientboundSetPassengersPacketImpl packet = new ClientboundSetPassengersPacketImpl(vehicleID, passengers); + ClientboundSetPassengersPacket createdPacket = (ClientboundSetPassengersPacket) packet.createPacket(); + + // Check packet + assert createdPacket.getVehicle() == vehicleID; + assert createdPacket.getPassengers().length == 2; + assert createdPacket.getPassengers()[0] == 571; + assert createdPacket.getPassengers()[1] == 572; + } +} diff --git a/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImplTest.java b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImplTest.java new file mode 100644 index 00000000..b1827c61 --- /dev/null +++ b/libraries/packets/implementations/1_21_4/src/test/java/de/oliver/fancysitula/versions/v1_21_4/packets/ClientboundTeleportEntityPacketImplTest.java @@ -0,0 +1,32 @@ +package de.oliver.fancysitula.versions.v1_21_4.packets; + +import de.oliver.fancysitula.api.utils.AngelConverter; +import de.oliver.fancysitula.versions.v1_21_4.packets.ClientboundTeleportEntityPacketImpl; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import org.junit.jupiter.api.Test; + +class ClientboundTeleportEntityPacketImplTest { + + @Test + void createPacket() { + int entityId = 4313; + double x = 15.0; + double y = 57.0; + double z = -27.0; + float yaw = 90.0f; + float pitch = 45.0f; + boolean onGround = true; + + ClientboundTeleportEntityPacketImpl packet = new ClientboundTeleportEntityPacketImpl(entityId, x, y, z, yaw, pitch, onGround); + ClientboundTeleportEntityPacket createdPacket = (ClientboundTeleportEntityPacket) packet.createPacket(); + + assert createdPacket != null; + assert createdPacket.getId() == entityId; + assert createdPacket.getX() == x; + assert createdPacket.getY() == y; + assert createdPacket.getZ() == z; + assert createdPacket.getyRot() == AngelConverter.degreesToVanillaByte(yaw); + assert createdPacket.getxRot() == AngelConverter.degreesToVanillaByte(pitch); + assert createdPacket.isOnGround() == onGround; + } +} \ No newline at end of file diff --git a/libraries/packets/implementations/build.gradle.kts b/libraries/packets/implementations/build.gradle.kts new file mode 100644 index 00000000..9af39935 --- /dev/null +++ b/libraries/packets/implementations/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("java-library") +} \ No newline at end of file diff --git a/libraries/packets/settings.gradle.kts b/libraries/packets/settings.gradle.kts new file mode 100644 index 00000000..eb06bfd3 --- /dev/null +++ b/libraries/packets/settings.gradle.kts @@ -0,0 +1,12 @@ +rootProject.name = "FancySitula" + +include(":api") +include(":factories") +include(":implementations") +include(":implementations:1_20_6") +include(":implementations:1_21") +include(":implementations:1_21_1") +include(":implementations:1_21_3") +include(":implementations:1_21_4") + +include(":test_plugin") \ No newline at end of file diff --git a/libraries/packets/test_plugin/build.gradle.kts b/libraries/packets/test_plugin/build.gradle.kts new file mode 100644 index 00000000..00af0041 --- /dev/null +++ b/libraries/packets/test_plugin/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + id("java-library") + id("maven-publish") + + id("xyz.jpenilla.run-paper") + id("com.gradleup.shadow") + id("net.minecrell.plugin-yml.paper") +} + +runPaper.folia.registerTask() + +repositories { + mavenLocal() + mavenCentral() + maven(url = "https://repo.papermc.io/repository/maven-public/") + maven(url = "https://repo.fancyplugins.de/releases") +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + + implementation(project(":libraries:packets:api")) + implementation(project(":libraries:packets:factories")) + implementation(project(":libraries:packets:implementations:1_20_6")) + implementation(project(":libraries:packets:implementations:1_21_3")) + implementation("de.oliver.FancyAnalytics:logger:0.0.4") +} + +paper { + name = "FancySitulaTestPlugin" + main = "de.oliver.fancysitula.FancySitulaPlugin" + bootstrapper = "de.oliver.fancysitula.loaders.FancySitulaPluginBootstrapper" + loader = "de.oliver.fancysitula.loaders.FancySitulaPluginLoader" + foliaSupported = true + version = "1.0.0" + description = "Test plugin for FancySitula" + apiVersion = "1.19" +} + +tasks { + runServer { + minecraftVersion(findProperty("minecraftVersion").toString()) +// minecraftVersion("1.20.4") + } + + shadowJar { + archiveClassifier.set("") + } + + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release = 21 + } + + javadoc { + options.encoding = Charsets.UTF_8.name() + } + + processResources { + filteringCharset = Charsets.UTF_8.name() + } +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} \ No newline at end of file diff --git a/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/FancySitulaPlugin.java b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/FancySitulaPlugin.java new file mode 100644 index 00000000..413d61e8 --- /dev/null +++ b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/FancySitulaPlugin.java @@ -0,0 +1,16 @@ +package de.oliver.fancysitula; + +import de.oliver.fancyanalytics.logger.LogLevel; +import de.oliver.fancysitula.api.IFancySitula; +import de.oliver.fancysitula.commands.FancySitulaCMD; +import org.bukkit.plugin.java.JavaPlugin; + +public class FancySitulaPlugin extends JavaPlugin { + + @Override + public void onEnable() { + IFancySitula.LOGGER.setCurrentLevel(LogLevel.DEBUG); + + getServer().getCommandMap().register("fancysitula", new FancySitulaCMD()); + } +} diff --git a/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/commands/FancySitulaCMD.java b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/commands/FancySitulaCMD.java new file mode 100644 index 00000000..e3027524 --- /dev/null +++ b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/commands/FancySitulaCMD.java @@ -0,0 +1,118 @@ +package de.oliver.fancysitula.commands; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.packets.FS_Color; +import de.oliver.fancysitula.api.teams.FS_CollisionRule; +import de.oliver.fancysitula.api.teams.FS_NameTagVisibility; +import de.oliver.fancysitula.api.teams.FS_Team; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancysitula.factories.FancySitula; +import net.kyori.adventure.text.Component; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +public class FancySitulaCMD extends Command { + + public FancySitulaCMD() { + super("fancysitula"); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String s, @NotNull String[] args) { + if (!(sender instanceof Player p)) { + sender.sendMessage("You must be a player to execute this command!"); + return true; + } + + // Wrap the real player into an FS_Player instance + FS_RealPlayer fsPlayer = new FS_RealPlayer(p); + testTeam(p); + +// FS_TextDisplay fakeTextDisplay = new FS_TextDisplay(); +// fakeTextDisplay.setBillboardRenderConstraints((byte) 3); +// fakeTextDisplay.setScale(new Vector3f(5f)); +// fakeTextDisplay.setLocation(p.getLocation()); +// fakeTextDisplay.setText(Component.text("Hello, World!")); +// fakeTextDisplay.setBackground(0xFF00C8FF); +// FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, fakeTextDisplay); + +// FS_ItemDisplay fakeItemDisplay = new FS_ItemDisplay(); +// fakeItemDisplay.setBillboardRenderConstraints((byte) 3); +// fakeItemDisplay.setScale(new Vector3f(5f)); +// fakeItemDisplay.setLocation(p.getLocation()); +// fakeItemDisplay.setItem(p.getInventory().getItemInMainHand()); +// FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, fakeItemDisplay); + +// FS_BlockDisplay fakeBlockDisplay = new FS_BlockDisplay(); +// fakeBlockDisplay.setBillboardRenderConstraints((byte) 3); +// fakeBlockDisplay.setScale(new Vector3f(5f)); +// fakeBlockDisplay.setLocation(p.getLocation()); +// fakeBlockDisplay.setBlock(Material.DIAMOND_BLOCK.createBlockData().createBlockState()); +// FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, fakeBlockDisplay); + + return true; + } + + private void testTeam(Player to) { + FS_RealPlayer fsPlayer = new FS_RealPlayer(to); + + + FS_Team team = new FS_Team( + "myTeam", + Component.text("My Team"), + true, + true, + FS_NameTagVisibility.ALWAYS, + FS_CollisionRule.ALWAYS, + FS_Color.AQUA, + Component.text("Prefix"), + Component.text("Suffix"), + List.of("OliverHD", "OliverHD_2") + ); + + + FancySitula.TEAM_FACTORY.createTeamFor(fsPlayer, team); +// FancySitula.TEAM_FACTORY.updateTeamFor(fsPlayer, team); +// FancySitula.TEAM_FACTORY.addEntitiesToTeamFor(fsPlayer, team, List.of("OliverHD", "OliverHD_2")); + } + + private void fakeTablistEntries(Player to) { +// Wrap the real player into an FS_Player instance + FS_RealPlayer fsPlayer = new FS_RealPlayer(to); + + + UUID uuid1 = UUID.randomUUID(); + FS_ClientboundPlayerInfoUpdatePacket.Entry entry1 = new FS_ClientboundPlayerInfoUpdatePacket.Entry( + uuid1, + new FS_GameProfile(uuid1, ""), + true, + 69, + FS_GameType.SURVIVAL, + Component.text("player1") + ); + + UUID uuid2 = UUID.randomUUID(); + FS_ClientboundPlayerInfoUpdatePacket.Entry entry2 = new FS_ClientboundPlayerInfoUpdatePacket.Entry( + uuid2, + new FS_GameProfile(uuid2, ""), + true, + 69, + FS_GameType.SURVIVAL, + Component.text("player2") + ); + + FS_ClientboundPlayerInfoUpdatePacket playerInfoUpdatePacket = FancySitula.PACKET_FACTORY.createPlayerInfoUpdatePacket( + EnumSet.of(FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), + List.of(entry1, entry2) + ); + playerInfoUpdatePacket.send(fsPlayer); + } +} diff --git a/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginBootstrapper.java b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginBootstrapper.java new file mode 100644 index 00000000..4123aa79 --- /dev/null +++ b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginBootstrapper.java @@ -0,0 +1,19 @@ +package de.oliver.fancysitula.loaders; + +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +public class FancySitulaPluginBootstrapper implements PluginBootstrap { + @Override + public void bootstrap(@NotNull BootstrapContext bootstrapContext) { + + } + + @Override + public @NotNull JavaPlugin createPlugin(@NotNull PluginProviderContext context) { + return PluginBootstrap.super.createPlugin(context); + } +} diff --git a/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginLoader.java b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginLoader.java new file mode 100644 index 00000000..187b901d --- /dev/null +++ b/libraries/packets/test_plugin/src/main/java/de/oliver/fancysitula/loaders/FancySitulaPluginLoader.java @@ -0,0 +1,12 @@ +package de.oliver.fancysitula.loaders; + +import io.papermc.paper.plugin.loader.PluginClasspathBuilder; +import io.papermc.paper.plugin.loader.PluginLoader; +import org.jetbrains.annotations.NotNull; + +public class FancySitulaPluginLoader implements PluginLoader { + @Override + public void classloader(@NotNull PluginClasspathBuilder pluginClasspathBuilder) { + + } +} diff --git a/plugins/fancyholograms/api/build.gradle.kts b/plugins/fancyholograms/api/build.gradle.kts index cbfaba55..31c75233 100644 --- a/plugins/fancyholograms/api/build.gradle.kts +++ b/plugins/fancyholograms/api/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("java-library") id("maven-publish") - id("com.gradleup.shadow") version "8.3.6" + id("com.gradleup.shadow") } val minecraftVersion = "1.19.4" diff --git a/plugins/fancyholograms/build.gradle.kts b/plugins/fancyholograms/build.gradle.kts index 33ad3ecb..639194d1 100644 --- a/plugins/fancyholograms/build.gradle.kts +++ b/plugins/fancyholograms/build.gradle.kts @@ -7,11 +7,11 @@ plugins { id("java-library") id("maven-publish") - id("xyz.jpenilla.run-paper") version "2.3.1" - id("com.gradleup.shadow") version "8.3.6" - id("net.minecrell.plugin-yml.paper") version "0.6.0" - id("io.papermc.hangar-publish-plugin") version "0.1.2" - id("com.modrinth.minotaur") version "2.+" + id("xyz.jpenilla.run-paper") + id("com.gradleup.shadow") + id("net.minecrell.plugin-yml.paper") + id("io.papermc.hangar-publish-plugin") + id("com.modrinth.minotaur") } runPaper.folia.registerTask() @@ -63,7 +63,7 @@ dependencies { implementation(project(":plugins:fancyholograms::implementation_1_20_1", configuration = "reobf")) implementation(project(":plugins:fancyholograms::implementation_1_19_4", configuration = "reobf")) - implementation("de.oliver:FancyLib:35") + implementation(project(":libraries:common")) implementation("de.oliver:FancySitula:0.0.13") implementation("de.oliver.FancyAnalytics:api:0.1.6") implementation("de.oliver.FancyAnalytics:logger:0.0.6") diff --git a/plugins/fancyholograms/implementation_1_19_4/build.gradle.kts b/plugins/fancyholograms/implementation_1_19_4/build.gradle.kts index 3b8b11a4..53cbf599 100644 --- a/plugins/fancyholograms/implementation_1_19_4/build.gradle.kts +++ b/plugins/fancyholograms/implementation_1_19_4/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java-library") - id("io.papermc.paperweight.userdev") version "1.7.7" + id("io.papermc.paperweight.userdev") } diff --git a/plugins/fancyholograms/implementation_1_20_1/build.gradle.kts b/plugins/fancyholograms/implementation_1_20_1/build.gradle.kts index 305839c0..c92130a0 100644 --- a/plugins/fancyholograms/implementation_1_20_1/build.gradle.kts +++ b/plugins/fancyholograms/implementation_1_20_1/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java-library") - id("io.papermc.paperweight.userdev") version "1.7.7" + id("io.papermc.paperweight.userdev") } diff --git a/plugins/fancyholograms/implementation_1_20_2/build.gradle.kts b/plugins/fancyholograms/implementation_1_20_2/build.gradle.kts index 95da256b..4dba7b5d 100644 --- a/plugins/fancyholograms/implementation_1_20_2/build.gradle.kts +++ b/plugins/fancyholograms/implementation_1_20_2/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java-library") - id("io.papermc.paperweight.userdev") version "1.7.7" + id("io.papermc.paperweight.userdev") } diff --git a/plugins/fancyholograms/implementation_1_20_4/build.gradle.kts b/plugins/fancyholograms/implementation_1_20_4/build.gradle.kts index 13e190ce..8f96f62a 100644 --- a/plugins/fancyholograms/implementation_1_20_4/build.gradle.kts +++ b/plugins/fancyholograms/implementation_1_20_4/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java-library") - id("io.papermc.paperweight.userdev") version "1.7.7" + id("io.papermc.paperweight.userdev") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 71715b29..57a10994 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,16 @@ include(":plugins:fancyholograms:api") include(":plugins:fancyholograms:implementation_1_20_4") include(":plugins:fancyholograms:implementation_1_20_2") include(":plugins:fancyholograms:implementation_1_20_1") -include(":plugins:fancyholograms:implementation_1_19_4") \ No newline at end of file +include(":plugins:fancyholograms:implementation_1_19_4") + +include(":libraries:common") + +include("libraries:packets:api") +include("libraries:packets:factories") +include("libraries:packets:implementations") +include("libraries:packets:implementations:1_20_6") +include("libraries:packets:implementations:1_21") +include("libraries:packets:implementations:1_21_1") +include("libraries:packets:implementations:1_21_3") +include("libraries:packets:implementations:1_21_4") +include("libraries:packets:test_plugin") \ No newline at end of file