From e3422f22c6f66f326ca74065b8efed117a30cdce Mon Sep 17 00:00:00 2001 From: 3328429240 <3328429240@qq.com> Date: Thu, 10 Jul 2025 18:10:49 +0800 Subject: [PATCH] Initial commit --- .github/znpc.png | Bin 0 -> 55428 bytes .gitignore | 46 + LICENSE | 674 +++++++++++++++ README.md | 49 ++ api/build.gradle | 35 + .../java/lol/pyr/znpcsplus/api/NpcApi.java | 56 ++ .../lol/pyr/znpcsplus/api/NpcApiProvider.java | 50 ++ .../api/NpcPropertyRegistryProvider.java | 50 ++ .../znpcsplus/api/entity/EntityProperty.java | 25 + .../api/entity/EntityPropertyRegistry.java | 55 ++ .../znpcsplus/api/entity/PropertyHolder.java | 61 ++ .../znpcsplus/api/event/NpcDespawnEvent.java | 32 + .../znpcsplus/api/event/NpcInteractEvent.java | 45 + .../znpcsplus/api/event/NpcSpawnEvent.java | 32 + .../api/event/util/CancellableNpcEvent.java | 30 + .../znpcsplus/api/event/util/NpcEvent.java | 48 ++ .../pyr/znpcsplus/api/hologram/Hologram.java | 56 ++ .../api/interaction/ActionFactory.java | 10 + .../api/interaction/ActionRegistry.java | 7 + .../api/interaction/InteractionAction.java | 76 ++ .../interaction/InteractionActionType.java | 7 + .../api/interaction/InteractionType.java | 13 + .../java/lol/pyr/znpcsplus/api/npc/Npc.java | 212 +++++ .../lol/pyr/znpcsplus/api/npc/NpcEntry.java | 56 ++ .../pyr/znpcsplus/api/npc/NpcRegistry.java | 90 ++ .../lol/pyr/znpcsplus/api/npc/NpcType.java | 29 + .../znpcsplus/api/npc/NpcTypeRegistry.java | 21 + .../api/serialization/NpcSerializer.java | 20 + .../serialization/NpcSerializerRegistry.java | 19 + .../java/lol/pyr/znpcsplus/api/skin/Skin.java | 6 + .../znpcsplus/api/skin/SkinDescriptor.java | 11 + .../api/skin/SkinDescriptorFactory.java | 18 + .../pyr/znpcsplus/util/ArmadilloState.java | 8 + .../pyr/znpcsplus/util/AttachDirection.java | 10 + .../pyr/znpcsplus/util/AxolotlVariant.java | 9 + .../lol/pyr/znpcsplus/util/BlockState.java | 14 + .../lol/pyr/znpcsplus/util/CatVariant.java | 15 + .../lol/pyr/znpcsplus/util/CreeperState.java | 16 + .../lol/pyr/znpcsplus/util/FoxVariant.java | 6 + .../lol/pyr/znpcsplus/util/FrogVariant.java | 7 + .../lol/pyr/znpcsplus/util/HorseArmor.java | 8 + .../lol/pyr/znpcsplus/util/HorseColor.java | 11 + .../lol/pyr/znpcsplus/util/HorseStyle.java | 9 + .../lol/pyr/znpcsplus/util/HorseType.java | 9 + .../lol/pyr/znpcsplus/util/LlamaVariant.java | 8 + .../java/lol/pyr/znpcsplus/util/LookType.java | 7 + .../pyr/znpcsplus/util/MooshroomVariant.java | 10 + .../lol/pyr/znpcsplus/util/NamedColor.java | 20 + .../lol/pyr/znpcsplus/util/NpcLocation.java | 112 +++ .../java/lol/pyr/znpcsplus/util/NpcPose.java | 27 + .../lol/pyr/znpcsplus/util/OcelotType.java | 8 + .../lol/pyr/znpcsplus/util/PandaGene.java | 11 + .../lol/pyr/znpcsplus/util/ParrotVariant.java | 9 + .../lol/pyr/znpcsplus/util/PuffState.java | 7 + .../lol/pyr/znpcsplus/util/RabbitType.java | 22 + .../lol/pyr/znpcsplus/util/SkeletonType.java | 11 + .../lol/pyr/znpcsplus/util/SnifferState.java | 11 + .../lol/pyr/znpcsplus/util/SpellType.java | 10 + .../znpcsplus/util/TropicalFishVariant.java | 104 +++ .../java/lol/pyr/znpcsplus/util/Vector3f.java | 46 + .../java/lol/pyr/znpcsplus/util/Vector3i.java | 46 + .../lol/pyr/znpcsplus/util/VillagerLevel.java | 9 + .../znpcsplus/util/VillagerProfession.java | 40 + .../lol/pyr/znpcsplus/util/VillagerType.java | 20 + .../lol/pyr/znpcsplus/util/WoldVariant.java | 23 + .../lol/pyr/znpcsplus/util/ZombieType.java | 11 + build.gradle | 37 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 +++++ gradlew.bat | 89 ++ plugin/build.gradle | 71 ++ .../java/lol/pyr/znpcsplus/ZNpcsPlus.java | 361 ++++++++ .../java/lol/pyr/znpcsplus/ZNpcsPlusApi.java | 73 ++ .../lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java | 73 ++ .../pyr/znpcsplus/commands/CenterCommand.java | 35 + .../znpcsplus/commands/ChangeIdCommand.java | 36 + .../pyr/znpcsplus/commands/CloneCommand.java | 44 + .../pyr/znpcsplus/commands/CreateCommand.java | 54 ++ .../pyr/znpcsplus/commands/DeleteCommand.java | 37 + .../pyr/znpcsplus/commands/ListCommand.java | 38 + .../znpcsplus/commands/LookAtMeCommand.java | 37 + .../pyr/znpcsplus/commands/MoveCommand.java | 40 + .../pyr/znpcsplus/commands/NearCommand.java | 60 ++ .../commands/ReloadConfigCommand.java | 22 + .../commands/SetLocationCommand.java | 56 ++ .../commands/SetRotationCommand.java | 58 ++ .../pyr/znpcsplus/commands/SkinCommand.java | 143 ++++ .../znpcsplus/commands/TeleportCommand.java | 41 + .../pyr/znpcsplus/commands/ToggleCommand.java | 42 + .../pyr/znpcsplus/commands/TypeCommand.java | 37 + .../znpcsplus/commands/VersionCommand.java | 66 ++ .../commands/action/ActionAddCommand.java | 51 ++ .../commands/action/ActionClearCommand.java | 36 + .../commands/action/ActionDeleteCommand.java | 41 + .../commands/action/ActionEditCommand.java | 64 ++ .../commands/action/ActionListCommand.java | 39 + .../commands/hologram/HoloAddCommand.java | 43 + .../commands/hologram/HoloAddItemCommand.java | 39 + .../commands/hologram/HoloDeleteCommand.java | 41 + .../commands/hologram/HoloInfoCommand.java | 41 + .../commands/hologram/HoloInsertCommand.java | 49 ++ .../hologram/HoloInsertItemCommand.java | 45 + .../commands/hologram/HoloOffsetCommand.java | 38 + .../hologram/HoloRefreshDelayCommand.java | 38 + .../commands/hologram/HoloSetCommand.java | 46 + .../commands/hologram/HoloSetItemCommand.java | 48 ++ .../property/PropertyRemoveCommand.java | 43 + .../commands/property/PropertySetCommand.java | 216 +++++ .../commands/storage/ImportCommand.java | 57 ++ .../commands/storage/LoadAllCommand.java | 33 + .../commands/storage/MigrateCommand.java | 179 ++++ .../commands/storage/SaveAllCommand.java | 33 + .../znpcsplus/config/ComponentSerializer.java | 25 + .../pyr/znpcsplus/config/ConfigManager.java | 63 ++ .../pyr/znpcsplus/config/DatabaseConfig.java | 46 + .../lol/pyr/znpcsplus/config/MainConfig.java | 81 ++ .../pyr/znpcsplus/config/MessageConfig.java | 38 + .../znpcsplus/conversion/DataImporter.java | 10 + .../conversion/DataImporterRegistry.java | 54 ++ .../conversion/citizens/CitizensImporter.java | 118 +++ .../citizens/model/CitizensTrait.java | 19 + .../model/CitizensTraitsRegistry.java | 35 + .../citizens/model/SectionCitizensTrait.java | 19 + .../citizens/model/StringCitizensTrait.java | 18 + .../citizens/model/traits/CommandTrait.java | 69 ++ .../citizens/model/traits/EquipmentTrait.java | 115 +++ .../citizens/model/traits/HologramTrait.java | 36 + .../citizens/model/traits/LocationTrait.java | 25 + .../citizens/model/traits/LookTrait.java | 23 + .../citizens/model/traits/MirrorTrait.java | 27 + .../model/traits/ProfessionTrait.java | 28 + .../model/traits/SkinLayersTrait.java | 38 + .../citizens/model/traits/SkinTrait.java | 27 + .../citizens/model/traits/SpawnedTrait.java | 20 + .../citizens/model/traits/TypeTrait.java | 31 + .../citizens/model/traits/VillagerTrait.java | 39 + .../fancynpcs/FancyNpcsImporter.java | 183 ++++ .../conversion/znpcs/ZNpcImporter.java | 243 ++++++ .../conversion/znpcs/model/ZNpcsAction.java | 25 + .../znpcs/model/ZNpcsConversation.java | 16 + .../znpcs/model/ZNpcsConversationText.java | 19 + .../conversion/znpcs/model/ZNpcsLocation.java | 35 + .../conversion/znpcs/model/ZNpcsModel.java | 91 ++ .../znpcs/model/ZnpcsConversations.java | 23 + .../entity/ArmorStandVehicleProperties.java | 76 ++ .../znpcsplus/entity/EntityPropertyImpl.java | 62 ++ .../entity/EntityPropertyRegistryImpl.java | 800 ++++++++++++++++++ .../entity/EnumPropertySerializer.java | 29 + .../pyr/znpcsplus/entity/PacketEntity.java | 224 +++++ .../znpcsplus/entity/ParrotNBTCompound.java | 21 + .../entity/PrimitivePropertySerializer.java | 30 + .../znpcsplus/entity/PropertySerializer.java | 12 + .../entity/properties/BitsetProperty.java | 52 ++ .../entity/properties/BooleanProperty.java | 34 + .../properties/CamelSittingProperty.java | 33 + .../entity/properties/CustomTypeProperty.java | 32 + .../entity/properties/DinnerboneProperty.java | 35 + .../entity/properties/DummyProperty.java | 33 + .../properties/EncodedByteProperty.java | 48 ++ .../properties/EncodedIntegerProperty.java | 48 ++ .../properties/EncodedStringProperty.java | 48 ++ .../properties/EntitySittingProperty.java | 37 + .../entity/properties/EquipmentProperty.java | 28 + .../properties/ForceBodyRotationProperty.java | 25 + .../entity/properties/GlowProperty.java | 37 + .../entity/properties/HealthProperty.java | 27 + .../properties/HologramItemProperty.java | 24 + .../entity/properties/HorseColorProperty.java | 30 + .../entity/properties/HorseStyleProperty.java | 30 + .../entity/properties/IntegerProperty.java | 31 + .../entity/properties/LegacyBabyProperty.java | 29 + .../entity/properties/NBTProperty.java | 60 ++ .../entity/properties/NameProperty.java | 46 + .../properties/OptionalBlockPosProperty.java | 28 + .../entity/properties/RabbitTypeProperty.java | 53 ++ .../entity/properties/RotationProperty.java | 25 + .../entity/properties/TargetNpcProperty.java | 29 + .../TropicalFishVariantProperty.java | 48 ++ .../attributes/AttributeProperty.java | 63 ++ .../villager/VillagerDataProperty.java | 31 + .../villager/VillagerLevelProperty.java | 16 + .../villager/VillagerProfessionProperty.java | 17 + .../villager/VillagerTypeProperty.java | 17 + .../BlockStatePropertySerializer.java | 27 + .../BooleanPropertySerializer.java | 20 + .../serializers/ColorPropertySerializer.java | 21 + .../ComponentPropertySerializer.java | 25 + .../entity/serializers/GenericSerializer.java | 32 + .../ItemStackPropertySerializer.java | 23 + .../serializers/LookTypeSerializer.java | 26 + .../NamedColorPropertySerializer.java | 25 + .../serializers/SkinDescriptorSerializer.java | 29 + .../TargetNpcPropertySerializer.java | 21 + .../Vector3fPropertySerializer.java | 22 + .../pyr/znpcsplus/hologram/HologramImpl.java | 197 +++++ .../pyr/znpcsplus/hologram/HologramItem.java | 109 +++ .../pyr/znpcsplus/hologram/HologramLine.java | 91 ++ .../pyr/znpcsplus/hologram/HologramText.java | 42 + .../interaction/ActionFactoryImpl.java | 48 ++ .../interaction/ActionRegistryImpl.java | 65 ++ .../interaction/InteractionActionImpl.java | 14 + .../InteractionCommandHandler.java | 25 + .../InteractionPacketListener.java | 87 ++ .../consolecommand/ConsoleCommandAction.java | 55 ++ .../ConsoleCommandActionType.java | 66 ++ .../interaction/message/MessageAction.java | 58 ++ .../message/MessageActionType.java | 69 ++ .../playerchat/PlayerChatAction.java | 53 ++ .../playerchat/PlayerChatActionType.java | 65 ++ .../playercommand/PlayerCommandAction.java | 54 ++ .../PlayerCommandActionType.java | 66 ++ .../switchserver/SwitchServerAction.java | 52 ++ .../switchserver/SwitchServerActionType.java | 66 ++ .../lol/pyr/znpcsplus/npc/NpcEntryImpl.java | 63 ++ .../java/lol/pyr/znpcsplus/npc/NpcImpl.java | 296 +++++++ .../pyr/znpcsplus/npc/NpcRegistryImpl.java | 231 +++++ .../lol/pyr/znpcsplus/npc/NpcTypeImpl.java | 217 +++++ .../znpcsplus/npc/NpcTypeRegistryImpl.java | 423 +++++++++ .../pyr/znpcsplus/packets/PacketFactory.java | 31 + .../znpcsplus/packets/V1_17PacketFactory.java | 34 + .../packets/V1_19_3PacketFactory.java | 47 + .../packets/V1_20_2PacketFactory.java | 43 + .../packets/V1_21_3PacketFactory.java | 29 + .../packets/V1_21_7PacketFactory.java | 14 + .../znpcsplus/packets/V1_8PacketFactory.java | 208 +++++ .../pyr/znpcsplus/parsers/ColorParser.java | 28 + .../parsers/EntityPropertyParser.java | 27 + .../lol/pyr/znpcsplus/parsers/EnumParser.java | 27 + .../parsers/InteractionTypeParser.java | 24 + .../znpcsplus/parsers/NamedColorParser.java | 23 + .../pyr/znpcsplus/parsers/NpcEntryParser.java | 26 + .../pyr/znpcsplus/parsers/NpcTypeParser.java | 26 + .../pyr/znpcsplus/parsers/StringParser.java | 19 + .../pyr/znpcsplus/parsers/Vector3fParser.java | 27 + .../pyr/znpcsplus/parsers/Vector3iParser.java | 30 + .../reflection/ReflectionBuilder.java | 126 +++ .../reflection/ReflectionLazyLoader.java | 55 ++ .../reflection/ReflectionPackage.java | 40 + .../pyr/znpcsplus/reflection/Reflections.java | 253 ++++++ .../reflection/types/ClassReflection.java | 14 + .../reflection/types/FieldReflection.java | 123 +++ .../reflection/types/MethodReflection.java | 63 ++ .../znpcsplus/scheduling/FoliaScheduler.java | 92 ++ .../znpcsplus/scheduling/SpigotScheduler.java | 46 + .../znpcsplus/scheduling/TaskScheduler.java | 20 + .../NpcSerializerRegistryImpl.java | 33 + .../serialization/YamlSerializer.java | 154 ++++ .../znpcsplus/skin/BaseSkinDescriptor.java | 44 + .../skin/SkinDescriptorFactoryImpl.java | 67 ++ .../java/lol/pyr/znpcsplus/skin/SkinImpl.java | 91 ++ .../pyr/znpcsplus/skin/cache/CachedId.java | 18 + .../znpcsplus/skin/cache/MojangSkinCache.java | 277 ++++++ .../skin/cache/SkinCacheCleanTask.java | 16 + .../skin/descriptor/MirrorDescriptor.java | 37 + .../descriptor/NameFetchingDescriptor.java | 44 + .../skin/descriptor/PrefetchedDescriptor.java | 70 ++ .../descriptor/UUIDFetchingDescriptor.java | 42 + .../lol/pyr/znpcsplus/storage/NpcStorage.java | 14 + .../pyr/znpcsplus/storage/NpcStorageType.java | 48 ++ .../znpcsplus/storage/database/Database.java | 18 + .../pyr/znpcsplus/storage/mysql/MySQL.java | 131 +++ .../znpcsplus/storage/mysql/MySQLStorage.java | 322 +++++++ .../pyr/znpcsplus/storage/sqlite/SQLite.java | 112 +++ .../storage/sqlite/SQLiteStorage.java | 320 +++++++ .../znpcsplus/storage/yaml/YamlStorage.java | 84 ++ .../znpcsplus/tasks/HologramRefreshTask.java | 22 + .../pyr/znpcsplus/tasks/NpcProcessorTask.java | 150 ++++ .../tasks/ViewableHideOnLeaveListener.java | 15 + .../pyr/znpcsplus/updater/UpdateChecker.java | 116 +++ .../updater/UpdateNotificationListener.java | 37 + .../znpcsplus/user/ClientPacketListener.java | 30 + .../java/lol/pyr/znpcsplus/user/User.java | 53 ++ .../lol/pyr/znpcsplus/user/UserListener.java | 24 + .../lol/pyr/znpcsplus/user/UserManager.java | 36 + .../pyr/znpcsplus/util/BungeeConnector.java | 35 + .../java/lol/pyr/znpcsplus/util/FileUtil.java | 23 + .../lol/pyr/znpcsplus/util/FoliaUtil.java | 30 + .../lol/pyr/znpcsplus/util/FutureUtil.java | 34 + .../znpcsplus/util/ItemSerializationUtil.java | 47 + .../lol/pyr/znpcsplus/util/LazyLoader.java | 21 + .../java/lol/pyr/znpcsplus/util/PapiUtil.java | 27 + .../java/lol/pyr/znpcsplus/util/Viewable.java | 123 +++ .../resources/messages/action-hover/add.txt | 13 + .../resources/messages/action-hover/clear.txt | 3 + .../messages/action-hover/delete.txt | 3 + .../resources/messages/action-hover/edit.txt | 3 + .../resources/messages/action-hover/list.txt | 3 + plugin/src/main/resources/messages/action.txt | 10 + .../resources/messages/holo-hover/add.txt | 0 .../resources/messages/holo-hover/additem.txt | 0 .../resources/messages/holo-hover/delete.txt | 0 .../resources/messages/holo-hover/info.txt | 0 .../resources/messages/holo-hover/insert.txt | 0 .../messages/holo-hover/insertitem.txt | 0 .../resources/messages/holo-hover/offset.txt | 0 .../messages/holo-hover/refreshdelay.txt | 0 .../resources/messages/holo-hover/set.txt | 0 .../resources/messages/holo-hover/setitem.txt | 0 plugin/src/main/resources/messages/holo.txt | 18 + .../messages/property-hover/remove.txt | 3 + .../resources/messages/property-hover/set.txt | 3 + .../src/main/resources/messages/property.txt | 7 + .../resources/messages/root-hover/center.txt | 3 + .../messages/root-hover/changeid.txt | 3 + .../resources/messages/root-hover/create.txt | 3 + .../resources/messages/root-hover/delete.txt | 3 + .../resources/messages/root-hover/list.txt | 3 + .../messages/root-hover/lookatme.txt | 3 + .../resources/messages/root-hover/move.txt | 3 + .../resources/messages/root-hover/near.txt | 3 + .../messages/root-hover/setlocation.txt | 3 + .../messages/root-hover/setrotation.txt | 3 + .../resources/messages/root-hover/skin.txt | 13 + .../messages/root-hover/teleport.txt | 3 + .../resources/messages/root-hover/toggle.txt | 3 + .../resources/messages/root-hover/type.txt | 3 + plugin/src/main/resources/messages/root.txt | 26 + .../messages/storage-hover/import.txt | 8 + .../messages/storage-hover/migrate.txt | 16 + .../messages/storage-hover/reload.txt | 4 + .../resources/messages/storage-hover/save.txt | 3 + .../src/main/resources/messages/storage.txt | 9 + plugin/src/main/resources/plugin.yml | 33 + settings.gradle | 3 + 325 files changed, 16892 insertions(+) create mode 100644 .github/znpc.png create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 api/build.gradle create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/LookType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java create mode 100644 api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 plugin/build.gradle create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/user/User.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java create mode 100644 plugin/src/main/resources/messages/action-hover/add.txt create mode 100644 plugin/src/main/resources/messages/action-hover/clear.txt create mode 100644 plugin/src/main/resources/messages/action-hover/delete.txt create mode 100644 plugin/src/main/resources/messages/action-hover/edit.txt create mode 100644 plugin/src/main/resources/messages/action-hover/list.txt create mode 100644 plugin/src/main/resources/messages/action.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/add.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/additem.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/delete.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/info.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/insert.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/insertitem.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/offset.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/refreshdelay.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/set.txt create mode 100644 plugin/src/main/resources/messages/holo-hover/setitem.txt create mode 100644 plugin/src/main/resources/messages/holo.txt create mode 100644 plugin/src/main/resources/messages/property-hover/remove.txt create mode 100644 plugin/src/main/resources/messages/property-hover/set.txt create mode 100644 plugin/src/main/resources/messages/property.txt create mode 100644 plugin/src/main/resources/messages/root-hover/center.txt create mode 100644 plugin/src/main/resources/messages/root-hover/changeid.txt create mode 100644 plugin/src/main/resources/messages/root-hover/create.txt create mode 100644 plugin/src/main/resources/messages/root-hover/delete.txt create mode 100644 plugin/src/main/resources/messages/root-hover/list.txt create mode 100644 plugin/src/main/resources/messages/root-hover/lookatme.txt create mode 100644 plugin/src/main/resources/messages/root-hover/move.txt create mode 100644 plugin/src/main/resources/messages/root-hover/near.txt create mode 100644 plugin/src/main/resources/messages/root-hover/setlocation.txt create mode 100644 plugin/src/main/resources/messages/root-hover/setrotation.txt create mode 100644 plugin/src/main/resources/messages/root-hover/skin.txt create mode 100644 plugin/src/main/resources/messages/root-hover/teleport.txt create mode 100644 plugin/src/main/resources/messages/root-hover/toggle.txt create mode 100644 plugin/src/main/resources/messages/root-hover/type.txt create mode 100644 plugin/src/main/resources/messages/root.txt create mode 100644 plugin/src/main/resources/messages/storage-hover/import.txt create mode 100644 plugin/src/main/resources/messages/storage-hover/migrate.txt create mode 100644 plugin/src/main/resources/messages/storage-hover/reload.txt create mode 100644 plugin/src/main/resources/messages/storage-hover/save.txt create mode 100644 plugin/src/main/resources/messages/storage.txt create mode 100644 plugin/src/main/resources/plugin.yml create mode 100644 settings.gradle diff --git a/.github/znpc.png b/.github/znpc.png new file mode 100644 index 0000000000000000000000000000000000000000..06a86864a5337673ba652be941369f1bdeff780b GIT binary patch literal 55428 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 b_Vz5t_ifIfgxQeG%|9($O9u<0nP2iB)K8m4 literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb5a603 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### 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 + +/plugin/run/ +/.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49b1a42 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# ZNPCsPlus [![](https://img.shields.io/discord/1099449144948555957?label=Discord&logo=Discord&style=plastic)](https://discord.gg/MAZz6XpPcg) [![](https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci.pyr.lol%2Fjob%2FZNPCsPlus%2F&style=plastic&logo=jenkins)](https://ci.pyr.lol/job/ZNPCsPlus/) +[![](https://img.shields.io/bstats/players/18244?style=plastic&label=bStats%20Players)]((https://bstats.org/plugin/bukkit/ZNPCsPlus/18244/)) [![](https://img.shields.io/bstats/servers/18244?style=plastic&label=bStats%20Servers)]((https://bstats.org/plugin/bukkit/ZNPCsPlus/18244/)) [![](https://img.shields.io/spiget/downloads/109380?style=plastic&label=Spigot%20Downloads)]((https://www.spigotmc.org/resources/znpcsplus.109380/)) + +[ZNPCsPlus](https://www.spigotmc.org/resources/znpcsplus.109380/) is a Spigot plugin that is used to create fake entities +that players can interact with to perform actions like switching servers on a network or executing commands. + +This plugin is a remake of a plugin called ZNPCs, we originally started because the maintainer of ZNPCs decided to announce that he was +[dropping support for the plugin](https://github.com/Pyrbu/ZNPCsPlus/blob/2.X/.github/znpc.png?raw=true). + +Looking for up-to-date builds of the plugin? Check out our [Jenkins](https://ci.pyr.lol/job/ZNPCsPlus/) + +## Why is it so good? +- 100% Packet Based - Nothing is ran on the main thread +- Performance & stability oriented code +- Support for all versions from 1.8 to 1.20.4 +- Support for multiple different storage options +- Intuitive command system + +### Requirements, Extensions & Supported Software +Requirements: +- Java 8+ +- Minecraft 1.8 - 1.21 + +Supported Softwares: +- Spigot ([Website](https://www.spigotmc.org/)) +- Paper ([Github](https://github.com/PaperMC/Paper)) ([Website](https://papermc.io/software/paper)) +- Folia ([Github](https://github.com/PaperMC/Folia)) ([Website](https://papermc.io/software/folia)) +- ArcLight ([Github](https://github.com/IzzelAliz/Arclight)) + +Optional Dependencies/Extensions: +- PlaceholderAPI + +## Found a bug? +Open an issue in the GitHub [issue tracker](https://github.com/Pyrbu/ZNPCsPlus/issues) or join our [support discord](https://discord.gg/MAZz6XpPcg) + +## BStats +[![](https://bstats.org/signatures/bukkit/znpcsplus.svg)](https://bstats.org/plugin/bukkit/ZNPCsPlus/18244/) + +#### Like what you see? Want the project to continue improving? Consider starring the repository & leaving a positive review on [Spigot](https://www.spigotmc.org/resources/znpcsplus.109380/)! + +## Credits +- [PacketEvents 2.0](https://github.com/retrooper/packetevents) - Packet library +- [Minecraft Wiki Protocol (formally wiki.vg)](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Main_Page) - Minecraft protocol documentation +- [gson](https://github.com/google/gson) - JSON parsing library made by Google +- [Mineskin.org](https://mineskin.org/) - Website for raw skin file uploads +- [adventure](https://docs.advntr.dev/) - Minecraft text api +- [DazzleConf](https://github.com/A248/DazzleConf) - Configuration library +- [Director](https://github.com/Pyrbu/Director) - Command library +- [PlaceholderAPI](https://github.com/PlaceholderAPI/PlaceholderAPI) - Universal string placeholder library diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..349cf17 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,35 @@ +plugins { + id "java" + id "maven-publish" +} + +java { + withSourcesJar() + withJavadocJar() +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = "znpcsplus-api" + + pom { + name.set("znpcsplus-api") + description.set("The API for the ZNPCsPlus plugin") + url.set("https://github.com/Pyrbu/ZNPCsPlus") + } + } + } + repositories { + maven { + Map systemProperties = System.getenv() + credentials { + if (systemProperties.containsKey("DIST_USERNAME")) username systemProperties.get("DIST_USERNAME") + if (systemProperties.containsKey("DIST_PASSWORD")) password systemProperties.get("DIST_PASSWORD") + } + // If the BUILD_ID enviroment variable is present that means its a Jenkins build & that it should go into the snapshots repo + url = systemProperties.containsKey("BUILD_ID") ? uri("https://repo.pyr.lol/snapshots/") : uri("https://repo.pyr.lol/releases/") + } + } +} \ No newline at end of file diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java new file mode 100644 index 0000000..2faf2c8 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApi.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.api; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.interaction.ActionFactory; +import lol.pyr.znpcsplus.api.interaction.ActionRegistry; +import lol.pyr.znpcsplus.api.npc.NpcRegistry; +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.api.serialization.NpcSerializerRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptorFactory; + +/** + * Main API class for ZNPCsPlus. + */ +public interface NpcApi { + /** + * Gets the NPC registry. + * @return the NPC registry + */ + NpcRegistry getNpcRegistry(); + + /** + * Gets the NPC type registry. + * @return the NPC type registry + */ + NpcTypeRegistry getNpcTypeRegistry(); + + /** + * Gets the entity property registry. + * @return the entity property registry + */ + EntityPropertyRegistry getPropertyRegistry(); + + /** + * Gets the action registry. + * @return the action registry + */ + ActionRegistry getActionRegistry(); + + /** + * Gets the action factory. + * @return the action factory + */ + ActionFactory getActionFactory(); + + /** + * Gets the skin descriptor factory. + * @return the skin descriptor factory + */ + SkinDescriptorFactory getSkinDescriptorFactory(); + + /** + * Gets the npc serializer registry. + * @return the npc serializer registry + */ + NpcSerializerRegistry getNpcSerializerRegistry(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java new file mode 100644 index 0000000..b35235c --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/NpcApiProvider.java @@ -0,0 +1,50 @@ +package lol.pyr.znpcsplus.api; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.ServicePriority; + +/** + * Provider for the registered api instance + */ +public class NpcApiProvider { + private static NpcApi api = null; + + private NpcApiProvider() { + throw new UnsupportedOperationException(); + } + + /** + * Static method that returns the api instance of the plugin + * + * @return The instance of the api for the ZNPCsPlus plugin + */ + public static NpcApi get() { + if (api == null) throw new IllegalStateException( + "ZNPCsPlus plugin isn't enabled yet!\n" + + "Please add it to your plugin.yml as a depend or softdepend." + ); + return api; + } + + /** + * Internal method used to register the main instance of the plugin as the api provider + * You probably shouldn't call this method under any circumstances + * + * @param plugin Instance of the ZNPCsPlus plugin + * @param api Instance of the ZNPCsPlus api + */ + public static void register(Plugin plugin, NpcApi api) { + NpcApiProvider.api = api; + Bukkit.getServicesManager().register(NpcApi.class, api, plugin, ServicePriority.Normal); + } + + /** + * Internal method used to unregister the plugin from the provider when the plugin shuts down + * You probably shouldn't call this method under any circumstances + */ + public static void unregister() { + Bukkit.getServicesManager().unregister(api); + NpcApiProvider.api = null; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java b/api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java new file mode 100644 index 0000000..bcfd0b5 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/NpcPropertyRegistryProvider.java @@ -0,0 +1,50 @@ +package lol.pyr.znpcsplus.api; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.ServicePriority; + +/** + * Provider for the registered entity property registry instance + */ +public class NpcPropertyRegistryProvider { + private static EntityPropertyRegistry registry = null; + + private NpcPropertyRegistryProvider() { + throw new UnsupportedOperationException(); + } + + /** + * Static method that returns the entity property registry instance of the plugin + * + * @return The instance of the entity property registry for the ZNPCsPlus plugin + */ + public static EntityPropertyRegistry get() { + if (registry == null) throw new IllegalStateException( + "ZNPCsPlus plugin isn't loaded yet!\n" + + "Please add it to your plugin.yml as a depend or softdepend." + ); + return registry; + } + + /** + * Internal method used to register the main instance of the plugin as the entity property registry provider + * You probably shouldn't call this method under any circumstances + * + * @param plugin Instance of the ZNPCsPlus plugin + * @param api Instance of the ZNPCsPlus entity property registry + */ + public static void register(Plugin plugin, EntityPropertyRegistry api) { + NpcPropertyRegistryProvider.registry = api; + Bukkit.getServicesManager().register(EntityPropertyRegistry.class, registry, plugin, ServicePriority.Normal); + } + + /** + * Internal method used to unregister the plugin from the provider when the plugin shuts down + * You probably shouldn't call this method under any circumstances + */ + public static void unregister() { + Bukkit.getServicesManager().unregister(registry); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java new file mode 100644 index 0000000..6ad3f89 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityProperty.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.api.entity; + +/** + * Class that represents a unique property + * @param The type of the value of this property + */ +public interface EntityProperty { + /** + * The default value of this property, if this is provided in {@link PropertyHolder#setProperty(EntityProperty, Object)} + * as the value the property will be removed from the holder + * + * @return The default value of this property + */ + T getDefaultValue(); + + /** + * @return The name of this property + */ + String getName(); + + /** + * @return Whether this property can be modified by players using commands + */ + boolean isPlayerModifiable(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java new file mode 100644 index 0000000..13c094f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/entity/EntityPropertyRegistry.java @@ -0,0 +1,55 @@ +package lol.pyr.znpcsplus.api.entity; + +import java.util.Collection; + +/** + * Class responsible for providing entity property keys + * Some property keys are only registered in certain situations for example different minecraft versions + */ +public interface EntityPropertyRegistry { + /** + * @return All of the possible property keys + */ + Collection> getAll(); + + /** + * Get a property key by it's name + * + * @param name The name of a property key + * @return The property key corresponding to the name or null if there is none + */ + EntityProperty getByName(String name); + + /** + * Get a property key by it's name and automatically cast the property to the proper type + * If you don't know the type of the property you are requesting use {@link EntityPropertyRegistry#getByName(String)} instead + * + * @param name The name of a property key + * @param type The class of the expected type of the returned property key + * @return The property key corresponding to the name + * @param The expected type of the returned property key + */ + EntityProperty getByName(String name, Class type); + + /** + * Register a dummy property that can be used to store unique information per npc
+ * Note: Properties registered this way will be player-modifiable by default + * + * @param name The name of the new property + * @param type The type of the new property + * @deprecated Use {@link #registerDummy(String, Class, boolean)} instead + */ + @Deprecated + default void registerDummy(String name, Class type) { + registerDummy(name, type, true); + } + + /** + * Register a dummy property that can be used to store unique information per npc + * + * @param name The name of the new property + * @param type The type of the new property + * @param playerModifiable Whether this property can be modified by players using commands + */ + void registerDummy(String name, Class type, boolean playerModifiable); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java b/api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java new file mode 100644 index 0000000..5de5ea9 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/entity/PropertyHolder.java @@ -0,0 +1,61 @@ +package lol.pyr.znpcsplus.api.entity; + +import org.bukkit.inventory.ItemStack; + +import java.util.Set; + +/** + * Represents classes that have property values attatched to them + */ +public interface PropertyHolder { + /** + * Method used to get the value of a property from a property holder + * + * @param key Unique key representing a property + * @return The value associated with the provided property key and this holder + * @param The type of the property value + */ + T getProperty(EntityProperty key); + + /** + * Method used to check if a property holder has a value set for a specific property key + * + * @param key Unique key representing a property + * @return Whether this holder has a value set for the provided key + */ + boolean hasProperty(EntityProperty key); + + /** + * Method used to set a value for the provided key on this property holder + * + * @param key Unique key representing a property + * @param value The value to assign to the property key on this holder + * @param The type of the property value + */ + void setProperty(EntityProperty key, T value); + + /** + * Weird fix which is sadly required in order to not decrease performance + * when using item properties, read https://github.com/Pyrbu/ZNPCsPlus/pull/129#issuecomment-1948777764 + * + * @param key Unique key representing a property + * @param value The value to assign to the property key on this holder + */ + void setItemProperty(EntityProperty key, ItemStack value); + + /** + * Weird fix which is sadly required in order to not decrease performance + * when using item properties, read https://github.com/Pyrbu/ZNPCsPlus/pull/129#issuecomment-1948777764 + * + * @param key Unique key representing a property + * @return the {@link ItemStack} associated with the provided property key and this holder + */ + ItemStack getItemProperty(EntityProperty key); + + /** + * Method used to get a set of all of the property keys that this holder has a value for + * + * @return Set of property keys + */ + Set> getAppliedProperties(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java new file mode 100644 index 0000000..f9fcc01 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcDespawnEvent.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.api.event; + +import lol.pyr.znpcsplus.api.event.util.CancellableNpcEvent; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event called when an NPC is despawned for a player + * Note: This event is async + */ +public class NpcDespawnEvent extends CancellableNpcEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public NpcDespawnEvent(Player player, NpcEntry entry) { + super(player, entry); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java new file mode 100644 index 0000000..84f63b8 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcInteractEvent.java @@ -0,0 +1,45 @@ +package lol.pyr.znpcsplus.api.event; + +import lol.pyr.znpcsplus.api.event.util.CancellableNpcEvent; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event called when an NPC is interacted with by a player + * Note: This event is async + */ +public class NpcInteractEvent extends CancellableNpcEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + private final InteractionType clickType; + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + * @param clickType The type of interaction. See {@link InteractionType} + */ + public NpcInteractEvent(Player player, NpcEntry entry, InteractionType clickType) { + super(player, entry); + this.clickType = clickType; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + /** + * Returns the type of interaction. See {@link InteractionType} + * @return The type of interaction + */ + public InteractionType getClickType() { + return clickType; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java new file mode 100644 index 0000000..1edeec6 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/NpcSpawnEvent.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.api.event; + +import lol.pyr.znpcsplus.api.event.util.CancellableNpcEvent; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event called when an NPC is spawned for a player + * Note: This event is async + */ +public class NpcSpawnEvent extends CancellableNpcEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public NpcSpawnEvent(Player player, NpcEntry entry) { + super(player, entry); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java new file mode 100644 index 0000000..558a55e --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/CancellableNpcEvent.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.api.event.util; + +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; + +/** + * Base class for all NPC events that can be cancelled + */ +public abstract class CancellableNpcEvent extends NpcEvent implements Cancellable { + private boolean cancelled = false; + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public CancellableNpcEvent(Player player, NpcEntry entry) { + super(player, entry); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + cancelled = cancel; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java new file mode 100644 index 0000000..cc5483f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/event/util/NpcEvent.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.api.event.util; + +import lol.pyr.znpcsplus.api.npc.Npc; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; + +/** + * Base class for all NPC events + */ +public abstract class NpcEvent extends Event { + private final NpcEntry entry; + private final Player player; + + /** + * @param player The player involved in the event + * @param entry The NPC entry involved in the event + */ + public NpcEvent(Player player, NpcEntry entry) { + super(true); // All events are async since 99% of the plugin is async + this.entry = entry; + this.player = player; + } + + /** + * Returns the player involved in the event + * @return The player involved in the event + */ + public Player getPlayer() { + return player; + } + + /** + * Returns the NPC entry involved in the event + * @return The NPC entry involved in the event + */ + public NpcEntry getEntry() { + return entry; + } + + /** + * Returns the NPC involved in the event + * @return The NPC involved in the event + */ + public Npc getNpc() { + return entry.getNpc(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java b/api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java new file mode 100644 index 0000000..b19e905 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/hologram/Hologram.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.api.hologram; + +/** + * Represents a hologram + */ +public interface Hologram { + /** + * Adds a line to the hologram + * Note: to add an item line, pass "item:<item>" as the line + * @param line The line to add + */ + void addLine(String line); + + /** + * Gets a line from the hologram + * @param index The index of the line to get + * @return The line at the index + */ + String getLine(int index); + + /** + * Removes a line from the hologram + * @param index The index of the line to remove + */ + void removeLine(int index); + + /** + * Clears all lines from the hologram + */ + void clearLines(); + + /** + * Inserts a line into the hologram + * @param index The index to insert the line at + * @param line The line to insert + */ + void insertLine(int index, String line); + + /** + * Gets the number of lines in the hologram + * @return The number of lines in the hologram + */ + int lineCount(); + + /** + * Gets the refresh delay of the hologram + * @return The refresh delay of the hologram + */ + long getRefreshDelay(); + + /** + * Sets the refresh delay of the hologram + * @param delay The delay to set + */ + void setRefreshDelay(long delay); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java new file mode 100644 index 0000000..b896c5f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionFactory.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.api.interaction; + +@SuppressWarnings("unused") +public interface ActionFactory { + InteractionAction createConsoleCommandAction(String command, InteractionType interactionType, long cooldown, long delay); + InteractionAction createMessageAction(String message, InteractionType interactionType, long cooldown, long delay); + InteractionAction createPlayerChatAction(String message, InteractionType interactionType, long cooldown, long delay); + InteractionAction createPlayerCommandAction(String command, InteractionType interactionType, long cooldown, long delay); + InteractionAction createSwitchServerAction(String server, InteractionType interactionType, long cooldown, long delay); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java new file mode 100644 index 0000000..6f5cedb --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/ActionRegistry.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.api.interaction; + +public interface ActionRegistry { + void register(InteractionActionType type); + + void unregister(Class clazz); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java new file mode 100644 index 0000000..401784a --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionAction.java @@ -0,0 +1,76 @@ +package lol.pyr.znpcsplus.api.interaction; + +import org.bukkit.entity.Player; + +import java.util.UUID; + +/** + * Base class for all NPC interactions + */ +public abstract class InteractionAction { + /** + * The unique ID of this interaction + */ + private final UUID id; + + /** + * The cooldown of this interaction in seconds + */ + private final long cooldown; + + /** + * The delay of this interaction in ticks + */ + private final long delay; + + /** + * The type of this interaction + */ + private final InteractionType interactionType; + + /** + * @param cooldown The cooldown of this interaction in seconds + * @param delay The delay of this interaction in ticks + * @param interactionType The type of this interaction + */ + protected InteractionAction(long cooldown, long delay, InteractionType interactionType) { + this.interactionType = interactionType; + this.id = UUID.randomUUID(); + this.cooldown = cooldown; + this.delay = delay; + } + + /** + * @return The unique ID of this interaction + */ + public UUID getUuid() { + return id; + } + + /** + * @return The cooldown of this interaction in seconds + */ + public long getCooldown() { + return cooldown; + } + + /** + * @return The delay of this interaction in ticks + */ + public long getDelay() { + return delay; + } + + /** + * @return The type of this interaction + */ + public InteractionType getInteractionType() { + return interactionType; + } + + /** + * Runs this interaction + * @param player The player that triggered this interaction + */ + public abstract void run(Player player); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java new file mode 100644 index 0000000..d8289fa --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionActionType.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.api.interaction; + +public interface InteractionActionType { + String serialize(T obj); + T deserialize(String str); + Class getActionClass(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java new file mode 100644 index 0000000..27121ef --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/interaction/InteractionType.java @@ -0,0 +1,13 @@ +package lol.pyr.znpcsplus.api.interaction; + +/** + * The type of interaction + * ANY_CLICK: Any click type + * LEFT_CLICK: Only left clicks + * RIGHT_CLICK: Only right clicks + */ +public enum InteractionType { + ANY_CLICK, + LEFT_CLICK, + RIGHT_CLICK +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java new file mode 100644 index 0000000..79872ba --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/Npc.java @@ -0,0 +1,212 @@ +package lol.pyr.znpcsplus.api.npc; + +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.api.hologram.Hologram; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Base class for all NPCs + */ +public interface Npc extends PropertyHolder { + /** + * Sets the npc type of this NPC + * @param type The {@link NpcType} to set + */ + void setType(NpcType type); + + /** + * @return The {@link NpcType} of this NPC + */ + NpcType getType(); + + /** + * Gets the location of this NPC + * @return The {@link NpcLocation} of this NPC + */ + NpcLocation getLocation(); + + /** + * Sets the location of this NPC + * @param location The {@link NpcLocation} to set + */ + void setLocation(NpcLocation location); + + /** + * Sets the world of this NPC + * @param world The bukkit world to set + */ + void setWorld(World world); + + /** + * Sets the world of this NPC + * @param name The name world to set + */ + void setWorld(String name); + + /** + * Gets the hologram of this NPC + * @return The {@link Hologram} of this NPC + */ + Hologram getHologram(); + + /** + * Sets if the npc is enabled or not, i.e. if it should be visible to players + * @param enabled If the npc should be enabled + */ + void setEnabled(boolean enabled); + + /** + * Gets if the npc is enabled or not, i.e. if it should be visible to players + * @return If the npc is enabled or not + */ + boolean isEnabled(); + + /** + * Gets the unique ID of this NPC + * @return The unique ID of this NPC + */ + UUID getUuid(); + + /** + * Gets the {@link World} this NPC is in + * Note: can be null if the world is unloaded or does not exist + * @return The {@link World} this NPC is in + */ + World getWorld(); + + /** + * Gets the name of the world this NPC is in + * Unlike {@link Npc#getWorld()} this will never be null + * @return The name of the world this NPC is in + */ + String getWorldName(); + + /** + * Gets the list of actions for this NPC + * @return The {@link List} of {@link InteractionAction}s for this NPC + */ + List getActions(); + + /** + * Removes an action from this NPC + * @param index The index of the action to remove + */ + void removeAction(int index); + + /** + * Adds an action to this NPC + * @param action The {@link InteractionAction} to add + */ + void addAction(InteractionAction action); + + /** + * Edits an action for this NPC + * @param index The index of the action to edit + * @param action The {@link InteractionAction} to set + */ + void editAction(int index, InteractionAction action); + + + /** + * Clears all actions from this NPC + */ + void clearActions(); + + /** + * Gets if this NPC is visible to a player + * @param player The {@link Player} to check + * @return If this NPC is visible to the player + */ + boolean isVisibleTo(Player player); + + /** + * Hides this NPC from a player + * @param player The {@link Player} to hide from + */ + void hide(Player player); + + /** + * Shows this NPC to a player + * @param player The {@link Player} to show to + * @return A future that completes when the npc is fully shown to the player + */ + CompletableFuture show(Player player); + + /** + * Respawns this NPC for a player + * @param player The {@link Player} to respawn for + * @return A future that completes when the npc is fully respawned + */ + CompletableFuture respawn(Player player); + + /** + * Sets the head rotation of this NPC for a player + * @param player The {@link Player} to set the head rotation for + * @param yaw The yaw to set + * @param pitch The pitch to set + */ + void setHeadRotation(Player player, float yaw, float pitch); + + /** + * Sets the head rotation of this NPC for all players/viewers + * @param yaw The yaw to set + * @param pitch The pitch to set + */ + void setHeadRotation(float yaw, float pitch); + + /** + * @return The entity id of the packet entity that this npc object represents + */ + int getPacketEntityId(); + + /** + * @return The set of players that can currently see this npc + */ + Set getViewers(); + + /** + * Swings the entity's hand + * @param offHand Should the hand be the offhand + */ + void swingHand(boolean offHand); + + /** + * Gets the passengers of this npc + * @return The list of entity ids of the passengers + */ + + @Nullable List getPassengers(); + + /** + * Adds a passenger to this npc + * @param entityId The entity id of the passenger to add + */ + void addPassenger(int entityId); + + /** + * Removes a passenger from this npc + * @param entityId The entity id of the passenger to remove + */ + void removePassenger(int entityId); + + /** + * Gets the vehicle entity id of this npc + * @return The entity id of the vehicle + */ + @Nullable Integer getVehicleId(); + + /** + * Sets the vehicle id of this npc + * @param vehicleId The entity id of the vehicle + */ + void setVehicleId(Integer vehicleId); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java new file mode 100644 index 0000000..aca4bc3 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcEntry.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.api.npc; + +/** + * Base class for all NPC entries + * An NPC entry is a wrapper around an NPC that contains additional information + */ +public interface NpcEntry { + /** + * Gets the ID of this NPC entry + * @return The ID of this NPC entry + */ + String getId(); + /** + * Gets the NPC of this NPC entry + * @return The {@link Npc} of this NPC entry + */ + Npc getNpc(); + + /** + * Gets if this NPC entry is processed or not + * @return If this NPC entry is processed or not + */ + boolean isProcessed(); + /** + * Sets if this NPC entry is processed or not + * @param value If this NPC entry is processed or not + */ + void setProcessed(boolean value); + + /** + * @return If this NPC entry SHOULD be saved into the storage or not + */ + boolean isSave(); + /** + * Sets if this NPC should be saved or not + * @param value If this NPC entry should be saved or not + */ + void setSave(boolean value); + + /** + * Gets if this NPC can be modified by commands + * @return {@code true} if this NPC can be modified by commands, {@code false} otherwise + */ + boolean isAllowCommandModification(); + /** + * Sets if this NPC can be modified by commands + * @param value {@code true} if this NPC can be modified by commands, {@code false} otherwise + */ + void setAllowCommandModification(boolean value); + + /** + * Enables everything for this NPC entry + * That is, it makes the NPC processed, saveable, and allows command modification + */ + void enableEverything(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java new file mode 100644 index 0000000..2d41981 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcRegistry.java @@ -0,0 +1,90 @@ +package lol.pyr.znpcsplus.api.npc; + +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.World; + +import java.util.Collection; +import java.util.UUID; + +/** + * Base class for all NPC registries + */ +public interface NpcRegistry { + + /** + * Gets all NPC entries + * @return All NPC entries + */ + Collection getAll(); + + /** + * Gets all NPC IDs + * @return All NPC IDs + */ + Collection getAllIds(); + + /** + * Gets all NPC entries that are player made + * @return All player made NPC entries + */ + Collection getAllPlayerMade(); + + /** + * Gets IDs of all player made NPCs + * @return IDs of all player made NPCs + */ + Collection getAllPlayerMadeIds(); + + /** + * Creates a new NPC entry + * @param id The ID of the NPC entry + * @param world The {@link World} of the NPC entry + * @param type The {@link NpcType} of the NPC entry + * @param location The {@link NpcLocation} of the NPC entry + * @return The entry of the newly created npc + */ + NpcEntry create(String id, World world, NpcType type, NpcLocation location); + + /** + * Gets an NPC entry by its ID + * @param id The ID of the NPC entry + * @return The NPC entry + */ + NpcEntry getById(String id); + + /** + * Gets an NPC entry by its UUID + * @param uuid The UUID of the NPC entry + * @return The NPC entry + */ + NpcEntry getByUuid(UUID uuid); + + /** + * Deletes an NPC entry by its ID + * @param id The ID of the NPC entry + */ + void delete(String id); + + /** + * Deletes an NPC entry by its UUID + * @param uuid The UUID of the NPC entry + */ + void delete(UUID uuid); + + /** + * Register an NPC to this registry + * NpcEntry instances can be obtained through the NpcSerializer classes + * @param entry The npc to be registered + */ + void register(NpcEntry entry); + + /** + * Reload all saveable npcs from storage + */ + void reload(); + + /** + * Save all saveable npcs to storage + */ + void save(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java new file mode 100644 index 0000000..004f512 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcType.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.api.npc; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; + +import java.util.Set; + +/** + * Represents a type of NPC. + * This defines the {@link org.bukkit.entity.EntityType} of the NPC, as well as the properties that are allowed to be set on the NPC. + */ +public interface NpcType { + /** + * The name of the NPC type. + * @return The name of the NPC type. + */ + String getName(); + + /** + * The offset of the hologram above the NPC. + * @return the offset + */ + double getHologramOffset(); + + /** + * Set of properties that are allowed to be set on the NPC. + * @return allowed properties + */ + Set> getAllowedProperties(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java new file mode 100644 index 0000000..b0720db --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/npc/NpcTypeRegistry.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.api.npc; + +import java.util.Collection; + +/** + * Base for NpcType registries. + */ +public interface NpcTypeRegistry { + /** + * Gets a NPC type by name. + * @param name The name of the NPC type. + * @return The type that is represented by the name or null if it doesnt exist + */ + NpcType getByName(String name); + + /** + * Gets all NPC types. + * @return all of the npc types + */ + Collection getAll(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java new file mode 100644 index 0000000..f35cbed --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializer.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.api.serialization; + +import lol.pyr.znpcsplus.api.npc.NpcEntry; + +public interface NpcSerializer { + /** + * Serialize an npc into the type of this serializer + * @param entry The npc entry + * @return The serialized class + */ + T serialize(NpcEntry entry); + + /** + * Deserialize an npc from a serialized class + * Note: This npc will not be registered, you need to also register it using the NpcRegistry#register(NpcEntry) method + * @param model The serialized class + * @return The deserialized NpcEntry + */ + NpcEntry deserialize(T model); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java new file mode 100644 index 0000000..74e8d89 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/serialization/NpcSerializerRegistry.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.api.serialization; + +public interface NpcSerializerRegistry { + /** + * Get an NpcSerializer that serializes npcs into the provided class + * @param clazz The class to serialize into + * @return The npc serializer instance + * @param The type of the class that the serializer serializes into + */ + NpcSerializer getSerializer(Class clazz); + + /** + * Register an NpcSerializer to be used by other plugins + * @param clazz The class that the serializer serializes into + * @param serializer The serializer itself + * @param The type of the class that the serializer serializes into + */ + void registerSerializer(Class clazz, NpcSerializer serializer); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java new file mode 100644 index 0000000..53ed651 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/Skin.java @@ -0,0 +1,6 @@ +package lol.pyr.znpcsplus.api.skin; + +public interface Skin { + String getTexture(); + String getSignature(); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java new file mode 100644 index 0000000..2e93207 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptor.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.api.skin; + +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public interface SkinDescriptor { + CompletableFuture fetch(Player player); + Skin fetchInstant(Player player); + boolean supportsInstant(Player player); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java new file mode 100644 index 0000000..46d979a --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/api/skin/SkinDescriptorFactory.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.api.skin; + +import java.net.URL; +import java.util.UUID; + +/** + * Factory for creating skin descriptors. + */ +public interface SkinDescriptorFactory { + SkinDescriptor createMirrorDescriptor(); + SkinDescriptor createRefreshingDescriptor(String playerName); + SkinDescriptor createRefreshingDescriptor(UUID playerUUID); + SkinDescriptor createStaticDescriptor(String playerName); + SkinDescriptor createStaticDescriptor(String texture, String signature); + SkinDescriptor createUrlDescriptor(String url, String variant); + SkinDescriptor createUrlDescriptor(URL url, String variant); + SkinDescriptor createFileDescriptor(String path); +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java b/api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java new file mode 100644 index 0000000..bdea88f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/ArmadilloState.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum ArmadilloState { + IDLE, + ROLLING, + SCARED, + UNROLLING +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java b/api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java new file mode 100644 index 0000000..9322749 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/AttachDirection.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.util; + +public enum AttachDirection { + DOWN, + UP, + NORTH, + SOUTH, + WEST, + EAST +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java new file mode 100644 index 0000000..b38b907 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/AxolotlVariant.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum AxolotlVariant { + LUCY, + WILD, + GOLD, + CYAN, + BLUE +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java b/api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java new file mode 100644 index 0000000..6a4e41b --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/BlockState.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.util; + +public class BlockState { + private final int globalId; + + public BlockState(int globalId) { + this.globalId = globalId; + } + + public int getGlobalId() { + return globalId; + } + +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java new file mode 100644 index 0000000..9aaa7f4 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/CatVariant.java @@ -0,0 +1,15 @@ +package lol.pyr.znpcsplus.util; + +public enum CatVariant { + TABBY, + BLACK, + RED, + SIAMESE, + BRITISH_SHORTHAIR, + CALICO, + PERSIAN, + RAGDOLL, + WHITE, + JELLIE, + ALL_BLACK +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java b/api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java new file mode 100644 index 0000000..2b6a118 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/CreeperState.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.util; + +public enum CreeperState { + IDLE(-1), + FUSE(1); + + private final int state; + + CreeperState(int state) { + this.state = state; + } + + public int getState() { + return state; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java new file mode 100644 index 0000000..5905cdb --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/FoxVariant.java @@ -0,0 +1,6 @@ +package lol.pyr.znpcsplus.util; + +public enum FoxVariant { + RED, + SNOW +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java new file mode 100644 index 0000000..53eaa5c --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/FrogVariant.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.util; + +public enum FrogVariant { + TEMPERATE, + WARM, + COLD +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java new file mode 100644 index 0000000..80fa2f2 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseArmor.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseArmor { + NONE, + IRON, + GOLD, + DIAMOND +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java new file mode 100644 index 0000000..ae6327a --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseColor.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseColor { + WHITE, + CREAMY, + CHESTNUT, + BROWN, + BLACK, + GRAY, + DARK_BROWN +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java new file mode 100644 index 0000000..b5544c4 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseStyle.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseStyle { + NONE, + WHITE, + WHITEFIELD, + WHITE_DOTS, + BLACK_DOTS +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java b/api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java new file mode 100644 index 0000000..9526d67 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/HorseType.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum HorseType { + HORSE, + DONKEY, + MULE, + ZOMBIE, + SKELETON +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java new file mode 100644 index 0000000..be5cdc9 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/LlamaVariant.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum LlamaVariant { + CREAMY, + WHITE, + BROWN, + GRAY +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/LookType.java b/api/src/main/java/lol/pyr/znpcsplus/util/LookType.java new file mode 100644 index 0000000..e6af244 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/LookType.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.util; + +public enum LookType { + FIXED, + CLOSEST_PLAYER, + PER_PLAYER +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java new file mode 100644 index 0000000..f79fa0b --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/MooshroomVariant.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.util; + +public enum MooshroomVariant { + RED, + BROWN; + + public static String getVariantName(MooshroomVariant variant) { + return variant.name().toLowerCase(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java b/api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java new file mode 100644 index 0000000..42147d6 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/NamedColor.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.util; + +public enum NamedColor { + BLACK, + DARK_BLUE, + DARK_GREEN, + DARK_AQUA, + DARK_RED, + DARK_PURPLE, + GOLD, + GRAY, + DARK_GRAY, + BLUE, + GREEN, + AQUA, + RED, + LIGHT_PURPLE, + YELLOW, + WHITE +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java b/api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java new file mode 100644 index 0000000..422258d --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/NpcLocation.java @@ -0,0 +1,112 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.NumberConversions; + +import java.util.Objects; + +public class NpcLocation { + private final double x; + private final double y; + private final double z; + private final float yaw; + private final float pitch; + + public NpcLocation(double x, double y, double z, float yaw, float pitch) { + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + public NpcLocation(Location location) { + this(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + } + + public double getX() { + return this.x; + } + + public int getBlockX() { + return (int) getX(); + } + + public double getY() { + return this.y; + } + + public int getBlockY() { + return (int) getY(); + } + + public double getZ() { + return this.z; + } + + public int getBlockZ() { + return (int) getZ(); + } + + public float getYaw() { + return this.yaw; + } + + public float getPitch() { + return this.pitch; + } + + public NpcLocation centered() { + return new NpcLocation(Math.floor(x) + 0.5, y, Math.floor(z) + 0.5, yaw, pitch); + } + + public Location toBukkitLocation(World world) { + return new Location(world, this.x, this.y, this.z, this.yaw, this.pitch); + } + + public NpcLocation withY(double y) { + return new NpcLocation(x, y, z, yaw, pitch); + } + + private static final double _2PI = 2 * Math.PI; + + public NpcLocation lookingAt(Location loc) { + return lookingAt(new NpcLocation(loc)); + } + + public NpcLocation lookingAt(NpcLocation loc) { + final double x = loc.getX() - this.x; + final double z = loc.getZ() - this.z; + final double y = loc.getY() - this.y; + + if (x == 0 && z == 0) return new NpcLocation(this.x, this.y, this.z, this.yaw, y > 0 ? -90 : 90); + + double x2 = NumberConversions.square(x); + double z2 = NumberConversions.square(z); + double xz = Math.sqrt(x2 + z2); + + double theta = Math.atan2(-x, z); + float yaw = (float) Math.toDegrees((theta + _2PI) % _2PI); + float pitch = (float) Math.toDegrees(Math.atan(-y / xz)); + + return new NpcLocation(this.x, this.y, this.z, yaw, pitch); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NpcLocation that = (NpcLocation) o; + return Double.compare(that.x, x) == 0 && + Double.compare(that.y, y) == 0 && + Double.compare(that.z, z) == 0 && + Float.compare(that.yaw, yaw) == 0 && + Float.compare(that.pitch, pitch) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, z, yaw, pitch); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java b/api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java new file mode 100644 index 0000000..605702e --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/NpcPose.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.util; + +// TODO: Seperate this out into multiple classes and multiple properties depending on the npc type +// TODO: For example USING_TONGUE is only for the frog type but its usable everywhere + +// TODO #2: Add some backwards compatibility to some of these, like for example CROUCHING can be done +// TODO #2: on older versions using the general Entity number 0 bitmask +public enum NpcPose { + STANDING, + FALL_FLYING, + SLEEPING, + SWIMMING, + SPIN_ATTACK, + CROUCHING, + LONG_JUMPING, + DYING, + CROAKING, + USING_TONGUE, + SITTING, + ROARING, + SNIFFING, + EMERGING, + DIGGING, + SLIDING, + SHOOTING, + INHALING, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java b/api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java new file mode 100644 index 0000000..593ec57 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/OcelotType.java @@ -0,0 +1,8 @@ +package lol.pyr.znpcsplus.util; + +public enum OcelotType { + OCELOT, + TUXEDO, + TABBY, + SIAMESE, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java b/api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java new file mode 100644 index 0000000..dcb3f47 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/PandaGene.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum PandaGene { + NORMAL, + LAZY, + WORRIED, + PLAYFUL, + BROWN, + WEAK, + AGGRESSIVE +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java new file mode 100644 index 0000000..82d56b1 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/ParrotVariant.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum ParrotVariant { + RED_BLUE, + BLUE, + GREEN, + YELLOW_BLUE, + GRAY +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java b/api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java new file mode 100644 index 0000000..0243932 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/PuffState.java @@ -0,0 +1,7 @@ +package lol.pyr.znpcsplus.util; + +public enum PuffState { + DEFLATED, + HALF_INFLATED, + FULLY_INFLATED, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java b/api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java new file mode 100644 index 0000000..fbdb47f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/RabbitType.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.util; + +public enum RabbitType { + BROWN(0), + WHITE(1), + BLACK(2), + BLACK_AND_WHITE(3), + GOLD(4), + SALT_AND_PEPPER(5), + THE_KILLER_BUNNY(99), + TOAST(100); + + private final int id; + + RabbitType(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java b/api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java new file mode 100644 index 0000000..ba3f05f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/SkeletonType.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum SkeletonType { + NORMAL, + WITHER, + STRAY; + + public byte getLegacyId() { + return (byte) ordinal(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java b/api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java new file mode 100644 index 0000000..a532947 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/SnifferState.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum SnifferState { + IDLING, + FEELING_HAPPY, + SCENTING, + SNIFFING, + SEARCHING, + DIGGING, + RISING +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java b/api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java new file mode 100644 index 0000000..cf67c3b --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/SpellType.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.util; + +public enum SpellType { + NONE, + SUMMON_VEX, + ATTACK, + WOLOLO, + DISAPPEAR, + BLINDNESS, +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java new file mode 100644 index 0000000..e201886 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/TropicalFishVariant.java @@ -0,0 +1,104 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.DyeColor; + +import java.util.function.IntFunction; + +public class TropicalFishVariant { + + private final TropicalFishPattern pattern; + private final DyeColor bodyColor; + private final DyeColor patternColor; + + public TropicalFishVariant(TropicalFishPattern pattern, DyeColor bodyColor, DyeColor patternColor) { + this.pattern = pattern; + this.bodyColor = bodyColor; + this.patternColor = patternColor; + } + + public int getVariant() { + return pattern.getId() & '\uffff' | (bodyColor.ordinal() & 255) << 16 | (patternColor.ordinal() & 255) << 24; + } + + public enum TropicalFishPattern { + KOB(0, 0), + SUNSTREAK(0, 1), + SNOOPER(0, 2), + DASHER(0, 3), + BRINELY(0, 4), + SPOTTY(0, 5), + FLOPPER(1, 0), + STRIPEY(1, 1), + GLITTER(1, 2), + BLOCKFISH(1, 3), + BETTY(1, 4), + CLAYFISH(1, 5); + + private final int size; + private final int id; + private static final IntFunction BY_ID = (id) -> { + for (TropicalFishPattern pattern : values()) { + if (pattern.id == id) { + return pattern; + } + } + return null; + }; + + TropicalFishPattern(int size, int pattern) { + this.size = size; + this.id = size | pattern << 8; + } + + public int getSize() { + return size; + } + + public int getId() { + return id; + } + + public static TropicalFishPattern fromVariant(int variant) { + return BY_ID.apply(variant & '\uffff'); + } + } + + public static class Builder { + private TropicalFishPattern pattern; + private DyeColor bodyColor; + private DyeColor patternColor; + + public Builder() { + this.pattern = TropicalFishPattern.KOB; + this.bodyColor = DyeColor.WHITE; + this.patternColor = DyeColor.WHITE; + } + + public Builder pattern(TropicalFishPattern pattern) { + this.pattern = pattern; + return this; + } + + public Builder bodyColor(DyeColor bodyColor) { + this.bodyColor = bodyColor; + return this; + } + + public Builder patternColor(DyeColor patternColor) { + this.patternColor = patternColor; + return this; + } + + public static Builder fromInt(int variant) { + Builder builder = new Builder(); + builder.pattern = TropicalFishPattern.fromVariant(variant); + builder.bodyColor = DyeColor.values()[(variant >> 16) & 0xFF]; + builder.patternColor = DyeColor.values()[(variant >> 24) & 0xFF]; + return builder; + } + + public TropicalFishVariant build() { + return new TropicalFishVariant(pattern, bodyColor, patternColor); + } + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java new file mode 100644 index 0000000..eb55cac --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3f.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.util; + +public class Vector3f { + private final float x; + private final float y; + private final float z; + + public Vector3f() { + this.x = 0.0F; + this.y = 0.0F; + this.z = 0.0F; + } + + public Vector3f(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3f(String s) { + String[] split = s.split(","); + this.x = Float.parseFloat(split[0]); + this.y = Float.parseFloat(split[1]); + this.z = Float.parseFloat(split[2]); + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public float getZ() { + return this.z; + } + + public String toString() { + return this.x + "," + this.y + "," + this.z; + } + + public static Vector3f zero() { + return new Vector3f(); + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java new file mode 100644 index 0000000..ae6a933 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/Vector3i.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.util; + +public class Vector3i { + private final int x; + private final int y; + private final int z; + + public Vector3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getZ() { + return this.z; + } + + public String toString() { + return this.x + "," + this.y + "," + this.z; + } + + public String toPrettyString() { + return "(" + this.x + ", " + this.y + ", " + this.z + ")"; + } + + public static Vector3i fromString(String s) { + String[] split = s.split(","); + if (split.length < 3) { + return null; + } else { + try { + return new Vector3i(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); + } catch (NumberFormatException var3) { + return null; + } + } + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java new file mode 100644 index 0000000..c1d481f --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerLevel.java @@ -0,0 +1,9 @@ +package lol.pyr.znpcsplus.util; + +public enum VillagerLevel { + STONE, + IRON, + GOLD, + EMERALD, + DIAMOND +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java new file mode 100644 index 0000000..939f425 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerProfession.java @@ -0,0 +1,40 @@ +package lol.pyr.znpcsplus.util; + +public enum VillagerProfession { + NONE(0, 0), + ARMORER(1, 3), + BUTCHER(2, 4), + CARTOGRAPHER(3, 1), + CLERIC(4, 2), + FARMER(5, 0), + FISHERMAN(6, 0), + FLETCHER(7, 0), + LEATHER_WORKER(8, 4), + LIBRARIAN(9, 1), + MASON(10), + NITWIT(11, 5), + SHEPHERD(12, 0), + TOOL_SMITH(13, 3), + WEAPON_SMITH(14, 3); + + private final int id; + private final int legacyId; + + VillagerProfession(int id) { + this.id = id; + this.legacyId = 0; + } + + VillagerProfession(int id, int legacyId) { + this.id = id; + this.legacyId = legacyId; + } + + public int getId() { + return id; + } + + public int getLegacyId() { + return legacyId; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java new file mode 100644 index 0000000..77623ff --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/VillagerType.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.util; + +public enum VillagerType { + DESERT(0), + JUNGLE(1), + PLAINS(2), + SAVANNA(3), + SNOW(4), + SWAMP(5), + TAIGA(6); + private final int id; + + VillagerType(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java b/api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java new file mode 100644 index 0000000..fed5d66 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/WoldVariant.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.util; + +public enum WoldVariant { + PALE(3), + SPOTTED(6), + SNOWY(5), + BLACK(1), + ASHEN(0), + RUSTY(4), + WOODS(8), + CHESTNUT(2), + STRIPED(7); + + private final int id; + + WoldVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java b/api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java new file mode 100644 index 0000000..8751b82 --- /dev/null +++ b/api/src/main/java/lol/pyr/znpcsplus/util/ZombieType.java @@ -0,0 +1,11 @@ +package lol.pyr.znpcsplus.util; + +public enum ZombieType { + ZOMBIE, + FARMER, + LIBRARIAN, + PRIEST, + BLACKSMITH, + BUTCHER, + HUSK +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..39cfcbd --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +subprojects { + apply plugin: "java" + + group "lol.pyr" + version "2.1.0" + (System.getenv().containsKey("BUILD_ID") ? "-SNAPSHOT" : "") + + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(8)) + } + + dependencies { + compileOnly "org.jetbrains:annotations:26.0.1" + compileOnly "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT" + } + + repositories { + mavenCentral() + maven { + url "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" + } + maven { + url "https://repo.codemc.io/repository/maven-releases/" + } + maven { + url "https://libraries.minecraft.net" + } + maven { + url "https://repo.papermc.io/repository/maven-public/" + } + maven { + url "https://repo.extendedclip.com/content/repositories/placeholderapi/" + } + maven { + url "https://repo.pyr.lol/releases" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a595206 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100644 index 0000000..2d3def9 --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,71 @@ +plugins { + id "java" + id "com.github.johnrengelman.shadow" version "8.1.1" + id "xyz.jpenilla.run-paper" version "2.2.0" +} + +runServer { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + } + minecraftVersion "1.21.4" +} + +processResources { + expand("version": version) +} + +dependencies { + compileOnly "me.clip:placeholderapi:2.11.6" // Placeholder support + implementation "com.google.code.gson:gson:2.10.1" // JSON parsing + implementation "org.bstats:bstats-bukkit:3.0.2" // Plugin stats + implementation "com.github.retrooper:packetevents-spigot:2.9.1" // Packets + implementation "space.arim.dazzleconf:dazzleconf-ext-snakeyaml:1.2.1" // Configs + implementation "lol.pyr:director-adventure:2.1.2" // Commands + + // Fancy text library + implementation "net.kyori:adventure-platform-bukkit:4.3.4" + implementation "net.kyori:adventure-text-minimessage:4.17.0" + + implementation project(":api") +} + +ext { + gitBranch = System.getenv('GIT_BRANCH') ?: '' + gitCommitHash = System.getenv('GIT_COMMIT') ?: '' + buildId = System.getenv('BUILD_ID') ?: '' +} + +shadowJar { + archivesBaseName = "ZNPCsPlus" + archiveClassifier.set "" + + manifest { + if (gitBranch?.trim()) { + attributes('Git-Branch': gitBranch) + } + if (gitCommitHash?.trim()) { + attributes('Git-Commit': gitCommitHash) + } + if (buildId?.trim()) { + attributes('Build-Id': buildId) + } + } + + relocate "org.objectweb.asm", "lol.pyr.znpcsplus.libraries.asm" + relocate "me.lucko.jarrelocator", "lol.pyr.znpcsplus.libraries.jarrelocator" + + relocate "org.bstats", "lol.pyr.znpcsplus.libraries.bstats" + relocate "net.kyori", "lol.pyr.znpcsplus.libraries.kyori" + relocate "org.checkerframework", "lol.pyr.znpcsplus.libraries.checkerframework" + relocate "com.google.gson", "lol.pyr.znpcsplus.libraries.gson" + relocate "com.github.retrooper.packetevents", "lol.pyr.znpcsplus.libraries.packetevents.api" + relocate "io.github.retrooper.packetevents", "lol.pyr.znpcsplus.libraries.packetevents.impl" + relocate "org.yaml.snakeyaml", "lol.pyr.znpcsplus.libraries.snakeyaml" + relocate "space.arim.dazzleconf", "lol.pyr.znpcsplus.libraries.dazzleconf" + relocate "lol.pyr.director", "lol.pyr.znpcsplus.libraries.command" + + minimize() +} + +tasks.assemble.dependsOn shadowJar diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java new file mode 100644 index 0000000..4582c95 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java @@ -0,0 +1,361 @@ +package lol.pyr.znpcsplus; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandManager; +import lol.pyr.director.adventure.command.MultiCommand; +import lol.pyr.director.adventure.parse.primitive.BooleanParser; +import lol.pyr.director.adventure.parse.primitive.DoubleParser; +import lol.pyr.director.adventure.parse.primitive.FloatParser; +import lol.pyr.director.adventure.parse.primitive.IntegerParser; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.api.NpcApiProvider; +import lol.pyr.znpcsplus.api.NpcPropertyRegistryProvider; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.commands.*; +import lol.pyr.znpcsplus.commands.action.*; +import lol.pyr.znpcsplus.commands.hologram.*; +import lol.pyr.znpcsplus.commands.property.PropertyRemoveCommand; +import lol.pyr.znpcsplus.commands.property.PropertySetCommand; +import lol.pyr.znpcsplus.commands.storage.ImportCommand; +import lol.pyr.znpcsplus.commands.storage.LoadAllCommand; +import lol.pyr.znpcsplus.commands.storage.MigrateCommand; +import lol.pyr.znpcsplus.commands.storage.SaveAllCommand; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporterRegistry; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionFactoryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.interaction.InteractionPacketListener; +import lol.pyr.znpcsplus.npc.*; +import lol.pyr.znpcsplus.packets.*; +import lol.pyr.znpcsplus.parsers.*; +import lol.pyr.znpcsplus.scheduling.FoliaScheduler; +import lol.pyr.znpcsplus.scheduling.SpigotScheduler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.cache.SkinCacheCleanTask; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import lol.pyr.znpcsplus.tasks.HologramRefreshTask; +import lol.pyr.znpcsplus.tasks.NpcProcessorTask; +import lol.pyr.znpcsplus.tasks.ViewableHideOnLeaveListener; +import lol.pyr.znpcsplus.updater.UpdateChecker; +import lol.pyr.znpcsplus.updater.UpdateNotificationListener; +import lol.pyr.znpcsplus.user.ClientPacketListener; +import lol.pyr.znpcsplus.user.UserListener; +import lol.pyr.znpcsplus.user.UserManager; +import lol.pyr.znpcsplus.util.*; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bstats.bukkit.Metrics; +import org.bukkit.*; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginManager; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +public class ZNpcsPlus { + private final LegacyComponentSerializer textSerializer = LegacyComponentSerializer.builder() + .character('&') + .hexCharacter('#') + .hexColors().build(); + + private final List shutdownTasks = new ArrayList<>(); + private final PacketEventsAPI packetEvents; + private final ZNpcsPlusBootstrap bootstrap; + + private final ConfigManager configManager; + private final MojangSkinCache skinCache; + private final EntityPropertyRegistryImpl propertyRegistry; + + public ZNpcsPlus(ZNpcsPlusBootstrap bootstrap) { + this.bootstrap = bootstrap; + packetEvents = SpigotPacketEventsBuilder.build(bootstrap); + PacketEvents.setAPI(packetEvents); + packetEvents.getSettings().checkForUpdates(false); + packetEvents.load(); + + configManager = new ConfigManager(getDataFolder()); + skinCache = new MojangSkinCache(configManager, new File(getDataFolder(), "skins")); + propertyRegistry = new EntityPropertyRegistryImpl(skinCache, configManager); + + NpcPropertyRegistryProvider.register(bootstrap, propertyRegistry); + shutdownTasks.add(NpcPropertyRegistryProvider::unregister); + } + + private void log(String str) { + Bukkit.getConsoleSender().sendMessage(str); + } + + public void onEnable() { + getDataFolder().mkdirs(); + + log(ChatColor.YELLOW + " ___ __ __ __"); + log(ChatColor.YELLOW + " _/ |\\ | |__) | (__` " + ChatColor.GOLD + "__|__ " + ChatColor.YELLOW + getDescription().getName() + " " + ChatColor.GOLD + "v" + getDescription().getVersion()); + log(ChatColor.YELLOW + " /__ | \\| | |__ .__) " + ChatColor.GOLD + " | " + ChatColor.GRAY + "Maintained with " + ChatColor.RED + "\u2764 " + ChatColor.GRAY + " by Pyr#6969"); + log(""); + + PluginManager pluginManager = Bukkit.getPluginManager(); + long before = System.currentTimeMillis(); + + log(ChatColor.WHITE + " * Initializing libraries..."); + + packetEvents.init(); + + BukkitAudiences adventure = BukkitAudiences.create(bootstrap); + shutdownTasks.add(adventure::close); + + log(ChatColor.WHITE + " * Initializing components..."); + + TaskScheduler scheduler = FoliaUtil.isFolia() ? new FoliaScheduler(bootstrap) : new SpigotScheduler(bootstrap); + shutdownTasks.add(scheduler::cancelAll); + + + PacketFactory packetFactory = setupPacketFactory(scheduler, propertyRegistry, configManager); + propertyRegistry.registerTypes(packetFactory, textSerializer, scheduler); + + BungeeConnector bungeeConnector = new BungeeConnector(bootstrap); + ActionRegistryImpl actionRegistry = new ActionRegistryImpl(); + ActionFactoryImpl actionFactory = new ActionFactoryImpl(scheduler, adventure, textSerializer, bungeeConnector); + NpcTypeRegistryImpl typeRegistry = new NpcTypeRegistryImpl(); + NpcSerializerRegistryImpl serializerRegistry = new NpcSerializerRegistryImpl(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + NpcRegistryImpl npcRegistry = new NpcRegistryImpl(configManager, this, packetFactory, actionRegistry, + scheduler, typeRegistry, propertyRegistry, serializerRegistry, textSerializer); + shutdownTasks.add(npcRegistry::unload); + + UserManager userManager = new UserManager(); + shutdownTasks.add(userManager::shutdown); + + DataImporterRegistry importerRegistry = new DataImporterRegistry(configManager, adventure, + scheduler, packetFactory, textSerializer, typeRegistry, getDataFolder().getParentFile(), + propertyRegistry, skinCache, npcRegistry, bungeeConnector); + + log(ChatColor.WHITE + " * Registering components..."); + + bungeeConnector.registerChannel(); + shutdownTasks.add(bungeeConnector::unregisterChannel); + + typeRegistry.registerDefault(packetEvents, propertyRegistry); + actionRegistry.registerTypes(scheduler, adventure, textSerializer, bungeeConnector); + packetEvents.getEventManager().registerListener(new InteractionPacketListener(userManager, npcRegistry, typeRegistry, scheduler), PacketListenerPriority.MONITOR); + packetEvents.getEventManager().registerListener(new ClientPacketListener(configManager), PacketListenerPriority.LOWEST); + new Metrics(bootstrap, 18244); + pluginManager.registerEvents(new UserListener(userManager), bootstrap); + + registerCommands(npcRegistry, skinCache, adventure, actionRegistry, + typeRegistry, propertyRegistry, importerRegistry, configManager, packetFactory, serializerRegistry); + + log(ChatColor.WHITE + " * Starting tasks..."); + if (configManager.getConfig().checkForUpdates()) { + UpdateChecker updateChecker = new UpdateChecker(getDescription()); + scheduler.runDelayedTimerAsync(updateChecker, 5L, 6000L); + pluginManager.registerEvents(new UpdateNotificationListener(this, adventure, updateChecker, scheduler), bootstrap); + } + + scheduler.runDelayedTimerAsync(new NpcProcessorTask(npcRegistry, propertyRegistry, userManager), 60L, 3L); + scheduler.runDelayedTimerAsync(new HologramRefreshTask(npcRegistry), 60L, 20L); + scheduler.runDelayedTimerAsync(new SkinCacheCleanTask(skinCache), 1200, 1200); + pluginManager.registerEvents(new ViewableHideOnLeaveListener(), bootstrap); + + log(ChatColor.WHITE + " * Loading data..."); + npcRegistry.reload(); + if (configManager.getConfig().autoSaveEnabled()) shutdownTasks.add(npcRegistry::save); + + if (bootstrap.movedLegacy()) { + log(ChatColor.WHITE + " * Converting legacy data..."); + try { + Collection entries = importerRegistry.getImporter("znpcsplus_legacy").importData(); + npcRegistry.registerAll(entries); + } catch (Exception exception) { + log(ChatColor.RED + " * Legacy data conversion failed! Check conversion.log for more info."); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(new File(getDataFolder(), "conversion.log").toPath(), StandardOpenOption.CREATE_NEW))) { + exception.printStackTrace(writer); + } catch (IOException e) { + log(ChatColor.DARK_RED + " * Critical error! Writing to conversion.log failed."); + e.printStackTrace(); + } + } + } + + NpcApiProvider.register(bootstrap, new ZNpcsPlusApi(npcRegistry, typeRegistry, propertyRegistry, actionRegistry, actionFactory, skinCache, serializerRegistry)); + log(ChatColor.WHITE + " * Loading complete! (" + (System.currentTimeMillis() - before) + "ms)"); + log(""); + + if (configManager.getConfig().debugEnabled()) { + World world = Bukkit.getWorld("world"); + if (world == null) world = Bukkit.getWorlds().get(0); + int i = 0; + for (NpcTypeImpl type : typeRegistry.getAllImpl()) { + NpcEntryImpl entry = npcRegistry.create("debug_npc_" + i, world, type, new NpcLocation(i * 3, 200, 0, 0, 0)); + entry.setProcessed(true); + NpcImpl npc = entry.getNpc(); + npc.getHologram().addTextLineComponent(Component.text("Hello, World!", TextColor.color(255, 0, 0))); + npc.getHologram().addTextLineComponent(Component.text("Hello, World!", TextColor.color(0, 255, 0))); + npc.getHologram().addTextLineComponent(Component.text("Hello, World!", TextColor.color(0, 0, 255))); + i++; + } + } + } + + public void onDisable() { + NpcApiProvider.unregister(); + for (Runnable runnable : shutdownTasks) try { + runnable.run(); + } catch (Throwable throwable) { + bootstrap.getLogger().severe("One of the registered shutdown tasks threw an exception:"); + throwable.printStackTrace(); + } + shutdownTasks.clear(); + PacketEvents.getAPI().terminate(); + } + + private PacketFactory setupPacketFactory(TaskScheduler scheduler, EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager) { + HashMap> versions = new HashMap<>(); + versions.put(ServerVersion.V_1_8, LazyLoader.of(() -> new V1_8PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_17, LazyLoader.of(() -> new V1_17PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_19_3, LazyLoader.of(() -> new V1_19_3PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_20_2, LazyLoader.of(() -> new V1_20_2PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_21_3, LazyLoader.of(() -> new V1_21_3PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + versions.put(ServerVersion.V_1_21_7, LazyLoader.of(() -> new V1_21_7PacketFactory(scheduler, packetEvents, propertyRegistry, textSerializer, configManager))); + + ServerVersion version = packetEvents.getServerManager().getVersion(); + if (versions.containsKey(version)) return versions.get(version).get(); + for (ServerVersion v : ServerVersion.reversedValues()) { + if (v.isNewerThan(version)) continue; + if (!versions.containsKey(v)) continue; + return versions.get(v).get(); + } + throw new RuntimeException("Unsupported version!"); + } + + private void registerCommands(NpcRegistryImpl npcRegistry, MojangSkinCache skinCache, BukkitAudiences adventure, + ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, + EntityPropertyRegistryImpl propertyRegistry, DataImporterRegistry importerRegistry, + ConfigManager configManager, PacketFactory packetFactory, NpcSerializerRegistryImpl serializerRegistry) { + + Message incorrectUsageMessage = context -> context.send(Component.text("Incorrect usage: /" + context.getUsage(), NamedTextColor.RED)); + CommandManager manager = new CommandManager(bootstrap, adventure, incorrectUsageMessage); + + manager.registerParser(NpcTypeImpl.class, new NpcTypeParser(incorrectUsageMessage, typeRegistry)); + manager.registerParser(NpcEntryImpl.class, new NpcEntryParser(npcRegistry, incorrectUsageMessage)); + manager.registerParser(EntityPropertyImpl.class, new EntityPropertyParser(incorrectUsageMessage, propertyRegistry)); + manager.registerParser(Integer.class, new IntegerParser(incorrectUsageMessage)); + manager.registerParser(Double.class, new DoubleParser(incorrectUsageMessage)); + manager.registerParser(Float.class, new FloatParser(incorrectUsageMessage)); + manager.registerParser(Boolean.class, new BooleanParser(incorrectUsageMessage)); + manager.registerParser(NamedColor.class, new NamedColorParser(incorrectUsageMessage)); + manager.registerParser(InteractionType.class, new InteractionTypeParser(incorrectUsageMessage)); + manager.registerParser(Color.class, new ColorParser(incorrectUsageMessage)); + manager.registerParser(Vector3f.class, new Vector3fParser(incorrectUsageMessage)); + manager.registerParser(String.class, new StringParser(incorrectUsageMessage)); + manager.registerParser(Vector3i.class, new Vector3iParser(incorrectUsageMessage)); + + // TODO: Need to find a better way to do this + registerEnumParser(manager, NpcPose.class, incorrectUsageMessage); + registerEnumParser(manager, DyeColor.class, incorrectUsageMessage); + registerEnumParser(manager, CatVariant.class, incorrectUsageMessage); + registerEnumParser(manager, CreeperState.class, incorrectUsageMessage); + registerEnumParser(manager, ParrotVariant.class, incorrectUsageMessage); + registerEnumParser(manager, SpellType.class, incorrectUsageMessage); + registerEnumParser(manager, FoxVariant.class, incorrectUsageMessage); + registerEnumParser(manager, FrogVariant.class, incorrectUsageMessage); + registerEnumParser(manager, VillagerType.class, incorrectUsageMessage); + registerEnumParser(manager, VillagerProfession.class, incorrectUsageMessage); + registerEnumParser(manager, VillagerLevel.class, incorrectUsageMessage); + registerEnumParser(manager, AxolotlVariant.class, incorrectUsageMessage); + registerEnumParser(manager, HorseType.class, incorrectUsageMessage); + registerEnumParser(manager, HorseStyle.class, incorrectUsageMessage); + registerEnumParser(manager, HorseColor.class, incorrectUsageMessage); + registerEnumParser(manager, HorseArmor.class, incorrectUsageMessage); + registerEnumParser(manager, LlamaVariant.class, incorrectUsageMessage); + registerEnumParser(manager, MooshroomVariant.class, incorrectUsageMessage); + registerEnumParser(manager, OcelotType.class, incorrectUsageMessage); + registerEnumParser(manager, PandaGene.class, incorrectUsageMessage); + registerEnumParser(manager, PuffState.class, incorrectUsageMessage); + registerEnumParser(manager, LookType.class, incorrectUsageMessage); + registerEnumParser(manager, TropicalFishVariant.TropicalFishPattern.class, incorrectUsageMessage); + registerEnumParser(manager, SnifferState.class, incorrectUsageMessage); + registerEnumParser(manager, RabbitType.class, incorrectUsageMessage); + registerEnumParser(manager, AttachDirection.class, incorrectUsageMessage); + registerEnumParser(manager, Sound.class, incorrectUsageMessage); + registerEnumParser(manager, ArmadilloState.class, incorrectUsageMessage); + registerEnumParser(manager, WoldVariant.class, incorrectUsageMessage); + registerEnumParser(manager, NpcStorageType.class, incorrectUsageMessage); + registerEnumParser(manager, SkeletonType.class, incorrectUsageMessage); + + manager.registerCommand("npc", new MultiCommand(bootstrap.loadHelpMessage("root")) + .addSubcommand("center", new CenterCommand(npcRegistry)) + .addSubcommand("create", new CreateCommand(npcRegistry, typeRegistry)) + .addSubcommand("clone", new CloneCommand(npcRegistry)) + .addSubcommand("reloadconfig", new ReloadConfigCommand(configManager)) + .addSubcommand("toggle", new ToggleCommand(npcRegistry)) + .addSubcommand("skin", new SkinCommand(skinCache, npcRegistry, typeRegistry, propertyRegistry)) + .addSubcommand("delete", new DeleteCommand(npcRegistry, adventure)) + .addSubcommand("move", new MoveCommand(npcRegistry)) + .addSubcommand("teleport", new TeleportCommand(npcRegistry)) + .addSubcommand("list", new ListCommand(npcRegistry)) + .addSubcommand("near", new NearCommand(npcRegistry)) + .addSubcommand("type", new TypeCommand(npcRegistry, typeRegistry)) + .addSubcommand("setlocation", new SetLocationCommand(npcRegistry)) + .addSubcommand("lookatme", new LookAtMeCommand(npcRegistry)) + .addSubcommand("setrotation", new SetRotationCommand(npcRegistry)) + .addSubcommand("changeid", new ChangeIdCommand(npcRegistry)) + .addSubcommand("property", new MultiCommand(bootstrap.loadHelpMessage("property")) + .addSubcommand("set", new PropertySetCommand(npcRegistry)) + .addSubcommand("remove", new PropertyRemoveCommand(npcRegistry))) + .addSubcommand("storage", new MultiCommand(bootstrap.loadHelpMessage("storage")) + .addSubcommand("save", new SaveAllCommand(npcRegistry)) + .addSubcommand("reload", new LoadAllCommand(npcRegistry)) + .addSubcommand("import", new ImportCommand(npcRegistry, importerRegistry)) + .addSubcommand("migrate", new MigrateCommand(configManager, this, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, npcRegistry.getStorage(), configManager.getConfig().storageType(), npcRegistry, serializerRegistry))) + .addSubcommand("holo", new MultiCommand(bootstrap.loadHelpMessage("holo")) + .addSubcommand("add", new HoloAddCommand(npcRegistry)) + .addSubcommand("additem", new HoloAddItemCommand(npcRegistry)) + .addSubcommand("delete", new HoloDeleteCommand(npcRegistry)) + .addSubcommand("info", new HoloInfoCommand(npcRegistry)) + .addSubcommand("insert", new HoloInsertCommand(npcRegistry)) + .addSubcommand("insertitem", new HoloInsertItemCommand(npcRegistry)) + .addSubcommand("set", new HoloSetCommand(npcRegistry)) + .addSubcommand("setitem", new HoloSetItemCommand(npcRegistry)) + .addSubcommand("offset", new HoloOffsetCommand(npcRegistry)) + .addSubcommand("refreshdelay", new HoloRefreshDelayCommand(npcRegistry))) + .addSubcommand("action", new MultiCommand(bootstrap.loadHelpMessage("action")) + .addSubcommand("add", new ActionAddCommand(npcRegistry, actionRegistry)) + .addSubcommand("clear", new ActionClearCommand(npcRegistry)) + .addSubcommand("delete", new ActionDeleteCommand(npcRegistry)) + .addSubcommand("edit", new ActionEditCommand(npcRegistry, actionRegistry)) + .addSubcommand("list", new ActionListCommand(npcRegistry))) + .addSubcommand("version", new VersionCommand(this)) + ); + } + + private > void registerEnumParser(CommandManager manager, Class clazz, Message message) { + manager.registerParser(clazz, new EnumParser<>(clazz, message)); + } + + public File getDataFolder() { + return bootstrap.getDataFolder(); + } + + public PluginDescriptionFile getDescription() { + return bootstrap.getDescription(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java new file mode 100644 index 0000000..7d272da --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusApi.java @@ -0,0 +1,73 @@ +package lol.pyr.znpcsplus; + +import lol.pyr.znpcsplus.api.NpcApi; +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.interaction.ActionFactory; +import lol.pyr.znpcsplus.api.interaction.ActionRegistry; +import lol.pyr.znpcsplus.api.npc.NpcRegistry; +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptorFactory; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionFactoryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.skin.SkinDescriptorFactoryImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; + +public class ZNpcsPlusApi implements NpcApi { + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final ActionRegistryImpl actionRegistry; + private final ActionFactoryImpl actionFactory; + private final SkinDescriptorFactoryImpl skinDescriptorFactory; + private final NpcSerializerRegistryImpl npcSerializerRegistry; + + public ZNpcsPlusApi(NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, ActionRegistryImpl actionRegistry, ActionFactoryImpl actionFactory, MojangSkinCache skinCache, NpcSerializerRegistryImpl npcSerializerRegistry) { + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.actionRegistry = actionRegistry; + this.actionFactory = actionFactory; + this.skinDescriptorFactory = new SkinDescriptorFactoryImpl(skinCache); + this.npcSerializerRegistry = npcSerializerRegistry; + } + + @Override + public NpcRegistry getNpcRegistry() { + return npcRegistry; + } + + @Override + public NpcTypeRegistry getNpcTypeRegistry() { + return typeRegistry; + } + + @Override + public EntityPropertyRegistry getPropertyRegistry() { + return propertyRegistry; + } + + @Override + public ActionRegistry getActionRegistry() { + return actionRegistry; + } + + @Override + public ActionFactory getActionFactory() { + return actionFactory; + } + + + @Override + public SkinDescriptorFactory getSkinDescriptorFactory() { + return skinDescriptorFactory; + } + + @Override + public NpcSerializerRegistryImpl getNpcSerializerRegistry() { + return npcSerializerRegistry; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java new file mode 100644 index 0000000..9eb1ab9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java @@ -0,0 +1,73 @@ +package lol.pyr.znpcsplus; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.FileUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ZNpcsPlusBootstrap extends JavaPlugin { + private ZNpcsPlus zNpcsPlus; + private boolean legacy; + + @Override + public void onLoad() { + legacy = new File(getDataFolder(), "data.json").isFile() && !new File(getDataFolder(), "data").isDirectory(); + if (legacy) try { + Files.move(getDataFolder().toPath(), new File(getDataFolder().getParentFile(), "ZNPCsPlusLegacy").toPath()); + } catch (IOException e) { + getLogger().severe(ChatColor.RED + "Failed to move legacy data folder! Plugin will disable."); + e.printStackTrace(); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + zNpcsPlus = new ZNpcsPlus(this); + } + + @Override + public void onEnable() { + if (zNpcsPlus != null) zNpcsPlus.onEnable(); + } + + @Override + public void onDisable() { + if (zNpcsPlus != null) zNpcsPlus.onDisable(); + } + + private final static Pattern EMBEDDED_FILE_PATTERN = Pattern.compile("\\{@(.*?)}"); + + private String loadMessageFile(String file) { + Reader reader = getTextResource("messages/" + file + ".txt"); + if (reader == null) throw new RuntimeException(file + ".txt is missing from ZNPCsPlus jar!"); + String text = FileUtil.dumpReaderAsString(reader); + Matcher matcher = EMBEDDED_FILE_PATTERN.matcher(text); + StringBuilder builder = new StringBuilder(); + int lastMatchEnd = 0; + while (matcher.find()) { + builder.append(text, lastMatchEnd, matcher.start()); + lastMatchEnd = matcher.end(); + builder.append(loadMessageFile(matcher.group(1))); + } + builder.append(text, lastMatchEnd, text.length()); + return builder.toString(); + } + + protected Message loadHelpMessage(String name) { + Component component = MiniMessage.miniMessage().deserialize(loadMessageFile(name)); + return context -> context.send(component); + } + + public boolean movedLegacy() { + return legacy; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java new file mode 100644 index 0000000..b1a806d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CenterCommand.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class CenterCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public CenterCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " center "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.setLocation(npc.getLocation().centered()); + context.send(Component.text("NPC has been centered on it's current block.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java new file mode 100644 index 0000000..1d64db6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ChangeIdCommand.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class ChangeIdCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ChangeIdCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " changeid "); + NpcEntryImpl old = context.parse(NpcEntryImpl.class); + String newId = context.popString(); + if (npcRegistry.getById(newId) != null) context.halt(Component.text("There is already an npc with the new id you have provided", NamedTextColor.RED)); + npcRegistry.switchIds(old.getId(), newId); + context.send(Component.text("Npc's id changed to " + newId.toLowerCase(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java new file mode 100644 index 0000000..eb9d001 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CloneCommand.java @@ -0,0 +1,44 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class CloneCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public CloneCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " clone "); + Player player = context.ensureSenderIsPlayer(); + + String id = context.popString(); + if (npcRegistry.getById(id) == null) context.halt(Component.text("NPC with ID " + id + " does not exist.", NamedTextColor.RED)); + String newId = context.popString(); + if (npcRegistry.getById(newId) != null) context.halt(Component.text("NPC with ID " + newId + " already exists.", NamedTextColor.RED)); + + npcRegistry.clone(id, newId, player.getWorld(), new NpcLocation(player.getLocation())); + + context.send(Component.text("Cloned NPC with ID " + id + " to ID " + newId + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java new file mode 100644 index 0000000..341edca --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/CreateCommand.java @@ -0,0 +1,54 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class CreateCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + + public CreateCommand(NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry) { + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " create []"); + Player player = context.ensureSenderIsPlayer(); + + String id = context.popString(); + if (npcRegistry.getById(id) != null) context.halt(Component.text("NPC with that ID already exists.", NamedTextColor.RED)); + + NpcTypeImpl type; + if (context.argSize() == 1) { + type = context.parse(NpcTypeImpl.class); + } else { + type = typeRegistry.getByName("player"); + } + + NpcEntryImpl entry = npcRegistry.create(id, player.getWorld(), type, new NpcLocation(player.getLocation())); + entry.enableEverything(); + + context.send(Component.text("Created a " + type.getName() + " NPC with ID " + id + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(typeRegistry.getAllImpl().stream().map(NpcTypeImpl::getName)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java new file mode 100644 index 0000000..d22cc52 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/DeleteCommand.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class DeleteCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final BukkitAudiences adventure; + + public DeleteCommand(NpcRegistryImpl npcRegistry, BukkitAudiences adventure) { + this.npcRegistry = npcRegistry; + this.adventure = adventure; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " delete "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + npcRegistry.delete(entry.getId()); + adventure.sender(context.getSender()).sendMessage(Component.text("Deleted NPC with ID: " + entry.getId(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java new file mode 100644 index 0000000..09bff36 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ListCommand.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +public class ListCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ListCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + Component component = Component.text("Npc List:", NamedTextColor.GOLD).appendNewline(); + for (String id : npcRegistry.getModifiableIds()) { + NpcImpl npc = npcRegistry.getById(id).getNpc(); + NpcLocation location = npc.getLocation(); + component = component.append(Component.text("ID: " + id, npc.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Type: ", NamedTextColor.GREEN)) + .append(Component.text(npc.getType().getName(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Location: " + npc.getWorldName() + " X:" + location.getBlockX() + " Y:" + location.getBlockY() + " Z:" + location.getBlockZ(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[TELEPORT]", NamedTextColor.DARK_GREEN).clickEvent(ClickEvent.runCommand("/znpcs teleport " + id))) + .appendNewline(); + } + context.send(component); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java new file mode 100644 index 0000000..d95a146 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/LookAtMeCommand.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class LookAtMeCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public LookAtMeCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " lookatme "); + Player player = context.ensureSenderIsPlayer(); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.setLocation(npc.getLocation().lookingAt(player.getLocation())); + context.send(Component.text("NPC is now looking at you.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java new file mode 100644 index 0000000..452cf44 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/MoveCommand.java @@ -0,0 +1,40 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class MoveCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public MoveCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " move "); + Player player = context.ensureSenderIsPlayer(); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.setLocation(new NpcLocation(player.getLocation())); + if (!Objects.equals(npc.getWorld(), player.getWorld())) npc.setWorld(player.getWorld()); + context.send(Component.text("NPC moved to your current location.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java new file mode 100644 index 0000000..6f965f7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/NearCommand.java @@ -0,0 +1,60 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class NearCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public NearCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " near "); + Player player = context.ensureSenderIsPlayer(); + int raw = context.parse(Integer.class); + double radius = Math.pow(raw, 2); + + List entries = npcRegistry.getAllModifiable().stream() + .filter(entry -> Objects.equals(entry.getNpc().getWorld(), player.getWorld())) + .filter(entry -> { + Location loc = entry.getNpc().getBukkitLocation(); + return loc != null && loc.distanceSquared(player.getLocation()) < radius; + }) + .collect(Collectors.toList()); + + if (entries.isEmpty()) context.halt(Component.text("There are no npcs within " + raw + " blocks around you.", NamedTextColor.RED)); + + Component component = Component.text("All NPCs that are within " + raw + " blocks from you:", NamedTextColor.GOLD).appendNewline(); + for (NpcEntryImpl entry : entries) { + NpcImpl npc = entry.getNpc(); + NpcLocation location = npc.getLocation(); + component = component.append(Component.text("ID: " + entry.getId(), npc.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Type: ", NamedTextColor.GREEN)) + .append(Component.text(npc.getType().getName(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Location: " + npc.getWorldName() + " X:" + location.getBlockX() + " Y:" + location.getBlockY() + " Z:" + location.getBlockZ(), NamedTextColor.GREEN)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[TELEPORT]", NamedTextColor.DARK_GREEN).clickEvent(ClickEvent.runCommand("/znpcs teleport " + entry.getId()))) + .appendNewline(); + } + context.send(component); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java new file mode 100644 index 0000000..2b9e12a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ReloadConfigCommand.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.config.ConfigManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +public class ReloadConfigCommand implements CommandHandler { + private final ConfigManager configManager; + + public ReloadConfigCommand(ConfigManager configManager) { + this.configManager = configManager; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + configManager.reload(); + context.send(Component.text("Plugin configuration reloaded successfully", NamedTextColor.GREEN)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java new file mode 100644 index 0000000..948d017 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetLocationCommand.java @@ -0,0 +1,56 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SetLocationCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public SetLocationCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " setlocation "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + double x = parseLocation(context.popString(), npc.getLocation().getX()); + double y = parseLocation(context.popString(), npc.getLocation().getY()); + double z = parseLocation(context.popString(), npc.getLocation().getZ()); + npc.setLocation(new NpcLocation(x, y, z, npc.getLocation().getYaw(), npc.getLocation().getPitch())); + context.send(Component.text("NPC has been moved to " + x + ", " + y + ", " + z + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + NpcImpl npc = context.suggestionParse(0, NpcEntryImpl.class).getNpc(); + if (context.argSize() == 2) return Arrays.asList(String.valueOf(npc.getLocation().getX()), "~"); + else if (context.argSize() == 3) return Arrays.asList(String.valueOf(npc.getLocation().getY()), "~"); + else if (context.argSize() == 4) return Arrays.asList(String.valueOf(npc.getLocation().getZ()), "~"); + return Collections.emptyList(); + } + + private static double parseLocation(String input, double current) throws CommandExecutionException { + if (input.equals("~")) return current; + if (input.startsWith("~")) { + try { + return current + Double.parseDouble(input.substring(1)); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } + return Double.parseDouble(input); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java new file mode 100644 index 0000000..dda74dc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SetRotationCommand.java @@ -0,0 +1,58 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SetRotationCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public SetRotationCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " setrotation "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + float yaw = parseRotation(context.popString(), npc.getLocation().getYaw()); + float pitch = parseRotation(context.popString(), npc.getLocation().getPitch()); + if (pitch < -90 || pitch > 90) { + pitch = Math.min(Math.max(pitch, -90), 90); + context.send(Component.text("Warning: pitch is outside of the -90 to 90 range. It has been normalized to " + pitch + ".", NamedTextColor.YELLOW)); + } + npc.setLocation(new NpcLocation(npc.getLocation().getX(), npc.getLocation().getY(), npc.getLocation().getZ(), yaw, pitch)); + context.send(Component.text("NPC has been rotated to " + yaw + ", " + pitch + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + NpcImpl npc = context.suggestionParse(0, NpcEntryImpl.class).getNpc(); + if (context.argSize() == 2) return Arrays.asList(String.valueOf(npc.getLocation().getYaw()), "~"); + else if (context.argSize() == 3) return Arrays.asList(String.valueOf(npc.getLocation().getPitch()), "~"); + return Collections.emptyList(); + } + + private static float parseRotation(String input, float current) throws CommandExecutionException { + if (input.equals("~")) return current; + if (input.startsWith("~")) { + try { + return current + Float.parseFloat(input.substring(1)); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } + return Float.parseFloat(input); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java new file mode 100644 index 0000000..48fa332 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/SkinCommand.java @@ -0,0 +1,143 @@ +package lol.pyr.znpcsplus.commands; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class SkinCommand implements CommandHandler { + private final MojangSkinCache skinCache; + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + + public SkinCommand(MojangSkinCache skinCache, NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry) { + this.skinCache = skinCache; + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " skin [value]"); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + if (npc.getType() != typeRegistry.getByEntityType(EntityTypes.PLAYER)) context.halt(Component.text("The NPC must be a player to have a skin", NamedTextColor.RED)); + String type = context.popString(); + + if (type.equalsIgnoreCase("mirror")) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + npc.respawn(); + context.halt(Component.text("The NPC's skin will now mirror the player that it's being displayed to", NamedTextColor.GREEN)); + } else if (type.equalsIgnoreCase("static")) { + context.ensureArgsNotEmpty(); + String name = context.dumpAllArgs(); + context.send(Component.text("Fetching skin \"" + name + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.forPlayer(skinCache, name).thenAccept(skin -> { + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the player name is valid?", NamedTextColor.RED)); + return; + } + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin); + npc.respawn(); + context.send(Component.text("The NPC's skin has been set to \"" + name + "\"", NamedTextColor.GREEN)); + }); + return; + } else if (type.equalsIgnoreCase("dynamic")) { + context.ensureArgsNotEmpty(); + String name = context.dumpAllArgs(); + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new NameFetchingDescriptor(skinCache, name)); + npc.respawn(); + context.halt(Component.text("The NPC's skin will now be resolved per-player from \"" + name + "\"")); + } else if (type.equalsIgnoreCase("url")) { + context.ensureArgsNotEmpty(); + String variant = context.popString().toLowerCase(); + if (!variant.equalsIgnoreCase("slim") && !variant.equalsIgnoreCase("classic")) { + context.send(Component.text("Invalid skin variant! Please use one of the following: slim, classic", NamedTextColor.RED)); + return; + } + String urlString = context.dumpAllArgs(); + try { + URL url = new URL(urlString); + context.send(Component.text("Fetching skin from url \"" + urlString + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.fromUrl(skinCache, url , variant).thenAccept(skin -> { + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the url is valid?", NamedTextColor.RED)); + return; + } + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin); + npc.respawn(); + context.send(Component.text("The NPC's skin has been set.", NamedTextColor.GREEN)); + }); + } catch (MalformedURLException e) { + context.send(Component.text("Invalid url!", NamedTextColor.RED)); + } + return; + } else if (type.equalsIgnoreCase("file")) { + context.ensureArgsNotEmpty(); + String path = context.dumpAllArgs(); + context.send(Component.text("Fetching skin from file \"" + path + "\"...", NamedTextColor.GREEN)); + PrefetchedDescriptor.fromFile(skinCache, path).exceptionally(e -> { + if (e instanceof FileNotFoundException || e.getCause() instanceof FileNotFoundException) { + context.send(Component.text("A file at the specified path could not be found!", NamedTextColor.RED)); + } else { + context.send(Component.text("An error occurred while fetching the skin from file! Check the console for more details.", NamedTextColor.RED)); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + return null; + }).thenAccept(skin -> { + if (skin == null) return; + if (skin.getSkin() == null) { + context.send(Component.text("Failed to fetch skin, are you sure the file path is valid?", NamedTextColor.RED)); + return; + } + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), skin); + npc.respawn(); + context.send(Component.text("The NPC's skin has been set.", NamedTextColor.GREEN)); + }); + return; + } + context.send(Component.text("Unknown skin type! Please use one of the following: mirror, static, dynamic, url")); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestLiteral("mirror", "static", "dynamic", "url", "file"); + if (context.matchSuggestion("*", "static")) return context.suggestPlayers(); + if (context.argSize() == 3 && context.matchSuggestion("*", "url")) { + return context.suggestLiteral("slim", "classic"); + } + if (context.argSize() == 3 && context.matchSuggestion("*", "file")) { + if (skinCache.getSkinsFolder().exists()) { + File[] files = skinCache.getSkinsFolder().listFiles(); + if (files != null) { + return Arrays.stream(files).map(File::getName).collect(Collectors.toList()); + } + } + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java new file mode 100644 index 0000000..b9705cd --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TeleportCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FoliaUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class TeleportCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public TeleportCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " teleport "); + Player player = context.ensureSenderIsPlayer(); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + Location location = npc.getBukkitLocation(); + if (location == null) context.halt("Unable to teleport to NPC, the world is not loaded!"); + FoliaUtil.teleport(player, location); + context.send(Component.text("Teleported to NPC!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java new file mode 100644 index 0000000..60a09c8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/ToggleCommand.java @@ -0,0 +1,42 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class ToggleCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ToggleCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " toggle []"); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + boolean enabled; + if (context.argSize() == 1) { + enabled = context.popString().equalsIgnoreCase("enable"); + } else { + enabled = !npc.isEnabled(); + } + npc.setEnabled(enabled); + context.send(Component.text("NPC has been " + (enabled ? "enabled" : "disabled"), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestLiteral("enable", "disable"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java new file mode 100644 index 0000000..4d57468 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/TypeCommand.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class TypeCommand implements CommandHandler { + private final NpcRegistryImpl registry; + private final NpcTypeRegistryImpl typeRegistry; + + public TypeCommand(NpcRegistryImpl registry, NpcTypeRegistryImpl typeRegistry) { + this.registry = registry; + this.typeRegistry = typeRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " type "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + NpcTypeImpl type = context.parse(NpcTypeImpl.class); + npc.setType(type); + context.send(Component.text("NPC type set to " + type.getName() + ".", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(registry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(typeRegistry.getAllImpl().stream().map(NpcTypeImpl::getName)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java new file mode 100644 index 0000000..77ce2c0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/VersionCommand.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.commands; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.ZNpcsPlus; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +public class VersionCommand implements CommandHandler { + + private final String pluginVersion; + private final String gitBranch; + private final String gitCommitHash; + private final String buildId; + + public VersionCommand(ZNpcsPlus plugin) { + pluginVersion = plugin.getDescription().getVersion(); + String gitBranch = ""; + String gitCommitHash = ""; + String buildId = ""; + try { + URL jarUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + JarFile jarFile = new JarFile(jarUrl.toURI().getPath()); + Attributes attributes = jarFile.getManifest().getMainAttributes(); + gitBranch = attributes.getValue("Git-Branch"); + gitCommitHash = attributes.getValue("Git-Commit-Hash"); + buildId = attributes.getValue("Build-Id"); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + this.gitBranch = gitBranch; + this.gitCommitHash = gitCommitHash; + this.buildId = buildId; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + + StringBuilder versionBuilder = new StringBuilder("This server is running ZNPCsPlus version ").append(pluginVersion); + if (gitBranch != null && !gitBranch.isEmpty()) { + versionBuilder.append("-").append(gitBranch); + } + if (gitCommitHash != null && !gitCommitHash.isEmpty()) { + versionBuilder.append("@").append(gitCommitHash); + } + if (buildId != null && !buildId.isEmpty()) { + versionBuilder.append(" (Build #").append(buildId).append(")"); + } else { + versionBuilder.append(" (Development Build)"); + } + + String version = versionBuilder.toString(); + + context.send(Component.text(version, NamedTextColor.GREEN) + .hoverEvent(Component.text("Click to copy version to clipboard")) + .clickEvent(ClickEvent.copyToClipboard(version))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java new file mode 100644 index 0000000..09b0749 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionAddCommand.java @@ -0,0 +1,51 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ActionAddCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final ActionRegistryImpl actionRegistry; + + public ActionAddCommand(NpcRegistryImpl npcRegistry, ActionRegistryImpl actionRegistry) { + this.npcRegistry = npcRegistry; + this.actionRegistry = actionRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + List commands = actionRegistry.getCommands(); + context.setUsage(context.getLabel() + " action add "); + String sub = context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) { + context.setUsage(context.getLabel() + " action add"); + command.run(context); + return; + } + context.send(Component.text("Invalid action type, available action types:\n" + + commands.stream().map(InteractionCommandHandler::getSubcommandName).collect(Collectors.joining(", ")), NamedTextColor.RED)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + List commands = actionRegistry.getCommands(); + if (context.argSize() == 1) return context.suggestStream(commands.stream().map(InteractionCommandHandler::getSubcommandName)); + if (context.argSize() == 2) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() >= 3) { + String sub = context.popString(); + context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) return command.suggest(context); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java new file mode 100644 index 0000000..2aa709a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionClearCommand.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class ActionClearCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ActionClearCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action clear "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + if (npc.getActions().size() == 0) context.halt(Component.text("That npc doesn't have any actions", NamedTextColor.RED)); + npc.clearActions(); + context.send(Component.text("Removed all actions from the npc", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java new file mode 100644 index 0000000..b282a35 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionDeleteCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class ActionDeleteCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ActionDeleteCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action delete "); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + int index = context.parse(Integer.class); + if (index >= npc.getActions().size() || index < 0) context.halt(Component.text("That npc doesn't have any action with the index " + index, NamedTextColor.RED)); + npc.removeAction(index); + context.send(Component.text("Removed action with index " + index, NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getActions().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java new file mode 100644 index 0000000..0b0eafe --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionEditCommand.java @@ -0,0 +1,64 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ActionEditCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final ActionRegistryImpl actionRegistry; + + private InteractionCommandHandler commandHandler = null; + + public ActionEditCommand(NpcRegistryImpl npcRegistry, ActionRegistryImpl actionRegistry) { + this.npcRegistry = npcRegistry; + this.actionRegistry = actionRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action edit ..."); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + int index = context.parse(Integer.class); + if (index >= entry.getNpc().getActions().size() || index < 0) context.halt(Component.text("That npc doesn't have any action with the index " + index, NamedTextColor.RED)); + List commands = actionRegistry.getCommands(); + String sub = context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) { + this.commandHandler = command; + } + if (this.commandHandler == null) { + context.send(Component.text("Invalid action type, available action types:\n" + + commands.stream().map(InteractionCommandHandler::getSubcommandName).collect(Collectors.joining(", ")), NamedTextColor.RED)); + } + InteractionAction newAction = this.commandHandler.parse(context); + entry.getNpc().editAction(index, newAction); + context.send(Component.text("Edited action with index " + index + " of Npc " + entry.getId(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getActions().size()) + .map(String::valueOf)); + List commands = actionRegistry.getCommands(); + if (context.argSize() == 3) return context.suggestStream(commands.stream().map(InteractionCommandHandler::getSubcommandName)); + context.popString(); + context.popString(); + String sub = context.popString(); + for (InteractionCommandHandler command : commands) if (command.getSubcommandName().equalsIgnoreCase(sub)) return command.suggest(context); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java new file mode 100644 index 0000000..fe12bd9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/action/ActionListCommand.java @@ -0,0 +1,39 @@ +package lol.pyr.znpcsplus.commands.action; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Collections; +import java.util.List; + +public class ActionListCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public ActionListCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " action list "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + List actions = entry.getNpc().getActions(); + context.send("Actions of Npc " + entry.getId() + ":"); + for (int i = 0; i < actions.size(); i++) { + if (actions.get(i) instanceof InteractionActionImpl) { + context.send(((InteractionActionImpl) actions.get(i)).getInfo(entry.getId(), i, context)); + } + } + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java new file mode 100644 index 0000000..f739d18 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddCommand.java @@ -0,0 +1,43 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.hologram.HologramItem; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class HoloAddCommand implements CommandHandler { + private final NpcRegistryImpl registry; + + public HoloAddCommand(NpcRegistryImpl registry) { + this.registry = registry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo add "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + context.ensureArgsNotEmpty(); + String in = context.dumpAllArgs(); + if (in.toLowerCase().startsWith("item:")) { + if (!HologramItem.ensureValidItemInput(in.substring(5))) { + context.halt(Component.text("The item input is invalid!", NamedTextColor.RED)); + } + } + hologram.addLine(in); + context.send(Component.text("NPC line added!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(registry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java new file mode 100644 index 0000000..cb23a56 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloAddItemCommand.java @@ -0,0 +1,39 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; + +public class HoloAddItemCommand implements CommandHandler { + private final NpcRegistryImpl registry; + + public HoloAddItemCommand(NpcRegistryImpl registry) { + this.registry = registry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo additem "); + Player player = context.ensureSenderIsPlayer(); + org.bukkit.inventory.ItemStack itemStack = player.getInventory().getItemInHand(); + if (itemStack == null) context.halt(Component.text("You must be holding an item!", NamedTextColor.RED)); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + hologram.addItemLineStack(itemStack); + context.send(Component.text("NPC item line added!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(registry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java new file mode 100644 index 0000000..fdf5b27 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloDeleteCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloDeleteCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloDeleteCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo delete "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + hologram.removeLine(line); + context.send(Component.text("NPC line removed.", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram().getLines().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java new file mode 100644 index 0000000..20c5a6c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInfoCommand.java @@ -0,0 +1,41 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class HoloInfoCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloInfoCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo info "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + HologramImpl hologram = entry.getNpc().getHologram(); + Component component = Component.text("NPC Hologram Info of ID " + entry.getId() + ":", NamedTextColor.GREEN).appendNewline(); + for (int i = 0; i < hologram.getLines().size(); i++) { + component = component.append(Component.text(i + ") ", NamedTextColor.GREEN)) + .append(Component.text(hologram.getLine(i), NamedTextColor.WHITE)) + .appendNewline(); + } + context.send(component); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java new file mode 100644 index 0000000..8bb8471 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertCommand.java @@ -0,0 +1,49 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.hologram.HologramItem; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloInsertCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloInsertCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo insert "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + context.ensureArgsNotEmpty(); + String in = context.dumpAllArgs(); + if (in.toLowerCase().startsWith("item:")) { + if (!HologramItem.ensureValidItemInput(in.substring(5))) { + context.halt(Component.text("The item input is invalid!", NamedTextColor.RED)); + } + } + hologram.insertLine(line, in); + context.send(Component.text("NPC line inserted!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram().getLines().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java new file mode 100644 index 0000000..68273e4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloInsertItemCommand.java @@ -0,0 +1,45 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloInsertItemCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloInsertItemCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo insertitem "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + Player player = context.ensureSenderIsPlayer(); + org.bukkit.inventory.ItemStack itemStack = player.getInventory().getItemInHand(); + if (itemStack == null) context.halt(Component.text("You must be holding an item!", NamedTextColor.RED)); + hologram.insertItemLineStack(line, itemStack); + context.send(Component.text("NPC item line inserted!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram().getLines().size()) + .map(String::valueOf)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java new file mode 100644 index 0000000..21f9163 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloOffsetCommand.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Collections; +import java.util.List; + +public class HoloOffsetCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloOffsetCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo offset "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + double offset = context.parse(Double.class); + hologram.setOffset(offset); + context.send("NPC hologram offset set!"); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + return context.suggestLiteral(String.valueOf(hologram.getOffset())); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java new file mode 100644 index 0000000..bdf3120 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloRefreshDelayCommand.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Collections; +import java.util.List; + +public class HoloRefreshDelayCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloRefreshDelayCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo refreshdelay "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + double delay = context.parse(Double.class); + hologram.setRefreshDelay((long) (delay * 1000)); + context.send("NPC refresh delay set!"); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + return context.suggestLiteral(String.valueOf(((double) hologram.getRefreshDelay()) / 1000)); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java new file mode 100644 index 0000000..7b106b0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetCommand.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloSetCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloSetCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo set "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + context.ensureArgsNotEmpty(); + hologram.removeLine(line); + hologram.insertLine(line, context.dumpAllArgs()); + context.send(Component.text("NPC line set!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() >= 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(hologram.getLines().size()).map(String::valueOf)); + if (context.argSize() == 3) return context.suggestLiteral(hologram.getLine(context.suggestionParse(1, Integer.class))); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java new file mode 100644 index 0000000..1e623ab --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/hologram/HoloSetItemCommand.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.commands.hologram; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class HoloSetItemCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public HoloSetItemCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " holo setitem "); + HologramImpl hologram = context.parse(NpcEntryImpl.class).getNpc().getHologram(); + int line = context.parse(Integer.class); + if (line < 0 || line >= hologram.getLines().size()) context.halt(Component.text("Invalid line number!", NamedTextColor.RED)); + Player player = context.ensureSenderIsPlayer(); + org.bukkit.inventory.ItemStack itemStack = player.getInventory().getItemInHand(); + if (itemStack == null) context.halt(Component.text("You must be holding an item!", NamedTextColor.RED)); + hologram.removeLine(line); + hologram.insertItemLineStack(line, itemStack); + context.send(Component.text("NPC item line set!", NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() >= 2) { + HologramImpl hologram = context.suggestionParse(0, NpcEntryImpl.class).getNpc().getHologram(); + if (context.argSize() == 2) return context.suggestStream(Stream.iterate(0, n -> n + 1) + .limit(hologram.getLines().size()).map(String::valueOf)); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java new file mode 100644 index 0000000..88b6ff1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertyRemoveCommand.java @@ -0,0 +1,43 @@ +package lol.pyr.znpcsplus.commands.property; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class PropertyRemoveCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public PropertyRemoveCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " property remove "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + NpcImpl npc = entry.getNpc(); + EntityPropertyImpl property = context.parse(EntityPropertyImpl.class); + if (!npc.hasProperty(property)) context.halt(Component.text("This npc doesn't have the " + property.getName() + " property set", NamedTextColor.RED)); + if (!property.isPlayerModifiable()) context.halt(Component.text("This property is not modifiable by players", NamedTextColor.RED)); + npc.setProperty(property, null); + context.send(Component.text("Removed property " + property.getName() + " from NPC " + entry.getId(), NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(context.suggestionParse(0, NpcEntryImpl.class) + .getNpc().getAllProperties().stream().filter(EntityProperty::isPlayerModifiable).map(EntityProperty::getName)); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java new file mode 100644 index 0000000..3d9b228 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/property/PropertySetCommand.java @@ -0,0 +1,216 @@ +package lol.pyr.znpcsplus.commands.property; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; +import com.github.retrooper.packetevents.protocol.world.states.type.StateType; +import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.properties.attributes.AttributeProperty; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Color; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PropertySetCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public PropertySetCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " property set "); + NpcEntryImpl entry = context.parse(NpcEntryImpl.class); + NpcImpl npc = entry.getNpc(); + EntityPropertyImpl property = context.parse(EntityPropertyImpl.class); + + // TODO: find a way to do this better & rewrite this mess + + if (!npc.getType().getAllowedProperties().contains(property)) context.halt(Component.text("Property " + property.getName() + " not allowed for npc type " + npc.getType().getName(), NamedTextColor.RED)); + if (!property.isPlayerModifiable()) context.halt(Component.text("This property is not modifiable by players", NamedTextColor.RED)); + Class type = property.getType(); + Object value; + String valueName; + if (type == ItemStack.class) { + org.bukkit.inventory.ItemStack bukkitStack = context.ensureSenderIsPlayer().getInventory().getItemInHand(); + if (bukkitStack.getAmount() == 0) { + value = null; + valueName = "EMPTY"; + } else { + value = SpigotConversionUtil.fromBukkitItemStack(bukkitStack); + valueName = bukkitStack.toString(); + } + } + else if (type == NamedColor.class && context.argSize() < 1 && npc.getProperty(property) != null) { + value = null; + valueName = "NONE"; + } + else if (type == Color.class && context.argSize() < 1 && npc.getProperty(property) != null) { + value = Color.BLACK; + valueName = "NONE"; + } + else if (type == ParrotVariant.class && context.argSize() < 1 && npc.getProperty(property) != null) { + value = null; + valueName = "NONE"; + } + else if (type == BlockState.class) { + String inputType = context.popString().toLowerCase(); + switch (inputType) { + case "hand": + org.bukkit.inventory.ItemStack bukkitStack = context.ensureSenderIsPlayer().getInventory().getItemInHand(); + if (bukkitStack.getAmount() == 0) { + value = new BlockState(0); + valueName = "EMPTY"; + } else { + WrappedBlockState blockState = StateTypes.getByName(bukkitStack.getType().name().toLowerCase()).createBlockState(); +// WrappedBlockState blockState = WrappedBlockState.getByString(bukkitStack.getType().name().toLowerCase()); + value = new BlockState(blockState.getGlobalId()); + valueName = bukkitStack.toString(); + } + break; + case "looking_at": + + // TODO + + value = new BlockState(0); + valueName = "EMPTY"; + break; + case "block": + context.ensureArgsNotEmpty(); + WrappedBlockState blockState = WrappedBlockState.getByString(context.popString()); + value = new BlockState(blockState.getGlobalId()); + valueName = blockState.toString(); + break; + default: + context.send(Component.text("Invalid input type " + inputType + ", must be hand, looking_at, or block", NamedTextColor.RED)); + return; + } + } + else if (type == SpellType.class) { + if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13)) { + value = context.parse(type); + valueName = String.valueOf(value); + if (((SpellType) value).ordinal() > 3) { + context.send(Component.text("Spell type " + valueName + " is not supported on this version", NamedTextColor.RED)); + return; + } + } + else { + value = context.parse(type); + valueName = String.valueOf(value); + } + } + else if (type == NpcEntryImpl.class) { + value = context.parse(type); + valueName = value == null ? "NONE" : ((NpcEntryImpl) value).getId(); + } + else if (type == Vector3i.class) { + value = context.parse(type); + valueName = value == null ? "NONE" : ((Vector3i) value).toPrettyString(); + } + else if (property instanceof AttributeProperty) { + value = context.parse(type); + if ((double) value < ((AttributeProperty) property).getMinValue() || (double) value > ((AttributeProperty) property).getMaxValue()) { + double sanitizedValue = ((AttributeProperty) property).sanitizeValue((double) value); + context.send(Component.text("WARNING: Value " + value + " is out of range for property " + property.getName() + ", setting to " + sanitizedValue, NamedTextColor.YELLOW)); + value = sanitizedValue; + } + valueName = String.valueOf(value); + } + else { + try { + value = context.parse(type); + valueName = String.valueOf(value); + } catch (NullPointerException e) { + context.send(Component.text("An error occurred while trying to parse the value. Please report this to the plugin author.", + NamedTextColor.RED)); + e.printStackTrace(); + return; + } + } + + npc.UNSAFE_setProperty(property, value); + context.send(Component.text("Set property " + property.getName() + " for NPC " + entry.getId() + " to " + valueName, NamedTextColor.GREEN)); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(npcRegistry.getModifiableIds()); + if (context.argSize() == 2) return context.suggestStream(context.suggestionParse(0, NpcEntryImpl.class) + .getNpc().getType().getAllowedProperties().stream().map(EntityProperty::getName)); + if (context.argSize() >= 3) { + EntityPropertyImpl property = context.suggestionParse(1, EntityPropertyImpl.class); + Class type = property.getType(); + if (type == Vector3f.class && context.argSize() <= 5) return context.suggestLiteral("0", "0.0"); + if (context.argSize() == 3) { + if (type == Boolean.class) return context.suggestLiteral("true", "false"); + if (type == NamedColor.class) return context.suggestEnum(NamedColor.values()); + if (type == Color.class) return context.suggestLiteral("0x0F00FF", "#FFFFFF"); + if (type == BlockState.class) return context.suggestLiteral("hand", "looking_at", "block"); + if (type == SpellType.class) return PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13) ? + context.suggestEnum(Arrays.stream(SpellType.values()).filter(spellType -> spellType.ordinal() <= 3).toArray(SpellType[]::new)) : + context.suggestEnum(SpellType.values()); + + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral( + targetBlock.getX() + "", + targetBlock.getX() + " " + targetBlock.getY(), + targetBlock.getX() + " " + targetBlock.getY() + " " + targetBlock.getZ()); + } + } + // Suggest enum values directly + if (type.isEnum()) { + return context.suggestEnum((Enum[]) type.getEnumConstants()); + } + } + else if (context.argSize() == 4) { + if (type == BlockState.class) { + // TODO: suggest block with nbt like minecraft setblock command + return context.suggestionParse(2, String.class).equals("block") ? context.suggestStream(StateTypes.values().stream().map(StateType::getName)) : Collections.emptyList(); + } + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral( + targetBlock.getY() + "", + targetBlock.getY() + " " + targetBlock.getZ()); + } + } + } else if (context.argSize() == 5) { + if (type == Vector3i.class) { + if (context.getSender() instanceof Player) { + Player player = (Player) context.getSender(); + Block targetBlock = player.getTargetBlock(Collections.singleton(Material.AIR), 5); + if (targetBlock.getType().equals(Material.AIR)) return Collections.emptyList(); + return context.suggestLiteral(targetBlock.getZ() + ""); + } + } + } + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java new file mode 100644 index 0000000..c6c598e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/ImportCommand.java @@ -0,0 +1,57 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.conversion.DataImporterRegistry; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class ImportCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + private final DataImporterRegistry importerRegistry; + + public ImportCommand(NpcRegistryImpl npcRegistry, DataImporterRegistry importerRegistry) { + this.npcRegistry = npcRegistry; + this.importerRegistry = importerRegistry; + } + + @SuppressWarnings("ConstantConditions") + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " storage import "); + String id = context.popString().toUpperCase(); + DataImporter importer = importerRegistry.getImporter(id); + if (importer == null) context.halt(Component.text("Importer not found! Possible importers: " + + String.join(", ", importerRegistry.getIds()), NamedTextColor.RED)); + + FutureUtil.exceptionPrintingRunAsync(() -> { + if (!importer.isValid()) { + context.send(Component.text("There is no data to import from this importer!", NamedTextColor.RED)); + return; + } + try { + Collection entries = importer.importData(); + npcRegistry.registerAll(entries); + context.send(Component.text(entries.size() + " npcs have been loaded from " + id, NamedTextColor.GREEN)); + } catch (Exception exception) { + context.send(Component.text("Importing failed! Please check the console for more details.", NamedTextColor.RED)); + exception.printStackTrace(); + } + }); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestCollection(importerRegistry.getIds()); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java new file mode 100644 index 0000000..93cc3de --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/LoadAllCommand.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class LoadAllCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public LoadAllCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + FutureUtil.exceptionPrintingRunAsync(() -> { + npcRegistry.reload(); + context.send(Component.text("All NPCs have been re-loaded from storage", NamedTextColor.GREEN)); + }); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java new file mode 100644 index 0000000..063b98a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/MigrateCommand.java @@ -0,0 +1,179 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class MigrateCommand implements CommandHandler { + private final ConfigManager configManager; + private final ZNpcsPlus plugin; + private final PacketFactory packetFactory; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final NpcStorage currentStorage; + private final NpcStorageType currentStorageType; + private final NpcRegistryImpl npcRegistry; + private final NpcSerializerRegistryImpl serializerRegistry; + + public MigrateCommand(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcStorage currentStorage, NpcStorageType currentStorageType, NpcRegistryImpl npcRegistry, NpcSerializerRegistryImpl serializerRegistry) { + this.configManager = configManager; + this.plugin = plugin; + this.packetFactory = packetFactory; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.currentStorage = currentStorage; + this.currentStorageType = currentStorageType; + this.npcRegistry = npcRegistry; + this.serializerRegistry = serializerRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + context.setUsage(context.getLabel() + " storage migrate [force]"); + NpcStorageType from = context.parse(NpcStorageType.class); + NpcStorageType to = context.parse(NpcStorageType.class); + boolean force = context.argSize() > 2 && context.parse(Boolean.class); + if (from.equals(to)) { + context.halt(Component.text("The storage types must be different.", NamedTextColor.RED)); + return; + } + NpcStorage fromStorage; + if (currentStorageType == from) { + fromStorage = currentStorage; + } else { + fromStorage = from.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + if (fromStorage == null) { + context.halt(Component.text("Failed to initialize the source storage. Please check the console for more information.", NamedTextColor.RED)); + return; + } + } + Collection entries; + try { + entries = fromStorage.loadNpcs(); + } catch (Exception e) { + context.halt(Component.text("Failed to load NPCs from the source storage.", NamedTextColor.RED)); + return; + } + if (entries.isEmpty()) { + context.send(Component.text("No NPCs to migrate.", NamedTextColor.YELLOW)); + return; + } + NpcStorage toStorage; + if (currentStorageType == to) { + toStorage = currentStorage; + } else { + toStorage = to.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + if (toStorage == null) { + context.halt(Component.text("Failed to initialize the destination storage. Please check the console for more information.", NamedTextColor.RED)); + return; + } + } + + Collection existingEntries; + try { + existingEntries = toStorage.loadNpcs(); + } catch (Exception e) { + context.halt(Component.text("Failed to load NPCs from the destination storage.", NamedTextColor.RED)); + return; + } + if (existingEntries.isEmpty()) { + toStorage.saveNpcs(entries); + context.send(Component.text("Migrated " + entries.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + return; + } + if (!force) { + Collection toSave = entries.stream().filter(e -> existingEntries.stream().noneMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList()); + Collection idExists = entries.stream().filter(e -> existingEntries.stream().anyMatch(e2 -> e2.getId().equals(e.getId()))).collect(Collectors.toList()); + if (toSave.isEmpty()) { + context.send(Component.text("No NPCs to migrate.", NamedTextColor.YELLOW)); + if (currentStorageType != to) { + toStorage.close(); + } + } else { + toStorage.saveNpcs(toSave); + context.send(Component.text("Migrated " + toSave.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + } + if (!idExists.isEmpty()) { + AtomicReference component = new AtomicReference<>(Component.text("The following NPCs were not migrated because their IDs already exist in the destination storage:").color(NamedTextColor.YELLOW)); + idExists.forEach(e -> { + component.set(component.get().append(Component.newline()).append(Component.text(e.getId(), NamedTextColor.RED))); + }); + component.set(component.get().append(Component.newline()) + .append(Component.text("Use the ", NamedTextColor.YELLOW)) + .append(Component.text("force", NamedTextColor.GOLD)) + .append(Component.text(" argument to overwrite them.", NamedTextColor.YELLOW))); + context.send(component.get()); + } + } else { + toStorage.saveNpcs(entries); + context.send(Component.text("Force migrated " + entries.size() + " NPCs from the source storage (", NamedTextColor.GREEN) + .append(Component.text(from.name(), NamedTextColor.GOLD)) + .append(Component.text(") to the destination storage (", NamedTextColor.GREEN)) + .append(Component.text(to.name(), NamedTextColor.GOLD)) + .append(Component.text(").", NamedTextColor.GREEN))); + if (currentStorageType == to) { + npcRegistry.reload(); + } else { + toStorage.close(); + } + } + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) { + return context.suggestEnum(NpcStorageType.values()); + } else if (context.argSize() == 2) { + NpcStorageType from = context.suggestionParse(0, NpcStorageType.class); + if (from == null) return Collections.emptyList(); + return context.suggestCollection(Arrays.stream(NpcStorageType.values()) + .filter(t -> t != from).map(Enum::name).collect(Collectors.toList())); + } else if (context.argSize() == 3) { + return context.suggestLiteral("true"); + } + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java new file mode 100644 index 0000000..4445cf1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/commands/storage/SaveAllCommand.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.commands.storage; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Collections; +import java.util.List; + +public class SaveAllCommand implements CommandHandler { + private final NpcRegistryImpl npcRegistry; + + public SaveAllCommand(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run(CommandContext context) throws CommandExecutionException { + FutureUtil.exceptionPrintingRunAsync(() -> { + npcRegistry.save(); + context.send(Component.text("All NPCs have been saved to storage", NamedTextColor.GREEN)); + }); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java new file mode 100644 index 0000000..a63beef --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/ComponentSerializer.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.config; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import space.arim.dazzleconf.error.BadValueException; +import space.arim.dazzleconf.serialiser.Decomposer; +import space.arim.dazzleconf.serialiser.FlexibleType; +import space.arim.dazzleconf.serialiser.ValueSerialiser; + +public class ComponentSerializer implements ValueSerialiser { + @Override + public Class getTargetClass() { + return Component.class; + } + + @Override + public Component deserialise(FlexibleType flexibleType) throws BadValueException { + return MiniMessage.miniMessage().deserialize(flexibleType.getString()); + } + + @Override + public Object serialise(Component value, Decomposer decomposer) { + return MiniMessage.miniMessage().serialize(value); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java new file mode 100644 index 0000000..30f35c5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/ConfigManager.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.config; + +import space.arim.dazzleconf.ConfigurationFactory; +import space.arim.dazzleconf.ConfigurationOptions; +import space.arim.dazzleconf.error.ConfigFormatSyntaxException; +import space.arim.dazzleconf.error.InvalidConfigException; +import space.arim.dazzleconf.ext.snakeyaml.CommentMode; +import space.arim.dazzleconf.ext.snakeyaml.SnakeYamlConfigurationFactory; +import space.arim.dazzleconf.ext.snakeyaml.SnakeYamlOptions; +import space.arim.dazzleconf.helper.ConfigurationHelper; +import space.arim.dazzleconf.serialiser.ValueSerialiser; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +public class ConfigManager { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Configuration Manager"); + + private volatile MainConfig config; + private final ConfigurationHelper configHelper; + + private volatile MessageConfig messages; + private final ConfigurationHelper messagesHelper; + + public ConfigManager(File pluginFolder) { + configHelper = createHelper(MainConfig.class, new File(pluginFolder, "config.yaml")); + messagesHelper = createHelper(MessageConfig.class, new File(pluginFolder, "messages.yaml"), new ComponentSerializer()); + reload(); + } + + private static ConfigurationHelper createHelper(Class configClass, File file, ValueSerialiser... serialisers) { + SnakeYamlOptions yamlOptions = new SnakeYamlOptions.Builder().commentMode(CommentMode.fullComments()).build(); + ConfigurationOptions.Builder optionBuilder = new ConfigurationOptions.Builder(); + if (serialisers != null && serialisers.length > 0) optionBuilder.addSerialisers(serialisers); + ConfigurationFactory configFactory = SnakeYamlConfigurationFactory.create(configClass, optionBuilder.build(), yamlOptions); + return new ConfigurationHelper<>(file.getParentFile().toPath(), file.getName(), configFactory); + } + + public void reload() { + try { + config = configHelper.reloadConfigData(); + messages = messagesHelper.reloadConfigData(); + } catch (IOException e) { + logger.severe("Couldn't open config file!"); + e.printStackTrace(); + } catch (ConfigFormatSyntaxException e) { + logger.severe("Invalid config syntax!"); + e.printStackTrace(); + } catch (InvalidConfigException e) { + logger.severe("Invalid config value!"); + e.printStackTrace(); + } + } + + public MainConfig getConfig() { + return config; + } + + public MessageConfig getMessages() { + return messages; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java new file mode 100644 index 0000000..a62e983 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/DatabaseConfig.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.config; + +import space.arim.dazzleconf.annote.ConfComments; +import space.arim.dazzleconf.annote.ConfDefault.*; +import space.arim.dazzleconf.annote.ConfKey; + +public interface DatabaseConfig { + @ConfKey("host") + @ConfComments("The host of the database") + @DefaultString("localhost") + String host(); + + @ConfKey("port") + @ConfComments("The port of the database") + @DefaultInteger(3306) + int port(); + + @ConfKey("username") + @ConfComments("The username to use to connect to the database") + @DefaultString("znpcsplus") + String username(); + + @ConfKey("password") + @ConfComments("The password to use to connect to the database") + @DefaultString("password") + String password(); + + @ConfKey("database-name") + @ConfComments("The name of the database to use") + @DefaultString("znpcsplus") + String databaseName(); + + @ConfKey("use-ssl") + @ConfComments("Should SSL be used when connecting to the database?") + @DefaultBoolean(false) + boolean useSSL(); + + default String createConnectionURL(String dbType) { + if (dbType.equalsIgnoreCase("mysql")) { + return "jdbc:mysql://" + host() + ":" + port() + "/" + databaseName() + "?useSSL=" + useSSL(); + } else { + throw new IllegalArgumentException("Unsupported database type: " + dbType); + } + } +} + diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java new file mode 100644 index 0000000..fee1ea7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/MainConfig.java @@ -0,0 +1,81 @@ +package lol.pyr.znpcsplus.config; + +import lol.pyr.znpcsplus.storage.NpcStorageType; +import space.arim.dazzleconf.annote.ConfComments; +import space.arim.dazzleconf.annote.ConfKey; +import space.arim.dazzleconf.annote.SubSection; + +import static space.arim.dazzleconf.annote.ConfDefault.*; + +public interface MainConfig { + @ConfKey("view-distance") + @ConfComments("How far away do you need to be from any NPC for it to disappear, measured in blocks") + @DefaultInteger(32) + int viewDistance(); + + @ConfKey("line-spacing") + @ConfComments("The height between hologram lines, measured in blocks") + @DefaultDouble(0.3D) + double lineSpacing(); + + @ConfKey("check-for-updates") + @ConfComments("Should the plugin check for available updates and notify admins about them?") + @DefaultBoolean(true) + boolean checkForUpdates(); + + @ConfKey("debug-enabled") + @ConfComments({ + "Should debug mode be enabled?", + "This is used in development to test various things, you probably don't want to enable this" + }) + @DefaultBoolean(false) + boolean debugEnabled(); + + @ConfKey("storage-type") + @ConfComments("The storage type to use. Available storage types: YAML, SQLITE, MYSQL") + @DefaultString("YAML") + NpcStorageType storageType(); + + @ConfKey("database-config") + @ConfComments("The database config. Only used if storage-type is MYSQL") + @SubSection + DatabaseConfig databaseConfig(); + + @ConfKey("disable-skin-fetcher-warnings") + @ConfComments("Set this to true if you don't want to be warned in the console when a skin fails to resolve") + @DefaultBoolean(false) + boolean disableSkinFetcherWarnings(); + + @ConfKey("auto-save-interval") + @ConfComments("How often to auto-save npcs, set this to -1 to disable. This value will only apply on restart") + @DefaultInteger(300) + int autoSaveInterval(); + + default boolean autoSaveEnabled() { + return autoSaveInterval() != -1; + } + + @ConfKey("look-property-distance") + @ConfComments("How far should the look property work from in blocks") + @DefaultDouble(10) + double lookPropertyDistance(); + + @ConfKey("tab-hide-delay") + @ConfComments({ + "The amount of time to wait before removing the npc from the player list (aka tab) in ticks", + "If you're on 1.19.2 or above changing this value will have almost no effect since npcs are hidden in tab", + "WARNING: Setting this value too low may cause issues with player npcs spawning" + }) + @DefaultInteger(60) + int tabHideDelay(); + + @ConfKey("tab-display-name") + @ConfComments("The display name to use for npcs in the player list (aka tab)") + @DefaultString("ZNPC[{id}]") + String tabDisplayName(); + + @ConfKey("fake-enforce-secure-chat") + @ConfComments("Should the plugin fake the enforce secure chat packet to hide the popup?") + @DefaultBoolean(false) + boolean fakeEnforceSecureChat(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java b/plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java new file mode 100644 index 0000000..666a59e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/config/MessageConfig.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.config; + +/** + * (OLD CONFIGURATION) + * NO_PERMISSION("messages", "&cYou do not have permission to execute this command.", String.class), + * SUCCESS("messages", "&aSuccess!", String.class), + * INCORRECT_USAGE("messages", "&cThe arguments you specified are invalid. Type &f/znpcs&c for examples.", String.class), + * COMMAND_NOT_FOUND("messages", "&cThe command you specified does not exist!", String.class), + * COMMAND_ERROR("messages", "&cAn error occurred when executing this command. See console for more information.", String.class), + * INVALID_NUMBER("messages", "&cThe ID you have specified is invalid. Please use positive integers only!", String.class), + * NPC_NOT_FOUND("messages", "&cNo NPCs could be found with this ID!", String.class), + * TOO_FEW_ARGUMENTS("messages", "&cThis command does not contain enough arguments. Type &f/znpcs&c or view our documentation for a list/examples of existing arguments.", String.class), + * PATH_START("messages", "&aSuccess! Move to create a path for your NPC. When finished, type &f/znpcs path exit&c to exit path creation.", String.class), + * EXIT_PATH("messages", "&cYou have exited path creation.", String.class), + * PATH_FOUND("messages", "&cThere is already a path with this getName.", String.class), + * NPC_FOUND("messages", "&cThere is already an NPC with this ID.", String.class), + * NO_PATH_FOUND("messages", "&cThe path you have specified does not exist.", String.class), + * NO_SKIN_FOUND("messages", "&cThe skin username/URL you have specified does not exist or is invalid.", String.class), + * NO_NPC_FOUND("messages", "&cThe NPC you have specified does not exist.", String.class), + * NO_ACTION_FOUND("messages", "&cThis action does not exist! Type &f/znpcs&c or view our documentation for a list/examples of existing action types.", String.class), + * METHOD_NOT_FOUND("messages", "&cThis method does not exist! Type &f/znpcs&c or view our documentation for a list/examples of existing methods.", String.class), + * INVALID_NAME_LENGTH("messages", "&cThe getName you specified either too short or long. Please enter a positive integer of (3 to 16) characters.", String.class), + * UNSUPPORTED_ENTITY("messages", "&cThis entity type not available in your current server version.", String.class), + * PATH_SET_INCORRECT_USAGE("messages", "&eUsage: &aset ", String.class), + * ACTION_ADD_INCORRECT_USAGE("messages", "&eUsage: &a ", String.class), + * ACTION_DELAY_INCORRECT_USAGE("messages", "&eUsage: &a ", String.class), + * CONVERSATION_SET_INCORRECT_USAGE("messages", "&cUsage: ", String.class), + * NO_CONVERSATION_FOUND("messages", "&cThe conversation you have specified does not exist!", String.class), + * CONVERSATION_FOUND("messages", "&cThere is already a conversation with this getName.", String.class), + * INVALID_SIZE("messages", "&cThe position you have specified cannot exceed the limit.", String.class), + * FETCHING_SKIN("messages", "&aFetching skin for getName: &f%s&a. Please wait...", String.class), + * CANT_GET_SKIN("messages", "&cCould not fetch skin for getName: %s.", String.class), + * GET_SKIN("messages", "&aSkin successfully fetched!", String.class), + * NOT_SUPPORTED_NPC_TYPE("messages", "&cThis NPC type doesn't exists or is not supported in your current server version.", String.class); + */ + +public interface MessageConfig { +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java new file mode 100644 index 0000000..3f932cc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporter.java @@ -0,0 +1,10 @@ +package lol.pyr.znpcsplus.conversion; + +import lol.pyr.znpcsplus.npc.NpcEntryImpl; + +import java.util.Collection; + +public interface DataImporter { + Collection importData(); + boolean isValid(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java new file mode 100644 index 0000000..1185eb4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/DataImporterRegistry.java @@ -0,0 +1,54 @@ +package lol.pyr.znpcsplus.conversion; + +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.citizens.CitizensImporter; +import lol.pyr.znpcsplus.conversion.fancynpcs.FancyNpcsImporter; +import lol.pyr.znpcsplus.conversion.znpcs.ZNpcImporter; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.BungeeConnector; +import lol.pyr.znpcsplus.util.LazyLoader; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class DataImporterRegistry { + private final Map> importers = new HashMap<>(); + + public DataImporterRegistry(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, File pluginsFolder, EntityPropertyRegistryImpl propertyRegistry, + MojangSkinCache skinCache, NpcRegistryImpl npcRegistry, BungeeConnector bungeeConnector) { + + register("znpcs", LazyLoader.of(() -> new ZNpcImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "ServersNPC/data.json"), bungeeConnector))); + register("znpcsplus_legacy", LazyLoader.of(() -> new ZNpcImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "ZNPCsPlusLegacy/data.json"), bungeeConnector))); + register("citizens", LazyLoader.of(() -> new CitizensImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "Citizens/saves.yml"), npcRegistry))); + register("fancynpcs", LazyLoader.of(() -> new FancyNpcsImporter(configManager, adventure, taskScheduler, + packetFactory, textSerializer, typeRegistry, propertyRegistry, skinCache, new File(pluginsFolder, "FancyNpcs/npcs.yml"), npcRegistry))); + } + + private void register(String id, LazyLoader loader) { + importers.put(id.toLowerCase(), loader); + } + + public DataImporter getImporter(String id) { + id = id.toLowerCase(); + return importers.containsKey(id) ? importers.get(id).get() : null; + } + + public Collection getIds() { + return Collections.unmodifiableSet(importers.keySet()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java new file mode 100644 index 0000000..aecfb93 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/CitizensImporter.java @@ -0,0 +1,118 @@ +package lol.pyr.znpcsplus.conversion.citizens; + +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.conversion.citizens.model.CitizensTrait; +import lol.pyr.znpcsplus.conversion.citizens.model.CitizensTraitsRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.traits.TypeTrait; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +@SuppressWarnings("FieldCanBeLocal") +public class CitizensImporter implements DataImporter { + private final ConfigManager configManager; + private final BukkitAudiences adventure; + private final TaskScheduler scheduler; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final MojangSkinCache skinCache; + private final File dataFile; + private final CitizensTraitsRegistry traitsRegistry; + private final NpcRegistryImpl npcRegistry; + + public CitizensImporter(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, MojangSkinCache skinCache, + File dataFile, NpcRegistryImpl npcRegistry) { + this.configManager = configManager; + this.adventure = adventure; + this.scheduler = taskScheduler; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.skinCache = skinCache; + this.dataFile = dataFile; + this.traitsRegistry = new CitizensTraitsRegistry(propertyRegistry, skinCache, taskScheduler, textSerializer); + this.npcRegistry = npcRegistry; + } + + @Override + public Collection importData() { + YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile); + ConfigurationSection npcsSection = config.getConfigurationSection("npc"); + if (npcsSection == null) { + return Collections.emptyList(); + } + ArrayList entries = new ArrayList<>(); + npcsSection.getKeys(false).forEach(key -> { + ConfigurationSection npcSection = npcsSection.getConfigurationSection(key); + if (npcSection == null) { + return; + } + String name = npcSection.getString("name", "Citizens NPC"); + UUID uuid; + try { + uuid = UUID.fromString(npcSection.getString("uuid")); + } catch (IllegalArgumentException e) { + uuid = UUID.randomUUID(); + } + String world = npcSection.getString("traits.location.world"); + if (world == null) { + world = Bukkit.getWorlds().get(0).getName(); + } + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, world, typeRegistry.getByName("armor_stand"), new NpcLocation(0, 0, 0, 0, 0)); + + ConfigurationSection traits = npcSection.getConfigurationSection("traits"); + if (traits != null) { + TypeTrait typeTrait = new TypeTrait(typeRegistry); + npc = typeTrait.apply(npc, traits.getString("type")); + npc.getType().applyDefaultProperties(npc); + for (String traitName : traits.getKeys(false)) { + Object trait = traits.get(traitName); + CitizensTrait citizensTrait = traitsRegistry.getByName(traitName); + if (citizensTrait != null) { + npc = citizensTrait.apply(npc, trait); + } + } + } + boolean nameVisible = Boolean.parseBoolean(npcSection.getString("metadata.name-visible", "true")); + if (nameVisible) { + npc.getHologram().addTextLineComponent(textSerializer.deserialize(name)); + } + String id = key.toLowerCase(); + while (npcRegistry.getById(id) != null) { + id += "_"; // TODO: make a backup of the old npc instead + } + NpcEntryImpl entry = new NpcEntryImpl(id, npc); + entry.enableEverything(); + entries.add(entry); + }); + return entries; + } + + @Override + public boolean isValid() { + return dataFile.isFile(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java new file mode 100644 index 0000000..0811589 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTrait.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.jetbrains.annotations.NotNull; + +public abstract class CitizensTrait { + private final String identifier; + + public CitizensTrait(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + public abstract @NotNull NpcImpl apply(NpcImpl npc, Object value); + +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java new file mode 100644 index 0000000..6b004e1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/CitizensTraitsRegistry.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.traits.*; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.HashMap; + +public class CitizensTraitsRegistry { + private final HashMap traitMap = new HashMap<>(); + + public CitizensTraitsRegistry(EntityPropertyRegistry propertyRegistry, MojangSkinCache skinCache, TaskScheduler taskScheduler, LegacyComponentSerializer textSerializer) { + register(new LocationTrait()); + register(new ProfessionTrait(propertyRegistry)); + register(new VillagerTrait(propertyRegistry)); + register(new SkinTrait(propertyRegistry)); + register(new MirrorTrait(propertyRegistry, skinCache)); + register(new SkinLayersTrait(propertyRegistry)); + register(new LookTrait(propertyRegistry)); + register(new CommandTrait(taskScheduler)); + register(new HologramTrait(textSerializer)); + register(new EquipmentTrait(propertyRegistry)); + register(new SpawnedTrait()); + } + + public CitizensTrait getByName(String name) { + return traitMap.get(name); + } + + public void register(CitizensTrait trait) { + traitMap.put(trait.getIdentifier(), trait); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java new file mode 100644 index 0000000..123eca5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/SectionCitizensTrait.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public abstract class SectionCitizensTrait extends CitizensTrait { + public SectionCitizensTrait(String identifier) { + super(identifier); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, Object value) { + if (!(value instanceof ConfigurationSection)) return npc; + return apply(npc, (ConfigurationSection) value); + } + + public abstract @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java new file mode 100644 index 0000000..58199a4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/StringCitizensTrait.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.conversion.citizens.model; + +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.jetbrains.annotations.NotNull; + +public abstract class StringCitizensTrait extends CitizensTrait { + public StringCitizensTrait(String identifier) { + super(identifier); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, Object value) { + if (!(value instanceof String)) return npc; + return apply(npc, (String) value); + } + + public abstract @NotNull NpcImpl apply(NpcImpl npc, String string); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java new file mode 100644 index 0000000..e6629f8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/CommandTrait.java @@ -0,0 +1,69 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public class CommandTrait extends SectionCitizensTrait { + private final TaskScheduler scheduler; + + public CommandTrait(TaskScheduler scheduler) { + super("commandtrait"); + this.scheduler = scheduler; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + ConfigurationSection commands = section.getConfigurationSection("commands"); + if (commands != null) { + Set keys = commands.getKeys(false); + if (keys != null) { + for (String key : keys) { + ConfigurationSection commandSection = commands.getConfigurationSection(key); + String command = commandSection.getString("command"); + String hand = commandSection.getString("hand", "BOTH"); + InteractionType clickType = wrapClickType(hand); + boolean isPlayerCommand = commandSection.getBoolean("player", true); + int cooldown = commandSection.getInt("cooldown", 0); + int delay = commandSection.getInt("delay", 0); + if (command != null) { + InteractionAction action; + if (isPlayerCommand) { + action = new PlayerCommandAction(scheduler, command, clickType, cooldown, delay); + } else { + action = new ConsoleCommandAction(scheduler, command, clickType, cooldown, delay); + } + npc.addAction(action); + } + } + } + } + return npc; + } + + private InteractionType wrapClickType(String hand) { + if (hand == null) { + return InteractionType.ANY_CLICK; + } + switch (hand) { + case "RIGHT": + case "SHIFT_RIGHT": + return InteractionType.RIGHT_CLICK; + case "LEFT": + case "SHIFT_LEFT": + return InteractionType.LEFT_CLICK; + case "BOTH": + return InteractionType.ANY_CLICK; + } + throw new IllegalStateException(); + } + +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java new file mode 100644 index 0000000..1f3393a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/EquipmentTrait.java @@ -0,0 +1,115 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.google.common.io.BaseEncoding; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class EquipmentTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry propertyRegistry; + private final HashMap EQUIPMENT_SLOT_MAP = new HashMap<>(); + + public EquipmentTrait(EntityPropertyRegistry propertyRegistry) { + super("equipment"); + this.propertyRegistry = propertyRegistry; + EQUIPMENT_SLOT_MAP.put("hand", EquipmentSlot.MAIN_HAND); + EQUIPMENT_SLOT_MAP.put("offhand", EquipmentSlot.OFF_HAND); + EQUIPMENT_SLOT_MAP.put("helmet", EquipmentSlot.HELMET); + EQUIPMENT_SLOT_MAP.put("chestplate", EquipmentSlot.CHEST_PLATE); + EQUIPMENT_SLOT_MAP.put("leggings", EquipmentSlot.LEGGINGS); + EQUIPMENT_SLOT_MAP.put("boots", EquipmentSlot.BOOTS); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + for (String key : section.getKeys(false)) { + EquipmentSlot slot = EQUIPMENT_SLOT_MAP.get(key); + if (slot == null) { + continue; + } + + ItemStack itemStack = parseItemStack(section.getConfigurationSection(key)); + if (itemStack == null) { + continue; + } + + EntityProperty property = propertyRegistry.getByName(key, ItemStack.class); + npc.setProperty(property, itemStack); + } + + return npc; + } + + private ItemStack parseItemStack(ConfigurationSection section) { + Material material = null; + if (section.isString("type_key")) { + material = Material.getMaterial(section.getString("type_key").toUpperCase()); + } else if (section.isString("type")) { + material = Material.matchMaterial(section.getString("type").toUpperCase()); + } else if (section.isString("id")) { + material = Material.matchMaterial(section.getString("id").toUpperCase()); + } + if (material == null || material == Material.AIR) { + return null; + } + org.bukkit.inventory.ItemStack itemStack = new org.bukkit.inventory.ItemStack(material, section.getInt("amount", 1), + (short) section.getInt("durability", section.getInt("data", 0))); + if (section.isInt("mdata")) { + //noinspection deprecation + itemStack.getData().setData((byte) section.getInt("mdata")); + } + if (section.isConfigurationSection("enchantments")) { + ConfigurationSection enchantments = section.getConfigurationSection("enchantments"); + itemStack.addUnsafeEnchantments(deserializeEnchantments(enchantments)); + } + if (section.isConfigurationSection("meta")) { + ItemMeta itemMeta = deserializeMeta(section.getConfigurationSection("meta")); + if (itemMeta != null) { + itemStack.setItemMeta(itemMeta); + } + } + return SpigotConversionUtil.fromBukkitItemStack(itemStack); + } + + private Map deserializeEnchantments(ConfigurationSection section) { + Map enchantments = new HashMap<>(); + for (String key : section.getKeys(false)) { + Enchantment enchantment = Enchantment.getByName(key); + if (enchantment == null) { + continue; + } + enchantments.put(enchantment, section.getInt(key)); + } + return enchantments; + } + + private ItemMeta deserializeMeta(ConfigurationSection section) { + if (section.isString("encoded-meta")) { + byte[] raw = BaseEncoding.base64().decode(section.getString("encoded-meta")); + try { + BukkitObjectInputStream inp = new BukkitObjectInputStream(new ByteArrayInputStream(raw)); + ItemMeta meta = (ItemMeta) inp.readObject(); + inp.close(); + return meta; + } catch (IOException | ClassNotFoundException e1) { + e1.printStackTrace(); + } + } + return null; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java new file mode 100644 index 0000000..48c850d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/HologramTrait.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class HologramTrait extends SectionCitizensTrait { + private final LegacyComponentSerializer textSerializer; + + public HologramTrait(LegacyComponentSerializer textSerializer) { + super("hologramtrait"); + this.textSerializer = textSerializer; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + ConfigurationSection linesSection = section.getConfigurationSection("lines"); + if (linesSection != null) { + List keys = new ArrayList<>(linesSection.getKeys(false)); + for (int i = keys.size() - 1; i >= 0; i--) { + String line = linesSection.isConfigurationSection(keys.get(i)) ? linesSection.getConfigurationSection(keys.get(i)).getString("text") : linesSection.getString(keys.get(i)); + if (line != null) { + Component component = textSerializer.deserialize(line); + npc.getHologram().addTextLineComponent(component); + } + } + } + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java new file mode 100644 index 0000000..c56793c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LocationTrait.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class LocationTrait extends SectionCitizensTrait { + public LocationTrait() { + super("location"); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + double x = Double.parseDouble(section.getString("x")); + double y = Double.parseDouble(section.getString("y")); + double z = Double.parseDouble(section.getString("z")); + float yaw = Float.parseFloat(section.getString("yaw")); + float pitch = Float.parseFloat(section.getString("pitch")); + NpcLocation location = new NpcLocation(x, y, z, yaw, pitch); + npc.setLocation(location); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java new file mode 100644 index 0000000..650b6a6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/LookTrait.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.LookType; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class LookTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + + public LookTrait(EntityPropertyRegistry registry) { + super("lookclose"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + if (section.getBoolean("enabled")) npc.setProperty(registry.getByName("look", LookType.class), LookType.CLOSEST_PLAYER); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java new file mode 100644 index 0000000..19360bf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/MirrorTrait.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class MirrorTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + private final MojangSkinCache skinCache; + + public MirrorTrait(EntityPropertyRegistry registry, MojangSkinCache skinCache) { + super("mirrortrait"); + this.registry = registry; + this.skinCache = skinCache; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + if (section.getBoolean("enabled")) npc.setProperty(registry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java new file mode 100644 index 0000000..551d9a8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/ProfessionTrait.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.StringCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.VillagerProfession; +import org.jetbrains.annotations.NotNull; + +public class ProfessionTrait extends StringCitizensTrait { + private final EntityPropertyRegistry registry; + + public ProfessionTrait(EntityPropertyRegistry registry) { + super("profession"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, String string) { + VillagerProfession profession; + try { + profession = VillagerProfession.valueOf(string.toUpperCase()); + } catch (IllegalArgumentException ignored) { + profession = VillagerProfession.NONE; + } + npc.setProperty(registry.getByName("villager_profession", VillagerProfession.class), profession); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java new file mode 100644 index 0000000..f2d7ed0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinLayersTrait.java @@ -0,0 +1,38 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class SkinLayersTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + private final Map skinLayers; + + public SkinLayersTrait(EntityPropertyRegistry registry) { + super("skinlayers"); + this.registry = registry; + this.skinLayers = new HashMap<>(); + this.skinLayers.put("cape", "skin_cape"); + this.skinLayers.put("hat", "skin_hat"); + this.skinLayers.put("jacket", "skin_jacket"); + this.skinLayers.put("left_sleeve", "skin_left_sleeve"); + this.skinLayers.put("left_pants", "skin_left_leg"); + this.skinLayers.put("right_sleeve", "skin_right_sleeve"); + this.skinLayers.put("right_pants", "skin_right_leg"); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + for (Map.Entry entry : this.skinLayers.entrySet()) { + String key = entry.getKey(); + String property = entry.getValue(); + if (section.contains(key)) npc.setProperty(registry.getByName(property, Boolean.class), section.getBoolean(key)); + } + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java new file mode 100644 index 0000000..d21ab4e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SkinTrait.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class SkinTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + + public SkinTrait(EntityPropertyRegistry registry) { + super("skintrait"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + String texture = section.getString("textureRaw"); + String signature = section.getString("signature"); + if (texture != null && signature != null) npc.setProperty(registry.getByName("skin", SkinDescriptor.class), new PrefetchedDescriptor(new SkinImpl(texture, signature))); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java new file mode 100644 index 0000000..9f080c5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/SpawnedTrait.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.conversion.citizens.model.CitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import org.jetbrains.annotations.NotNull; + +public class SpawnedTrait extends CitizensTrait { + + public SpawnedTrait() { + super("spawned"); + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, Object value) { + if (value != null) { + npc.setEnabled((boolean) value); + } + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java new file mode 100644 index 0000000..6af1d85 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/TypeTrait.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.StringCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeImpl; +import org.jetbrains.annotations.NotNull; + +public class TypeTrait extends StringCitizensTrait { + private final NpcTypeRegistry registry; + + public TypeTrait(NpcTypeRegistry registry) { + super("type"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, String string) { + NpcTypeImpl type = warpNpcType(string); + if (type == null) return npc; + npc.setType(type); + return npc; + } + + private NpcTypeImpl warpNpcType(String name) { + name = name.toLowerCase(); +// if (name.equals("player")) name = "human"; +// else if (name.equals("zombievillager")) name = "zombie_villager"; + return (NpcTypeImpl) registry.getByName(name); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java new file mode 100644 index 0000000..4998f78 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/citizens/model/traits/VillagerTrait.java @@ -0,0 +1,39 @@ +package lol.pyr.znpcsplus.conversion.citizens.model.traits; + +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.conversion.citizens.model.SectionCitizensTrait; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.util.VillagerLevel; +import lol.pyr.znpcsplus.util.VillagerType; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +public class VillagerTrait extends SectionCitizensTrait { + private final EntityPropertyRegistry registry; + + public VillagerTrait(EntityPropertyRegistry registry) { + super("villagertrait"); + this.registry = registry; + } + + @Override + public @NotNull NpcImpl apply(NpcImpl npc, ConfigurationSection section) { + int level = section.getInt("level"); + String type = section.getString("type", "plains"); + VillagerLevel villagerLevel; + try { + villagerLevel = VillagerLevel.values()[level]; + } catch (ArrayIndexOutOfBoundsException ignored) { + villagerLevel = VillagerLevel.STONE; + } + VillagerType villagerType; + try { + villagerType = VillagerType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException ignored) { + villagerType = VillagerType.PLAINS; + } + npc.setProperty(registry.getByName("villager_level", VillagerLevel.class), villagerLevel); + npc.setProperty(registry.getByName("villager_type", VillagerType.class), villagerType); + return npc; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java new file mode 100644 index 0000000..b5c981a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/fancynpcs/FancyNpcsImporter.java @@ -0,0 +1,183 @@ +package lol.pyr.znpcsplus.conversion.fancynpcs; + +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.message.MessageAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.npc.*; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.util.LookType; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +import java.io.File; +import java.util.*; + +public class FancyNpcsImporter implements DataImporter { + private final ConfigManager configManager; + private final BukkitAudiences adventure; + private final TaskScheduler scheduler; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final MojangSkinCache skinCache; + private final File dataFile; + private final NpcRegistryImpl npcRegistry; + + public FancyNpcsImporter(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, MojangSkinCache skinCache, + File dataFile, NpcRegistryImpl npcRegistry) { + this.configManager = configManager; + this.adventure = adventure; + this.scheduler = taskScheduler; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.skinCache = skinCache; + this.dataFile = dataFile; + this.npcRegistry = npcRegistry; + } + + @Override + public Collection importData() { + YamlConfiguration config = YamlConfiguration.loadConfiguration(dataFile); + ConfigurationSection npcsSection = config.getConfigurationSection("npcs"); + if (npcsSection == null) { + return Collections.emptyList(); + } + ArrayList entries = new ArrayList<>(); + npcsSection.getKeys(false).forEach(key -> { + ConfigurationSection npcSection = npcsSection.getConfigurationSection(key); + if (npcSection == null) { + return; + } + String name = npcSection.getString("name", "FancyNPC"); + UUID uuid = UUID.fromString(key); + String world = npcSection.getString("location.world"); + if (world == null) { + world = Bukkit.getWorlds().get(0).getName(); + } + NpcLocation location = new NpcLocation( + npcSection.getDouble("location.x"), + npcSection.getDouble("location.y"), + npcSection.getDouble("location.z"), + (float) npcSection.getDouble("location.yaw"), + (float) npcSection.getDouble("location.pitch") + ); + String typeString = npcSection.getString("type"); + NpcTypeImpl type = typeRegistry.getByName(typeString); + if (type == null) { + type = typeRegistry.getByName("player"); + } + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, world, type, location); + npc.getType().applyDefaultProperties(npc); + + npc.getHologram().addTextLineComponent(textSerializer.deserialize(name)); + boolean glowing = npcSection.getBoolean("glowing", false); + if (glowing) { + NamedColor color; + try { + color = NamedColor.valueOf(npcSection.getString("glowingColor", "white")); + } catch (IllegalArgumentException ignored) { + color = NamedColor.WHITE; + } + EntityPropertyImpl property = propertyRegistry.getByName("glow", NamedColor.class); + npc.setProperty(property, color); + } + if (npcSection.getBoolean("turnToPlayer", false)) { + EntityPropertyImpl property = propertyRegistry.getByName("look", LookType.class); + npc.setProperty(property, LookType.CLOSEST_PLAYER); + } + if (npcSection.isConfigurationSection("skin")) { + ConfigurationSection skinSection = npcSection.getConfigurationSection("skin"); + String texture = skinSection.getString("value"); + String signature = skinSection.getString("signature"); + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new PrefetchedDescriptor(new SkinImpl(texture, signature))); + } + if (npcSection.isConfigurationSection("equipment")) { + ConfigurationSection equipmentSection = npcSection.getConfigurationSection("equipment"); + for (String slot : equipmentSection.getKeys(false)) { + ItemStack item = equipmentSection.getItemStack(slot); + if (item != null) { + npc.setProperty(propertyRegistry.getByName(getEquipmentPropertyName(slot), + com.github.retrooper.packetevents.protocol.item.ItemStack.class), SpigotConversionUtil.fromBukkitItemStack(item)); + } + } + } + if (npcSection.getBoolean("mirrorSkin")) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + } + List playerCommands = npcSection.getStringList("playerCommands"); + if (!playerCommands.isEmpty()) { + long cooldown = npcSection.getLong("interactionCooldown", 0); + for (String command : playerCommands) { + npc.addAction(new PlayerCommandAction(scheduler, command, InteractionType.ANY_CLICK, cooldown, 0)); + } + } + String serverCommand = npcSection.getString("serverCommand"); + if (serverCommand != null) { + long cooldown = npcSection.getLong("interactionCooldown", 0); + npc.addAction(new ConsoleCommandAction(scheduler, serverCommand, InteractionType.ANY_CLICK, cooldown, 0)); + } + List messages = npcSection.getStringList("messages"); + if (!messages.isEmpty()) { + long cooldown = npcSection.getLong("interactionCooldown", 0); + for (String message : messages) { + npc.addAction(new MessageAction(adventure, textSerializer, message, InteractionType.ANY_CLICK, cooldown, 0)); + } + } + String id = npcSection.getString("name"); + while (npcRegistry.getById(id) != null) { + id += "_"; + } + NpcEntryImpl entry = new NpcEntryImpl(id, npc); + entry.enableEverything(); + entries.add(entry); + }); + return entries; + } + + private String getEquipmentPropertyName(String slot) { + switch (slot) { + case "MAINHAND": + return "hand"; + case "OFFHAND": + return "offhand"; + case "FEET": + return "boots"; + case "LEGS": + return "leggings"; + case "CHEST": + return "chestplate"; + case "HEAD": + return "helmet"; + default: + return null; + } + } + + @Override + public boolean isValid() { + return dataFile.isFile(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java new file mode 100644 index 0000000..024c66c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/ZNpcImporter.java @@ -0,0 +1,243 @@ +package lol.pyr.znpcsplus.conversion.znpcs; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.conversion.DataImporter; +import lol.pyr.znpcsplus.conversion.znpcs.model.*; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.message.MessageAction; +import lol.pyr.znpcsplus.interaction.playerchat.PlayerChatAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.interaction.switchserver.SwitchServerAction; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.util.BungeeConnector; +import lol.pyr.znpcsplus.util.ItemSerializationUtil; +import lol.pyr.znpcsplus.util.LookType; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.DyeColor; +import org.bukkit.inventory.ItemStack; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; + +public class ZNpcImporter implements DataImporter { + private final ConfigManager configManager; + private final BukkitAudiences adventure; + private final TaskScheduler taskScheduler; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final MojangSkinCache skinCache; + private final File dataFile; + private final File conversationFile; + private final Gson gson; + private final BungeeConnector bungeeConnector; + + public ZNpcImporter(ConfigManager configManager, BukkitAudiences adventure, + TaskScheduler taskScheduler, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, + NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, MojangSkinCache skinCache, File dataFile, BungeeConnector bungeeConnector) { + + this.configManager = configManager; + this.adventure = adventure; + this.taskScheduler = taskScheduler; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.skinCache = skinCache; + this.dataFile = dataFile; + this.conversationFile = new File(dataFile.getParentFile(), "conversations.json"); + this.bungeeConnector = bungeeConnector; + gson = new GsonBuilder() + .create(); + } + + @Override + public Collection importData() { + ZNpcsModel[] models; + try (BufferedReader fileReader = Files.newBufferedReader(dataFile.toPath())) { + JsonElement element = JsonParser.parseReader(fileReader); + models = gson.fromJson(element, ZNpcsModel[].class); + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + if (models == null) return Collections.emptyList(); + + + ZnpcsConversations[] conversations; + try (BufferedReader fileReader = Files.newBufferedReader(conversationFile.toPath())) { + JsonElement element = JsonParser.parseReader(fileReader); + conversations = gson.fromJson(element, ZnpcsConversations[].class); + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + if (conversations == null) return Collections.emptyList(); + + + ArrayList entries = new ArrayList<>(models.length); + for (ZNpcsModel model : models) { + String type = model.getNpcType(); + + switch (type.toLowerCase()) { + case "mushroom_cow": + type = "mooshroom"; + break; + case "snowman": + type = "snow_golem"; + break; + } + + ZNpcsLocation oldLoc = model.getLocation(); + NpcLocation location = new NpcLocation(oldLoc.getX(), oldLoc.getY(), oldLoc.getZ(), oldLoc.getYaw(), oldLoc.getPitch()); + UUID uuid = model.getUuid() == null ? UUID.randomUUID() : model.getUuid(); + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, oldLoc.getWorld(), typeRegistry.getByName(type), location); + npc.getType().applyDefaultProperties(npc); + + + // Convert the conversations from each NPC + ZNpcsConversation conversation = model.getConversation(); + if (conversation != null) { + + // Loop through all conversations in the conversations.json file + for (ZnpcsConversations conv : conversations) { + + // If the conversation name matches the conversation name in the data.json file, proceed + if (conv.getName().equalsIgnoreCase(conversation.getConversationName())) { + + int totalDelay = 0; + + // Loop through all texts in the conversation + for(ZNpcsConversationText text : conv.getTexts()) { + + // Add the delay in ticks to the total delay + totalDelay += text.getDelay() * 20; + + // Get the lines of text from the conversation + String[] lines = text.getLines(); + + // Loop through all lines of text + for (String line : lines) { + + // Create a new message action for each line of text + InteractionAction action = new MessageAction(adventure, textSerializer, line, InteractionType.ANY_CLICK, 0, totalDelay); + npc.addAction(action); + } + } + } + } + } + + + HologramImpl hologram = npc.getHologram(); + hologram.setOffset(model.getHologramHeight()); + Collections.reverse(model.getHologramLines()); + for (String raw : model.getHologramLines()) { + Component line = textSerializer.deserialize(raw); + hologram.addTextLineComponent(line); + } + + for (ZNpcsAction action : model.getClickActions()) { + InteractionType t = adaptClickType(action.getClickType()); + npc.addAction(adaptAction(action.getActionType(), t, action.getAction(), action.getDelay())); + } + + for (Map.Entry entry : model.getNpcEquip().entrySet()) { + EntityPropertyImpl property = propertyRegistry.getByName(entry.getKey(), ItemStack.class); + if (property == null) continue; + npc.setProperty(property, ItemSerializationUtil.itemFromB64(entry.getValue())); + } + + if (model.getSkinName() != null) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new NameFetchingDescriptor(skinCache, model.getSkinName())); + } + else if (model.getSkin() != null && model.getSignature() != null) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new PrefetchedDescriptor(new SkinImpl(model.getSkin(), model.getSignature()))); + } + + Map toggleValues = model.getNpcToggleValues() == null ? model.getNpcFunctions() : model.getNpcToggleValues(); + if (toggleValues != null) { + if (toggleValues.containsKey("look")) { + npc.setProperty(propertyRegistry.getByName("look", LookType.class), LookType.CLOSEST_PLAYER); + } + if (toggleValues.containsKey("mirror")) { + npc.setProperty(propertyRegistry.getByName("skin", SkinDescriptor.class), new MirrorDescriptor(skinCache)); + } + if (toggleValues.containsKey("glow") && (boolean) toggleValues.get("glow")) { + if (!model.getGlowName().isEmpty()) + try { + npc.setProperty(propertyRegistry.getByName("glow", DyeColor.class), DyeColor.valueOf(model.getGlowName())); + } catch (IllegalArgumentException e) { + npc.setProperty(propertyRegistry.getByName("glow", DyeColor.class), DyeColor.WHITE); + } + else + npc.setProperty(propertyRegistry.getByName("glow", DyeColor.class), DyeColor.WHITE); + } + } + + NpcEntryImpl entry = new NpcEntryImpl(String.valueOf(model.getId()), npc); + entry.enableEverything(); + entries.add(entry); + } + return entries; + } + + @Override + public boolean isValid() { + return dataFile.isFile(); + } + + private InteractionType adaptClickType(String clickType) { + switch (clickType.toLowerCase()) { + case "default": + return InteractionType.ANY_CLICK; + case "left": + return InteractionType.LEFT_CLICK; + case "right": + return InteractionType.RIGHT_CLICK; + } + throw new IllegalArgumentException("Couldn't adapt znpcs click type: " + clickType); + } + + private InteractionAction adaptAction(String type, InteractionType clickType, String parameter, int cooldown) { + switch (type.toLowerCase()) { + case "cmd": + return new PlayerCommandAction(taskScheduler, parameter, clickType, cooldown * 1000L, 0); + case "console": + return new ConsoleCommandAction(taskScheduler, parameter, clickType, cooldown * 1000L, 0); + case "chat": + return new PlayerChatAction(taskScheduler, parameter, clickType, cooldown * 1000L, 0); + case "message": + return new MessageAction(adventure, textSerializer, parameter, clickType, cooldown * 1000L, 0); + case "server": + return new SwitchServerAction(bungeeConnector, parameter, clickType, cooldown * 1000L, 0); + } + throw new IllegalArgumentException("Couldn't adapt znpcs click action: " + type); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java new file mode 100644 index 0000000..4092fa4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsAction.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsAction { + private String actionType; + private String clickType; + private String action; + private int delay; + + public String getActionType() { + return actionType; + } + + public String getClickType() { + return clickType; + } + + public String getAction() { + return action; + } + + public int getDelay() { + return delay; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java new file mode 100644 index 0000000..eb80d28 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversation.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsConversation { + + private String conversationName; + private String conversationType; + + public String getConversationName() { + return conversationName; + } + + public String getConversationType() { + return conversationType; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java new file mode 100644 index 0000000..e9a85b5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsConversationText.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsConversationText { + + private String[] lines; + private ZNpcsAction[] actions; + private int delay; + + public String[] getLines() { + return lines; + } + public ZNpcsAction[] getActions() { + return actions; + } + public int getDelay() { + return delay; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java new file mode 100644 index 0000000..b585cce --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsLocation.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZNpcsLocation { + private String world; + private double x; + private double y; + private double z; + private float yaw; + private float pitch; + + public String getWorld() { + return world; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public float getYaw() { + return yaw; + } + + public float getPitch() { + return pitch; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java new file mode 100644 index 0000000..987a9b4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZNpcsModel.java @@ -0,0 +1,91 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@SuppressWarnings("unused") +public class ZNpcsModel { + private int id; + private UUID uuid; + private double hologramHeight; + private String skinName; + private String skin; + private String signature; + + private String glowName; + + private ZNpcsConversation conversation; + private ZNpcsLocation location; + private String npcType; + private List hologramLines; + private List clickActions; + private Map npcEquip; + private Map npcToggleValues; + private Map npcFunctions; + private Map customizationMap; + + public int getId() { + return id; + } + + public UUID getUuid() { + return uuid; + } + + public double getHologramHeight() { + return hologramHeight; + } + + public String getSkinName() { + return skinName; + } + + public ZNpcsConversation getConversation() { + return conversation; + } + + public ZNpcsLocation getLocation() { + return location; + } + + public String getNpcType() { + return npcType; + } + + public List getHologramLines() { + return hologramLines; + } + + public List getClickActions() { + return clickActions; + } + + public Map getNpcEquip() { + return npcEquip; + } + + public Map getNpcToggleValues() { + return npcToggleValues; + } + + public Map getNpcFunctions() { + return npcFunctions; + } + + public Map getCustomizationMap() { + return customizationMap; + } + + public String getSkin() { + return skin; + } + + public String getSignature() { + return signature; + } + + public String getGlowName() { + return glowName; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java new file mode 100644 index 0000000..49cd913 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/conversion/znpcs/model/ZnpcsConversations.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.conversion.znpcs.model; + +@SuppressWarnings("unused") +public class ZnpcsConversations { + + private String name; + private ZNpcsConversationText[] texts; + private int radius; + private int delay; + + public String getName() { + return name; + } + public ZNpcsConversationText[] getTexts() { + return texts; + } + public int getRadius() { + return radius; + } + public int getDelay() { + return delay; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java new file mode 100644 index 0000000..4e4605a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ArmorStandVehicleProperties.java @@ -0,0 +1,76 @@ +package lol.pyr.znpcsplus.entity; + +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import org.bukkit.inventory.ItemStack; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Represents an armor stand vehicle entity. + *

+ * This entity is used to make the NPC sit on an invisible armor stand. + *

+ */ +public class ArmorStandVehicleProperties implements PropertyHolder { + + private final Map, Object> propertyMap = new HashMap<>(); + + public ArmorStandVehicleProperties(EntityPropertyRegistryImpl propertyRegistry) { + _setProperty(propertyRegistry.getByName("small", Boolean.class), true); + _setProperty(propertyRegistry.getByName("invisible", Boolean.class), true); + _setProperty(propertyRegistry.getByName("base_plate", Boolean.class), false); + } + + @SuppressWarnings("unchecked") + public T getProperty(EntityProperty key) { + return hasProperty(key) ? (T) propertyMap.get((EntityPropertyImpl) key) : key.getDefaultValue(); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return propertyMap.containsKey((EntityPropertyImpl) key); + } + + @SuppressWarnings("unchecked") + private void _setProperty(EntityProperty key, T value) { + Object val = value; + if (val instanceof ItemStack) val = SpigotConversionUtil.fromBukkitItemStack((ItemStack) val); + + setProperty((EntityPropertyImpl) key, (T) val); + } + + @Override + public void setProperty(EntityProperty key, T value) { + throw new UnsupportedOperationException("Cannot set properties on armor stands"); + } + + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + throw new UnsupportedOperationException("Cannot set item properties on armor stands"); + } + + @Override + public ItemStack getItemProperty(EntityProperty key) { + throw new UnsupportedOperationException("Cannot get item properties on armor stands"); + } + + public void setProperty(EntityPropertyImpl key, T value) { + if (key == null) return; + if (value == null || value.equals(key.getDefaultValue())) propertyMap.remove(key); + else propertyMap.put(key, value); + } + + public Set> getAllProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()); + } + + @Override + public Set> getAppliedProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java new file mode 100644 index 0000000..dcfdf5f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyImpl.java @@ -0,0 +1,62 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import org.bukkit.entity.Player; + +import java.util.*; + +public abstract class EntityPropertyImpl implements EntityProperty { + private final String name; + private final T defaultValue; + private final Class clazz; + private final Set> dependencies = new HashSet<>(); + private boolean playerModifiable = true; + + protected EntityPropertyImpl(String name, T defaultValue, Class clazz) { + this.name = name.toLowerCase(); + this.defaultValue = defaultValue; + this.clazz = clazz; + } + + @Override + public String getName() { + return name; + } + + @Override + public T getDefaultValue() { + return defaultValue; + } + + @Override + public boolean isPlayerModifiable() { + return playerModifiable; + } + + public void setPlayerModifiable(boolean playerModifiable) { + this.playerModifiable = playerModifiable; + } + + public Class getType() { + return clazz; + } + + public void addDependency(EntityPropertyImpl property) { + dependencies.add(property); + } + + protected static EntityData newEntityData(int index, EntityDataType type, V value) { + return new EntityData<>(index, type, value); + } + + public List> applyStandalone(Player player, PacketEntity packetEntity, boolean isSpawned) { + Map> map = new HashMap<>(); + apply(player, packetEntity, isSpawned, map); + for (EntityPropertyImpl property : dependencies) property.apply(player, packetEntity, isSpawned, map); + return new ArrayList<>(map.values()); + } + + abstract public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties); +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java new file mode 100644 index 0000000..28053e6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EntityPropertyRegistryImpl.java @@ -0,0 +1,800 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.attribute.Attributes; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.pose.EntityPose; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.nbt.NBTInt; +import com.github.retrooper.packetevents.protocol.nbt.NBTString; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.github.retrooper.packetevents.protocol.world.BlockFace; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.EntityPropertyRegistry; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.properties.*; +import lol.pyr.znpcsplus.entity.properties.attributes.AttributeProperty; +import lol.pyr.znpcsplus.entity.properties.villager.VillagerLevelProperty; +import lol.pyr.znpcsplus.entity.properties.villager.VillagerProfessionProperty; +import lol.pyr.znpcsplus.entity.properties.villager.VillagerTypeProperty; +import lol.pyr.znpcsplus.entity.serializers.*; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.*; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Color; +import org.bukkit.DyeColor; +import org.bukkit.Sound; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 1.8 ... + * 1.9 ... + * 1.10 ... + * 1.11 ... + * 1.12 ... + * 1.13 ... + * 1.14 ... + * 1.15 ... + * 1.16 ... + * 1.17 ... + * 1.18-1.19 ... + * 1.20 ... + * 1.21 ... + */ +@SuppressWarnings("unchecked") +public class EntityPropertyRegistryImpl implements EntityPropertyRegistry { + private final Map, PropertySerializer> serializerMap = new HashMap<>(); + private final Map> byName = new HashMap<>(); + private final ConfigManager configManager; + + public EntityPropertyRegistryImpl(MojangSkinCache skinCache, ConfigManager configManager) { + registerSerializer(new ComponentPropertySerializer()); + registerSerializer(new NamedColorPropertySerializer()); + registerSerializer(new SkinDescriptorSerializer(skinCache)); + registerSerializer(new ItemStackPropertySerializer()); + registerSerializer(new ColorPropertySerializer()); + registerSerializer(new Vector3fPropertySerializer()); + registerSerializer(new BlockStatePropertySerializer()); + registerSerializer(new LookTypeSerializer()); + registerSerializer(new GenericSerializer<>(Vector3i::toString, Vector3i::fromString, Vector3i.class)); + + registerEnumSerializer(NpcPose.class); + registerEnumSerializer(DyeColor.class); + registerEnumSerializer(CatVariant.class); + registerEnumSerializer(CreeperState.class); + registerEnumSerializer(ParrotVariant.class); + registerEnumSerializer(SpellType.class); + registerEnumSerializer(FoxVariant.class); + registerEnumSerializer(FrogVariant.class); + registerEnumSerializer(VillagerType.class); + registerEnumSerializer(VillagerProfession.class); + registerEnumSerializer(VillagerLevel.class); + registerEnumSerializer(AxolotlVariant.class); + registerEnumSerializer(HorseType.class); + registerEnumSerializer(HorseColor.class); + registerEnumSerializer(HorseStyle.class); + registerEnumSerializer(HorseArmor.class); + registerEnumSerializer(LlamaVariant.class); + registerEnumSerializer(MooshroomVariant.class); + registerEnumSerializer(OcelotType.class); + registerEnumSerializer(PandaGene.class); + registerEnumSerializer(PuffState.class); + registerEnumSerializer(TropicalFishVariant.TropicalFishPattern.class); + registerEnumSerializer(SnifferState.class); + registerEnumSerializer(RabbitType.class); + registerEnumSerializer(AttachDirection.class); + registerEnumSerializer(Sound.class); + registerEnumSerializer(ArmadilloState.class); + registerEnumSerializer(WoldVariant.class); + registerEnumSerializer(SkeletonType.class); + + registerPrimitiveSerializers(Integer.class, Boolean.class, Double.class, Float.class, Long.class, Short.class, Byte.class, String.class); + + this.configManager = configManager; + + /* + registerType("using_item", false); // TODO: fix it for 1.8 and add new property to use offhand item and riptide animation + + // Enderman + registerType("enderman_held_block", new BlockState(0)); // TODO: figure out the type on this + registerType("enderman_screaming", false); // TODO + registerType("enderman_staring", false); // TODO + */ + } + + public void registerTypes(PacketFactory packetFactory, LegacyComponentSerializer textSerializer, TaskScheduler taskScheduler) { + ServerVersion ver = PacketEvents.getAPI().getServerManager().getVersion(); + boolean legacyBooleans = ver.isOlderThan(ServerVersion.V_1_9); + boolean legacyNames = ver.isOlderThan(ServerVersion.V_1_9); + boolean optionalComponents = ver.isNewerThanOrEquals(ServerVersion.V_1_13); + + register(new EquipmentProperty(packetFactory, "helmet", EquipmentSlot.HELMET)); + register(new EquipmentProperty(packetFactory, "chestplate", EquipmentSlot.CHEST_PLATE)); + register(new EquipmentProperty(packetFactory, "leggings", EquipmentSlot.LEGGINGS)); + register(new EquipmentProperty(packetFactory, "boots", EquipmentSlot.BOOTS)); + register(new EquipmentProperty(packetFactory, "hand", EquipmentSlot.MAIN_HAND)); + register(new EquipmentProperty(packetFactory, "offhand", EquipmentSlot.OFF_HAND)); + + register(new NameProperty(textSerializer, legacyNames, optionalComponents)); + register(new DummyProperty<>("display_name", String.class)); + register(new DinnerboneProperty(legacyNames, optionalComponents)); + + register(new DummyProperty<>("look", LookType.FIXED)); + register(new DummyProperty<>("look_distance", configManager.getConfig().lookPropertyDistance())); + register(new DummyProperty<>("look_return", false)); + register(new DummyProperty<>("view_distance", configManager.getConfig().viewDistance())); + + register(new DummyProperty<>("permission_required", false)); + + register(new ForceBodyRotationProperty(taskScheduler)); + + register(new DummyProperty<>("player_knockback", false)); + register(new DummyProperty<>("player_knockback_exempt_permission", String.class)); + register(new DummyProperty<>("player_knockback_distance", 0.4)); + register(new DummyProperty<>("player_knockback_vertical", 0.4)); + register(new DummyProperty<>("player_knockback_horizontal", 0.9)); + register(new DummyProperty<>("player_knockback_cooldown", 1500)); + register(new DummyProperty<>("player_knockback_sound", false)); + register(new DummyProperty<>("player_knockback_sound_volume", 1.0f)); + register(new DummyProperty<>("player_knockback_sound_pitch", 1.0f)); + register(new DummyProperty<>("player_knockback_sound_name", Sound.valueOf( + PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_9) ? + "VILLAGER_NO" : "ENTITY_VILLAGER_NO" + ))); + + register(new GlowProperty(packetFactory)); + register(new BitsetProperty("fire", 0, 0x01)); + register(new BitsetProperty("invisible", 0, 0x20)); + register(new HologramItemProperty()); + linkProperties("glow", "fire", "invisible"); + register(new BooleanProperty("silent", 4, false, legacyBooleans)); + + // Attribute Max Health + register(new AttributeProperty(packetFactory, "attribute_max_health", Attributes.MAX_HEALTH)); + + // Health - LivingEntity + int healthIndex = 6; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) healthIndex = 9; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) healthIndex = 8; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) healthIndex = 7; + register(new HealthProperty(healthIndex)); + + final int tameableIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) tameableIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) tameableIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) tameableIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) tameableIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) tameableIndex = 12; + else tameableIndex = 16; + register(new BitsetProperty("sitting", tameableIndex, 0x01)); + register(new BitsetProperty("tamed", tameableIndex, 0x04)); + + int potionIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) potionIndex = 10; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) potionIndex = 9; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) potionIndex = 8; + else potionIndex = 7; + register(new EncodedIntegerProperty<>("potion_color", Color.class, potionIndex++, Color::asRGB)); + register(new BooleanProperty("potion_ambient", potionIndex, false, legacyBooleans)); + + int babyIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) babyIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) babyIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) babyIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) babyIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) babyIndex = 11; + else babyIndex = 12; + if (ver.isOlderThan(ServerVersion.V_1_9)) { + register(new LegacyBabyProperty(babyIndex)); + } else { + register(new BooleanProperty("baby", babyIndex, false, legacyBooleans)); + } + + register(new EntitySittingProperty(packetFactory, this)); + + // Player + register(new DummyProperty<>("skin", SkinDescriptor.class, false)); + final int skinLayersIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) skinLayersIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_16)) skinLayersIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) skinLayersIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) skinLayersIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) skinLayersIndex = 12; + else skinLayersIndex = 10; + register(new BitsetProperty("skin_cape", skinLayersIndex, 0x01)); + register(new BitsetProperty("skin_jacket", skinLayersIndex, 0x02)); + register(new BitsetProperty("skin_left_sleeve", skinLayersIndex, 0x04)); + register(new BitsetProperty("skin_right_sleeve", skinLayersIndex, 0x08)); + register(new BitsetProperty("skin_left_leg", skinLayersIndex, 0x10)); + register(new BitsetProperty("skin_right_leg", skinLayersIndex, 0x20)); + register(new BitsetProperty("skin_hat", skinLayersIndex, 0x40)); + linkProperties("skin_cape", "skin_jacket", "skin_left_sleeve", "skin_right_sleeve", "skin_left_leg", "skin_right_leg", "skin_hat"); + + // Armor Stand + int armorStandIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) armorStandIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) armorStandIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) armorStandIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) armorStandIndex = 11; + else armorStandIndex = 10; + register(new BitsetProperty("small", armorStandIndex, 0x01)); + register(new BitsetProperty("arms", armorStandIndex, 0x04)); + register(new BitsetProperty("base_plate", armorStandIndex++, 0x08, true)); + linkProperties("small", "arms", "base_plate"); + register(new RotationProperty("head_rotation", armorStandIndex++, Vector3f.zero())); + register(new RotationProperty("body_rotation", armorStandIndex++, Vector3f.zero())); + register(new RotationProperty("left_arm_rotation", armorStandIndex++, new Vector3f(-10, 0, -10))); + register(new RotationProperty("right_arm_rotation", armorStandIndex++, new Vector3f(-15, 0, 10))); + register(new RotationProperty("left_leg_rotation", armorStandIndex++, new Vector3f(-1, 0, -1))); + register(new RotationProperty("right_leg_rotation", armorStandIndex, new Vector3f(1, 0, 1))); + + // Ghast + final int ghastAttackingIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) ghastAttackingIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) ghastAttackingIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) ghastAttackingIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) ghastAttackingIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) ghastAttackingIndex = 11; + else ghastAttackingIndex = 16; + register(new BooleanProperty("attacking", ghastAttackingIndex, false, legacyBooleans)); + + // Bat + final int batIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) batIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) batIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) batIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) batIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) batIndex = 11; + else batIndex = 16; + register(new BooleanProperty("hanging", batIndex, false, true /* This isn't a mistake */)); + + // Blaze + final int blazeIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) blazeIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) blazeIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) blazeIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) blazeIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) blazeIndex = 11; + else blazeIndex = 16; + register(new BitsetProperty("blaze_on_fire", blazeIndex, 0x01)); + + // Creeper + int creeperIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) creeperIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) creeperIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) creeperIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) creeperIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) creeperIndex = 11; + else creeperIndex= 16; + register(new EncodedIntegerProperty<>("creeper_state", CreeperState.IDLE, creeperIndex++, CreeperState::getState)); + register(new BooleanProperty("creeper_charged", creeperIndex, false, legacyBooleans)); + + // Abstract Horse + int horseIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) horseIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) horseIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) horseIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) horseIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) horseIndex = 12; + else horseIndex = 16; + int horseEating = ver.isNewerThanOrEquals(ServerVersion.V_1_12) ? 0x10 : 0x20; + register(new BitsetProperty("is_tame", horseIndex, 0x02, false, legacyBooleans)); + if (ver.isOlderThan(ServerVersion.V_1_21)) { + register(new BitsetProperty("is_saddled", horseIndex, 0x04, false, legacyBooleans)); + } + register(new BitsetProperty("is_eating", horseIndex, horseEating, false, legacyBooleans)); + register(new BitsetProperty("is_rearing", horseIndex, horseEating << 1, false, legacyBooleans)); + register(new BitsetProperty("has_mouth_open", horseIndex, horseEating << 2, false, legacyBooleans)); + + // End Crystal + if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) { + int endCrystalIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) endCrystalIndex = 8; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) endCrystalIndex = 7; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) endCrystalIndex = 6; + else endCrystalIndex = 5; + register(new OptionalBlockPosProperty("beam_target", null, endCrystalIndex++)); + register(new BooleanProperty("show_base", endCrystalIndex, true, false)); + } + + // Guardian + if (ver.isOlderThan(ServerVersion.V_1_11)) { + int guardianIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) guardianIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) guardianIndex = 11; + else guardianIndex = 16; + register(new BitsetProperty("is_elder", guardianIndex, 0x04, false, legacyBooleans)); + register(new BitsetProperty("is_retracting_spikes", guardianIndex, 0x02, false, legacyBooleans)); + linkProperties("is_elder", "is_retracting_spikes"); + // TODO: add guardian beam target + } else { + int guardianIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) guardianIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) guardianIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) guardianIndex = 14; + else guardianIndex = 12; + register(new BooleanProperty("is_retracting_spikes", guardianIndex, false, false)); + } + + // Horse + if (ver.isNewerThanOrEquals(ServerVersion.V_1_8) && ver.isOlderThan(ServerVersion.V_1_9)) { + register(new EncodedByteProperty<>("horse_type", HorseType.HORSE, 19, obj -> (byte) obj.ordinal())); + } else if (ver.isOlderThan(ServerVersion.V_1_11)) { + int horseTypeIndex = 14; + if (ver.isOlderThan(ServerVersion.V_1_10)) horseTypeIndex = 13; + register(new EncodedIntegerProperty<>("horse_type", HorseType.HORSE, horseTypeIndex, Enum::ordinal)); + } + int horseVariantIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_18)) horseVariantIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) horseVariantIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) horseVariantIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) horseVariantIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) horseVariantIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) horseVariantIndex = 14; + else horseVariantIndex = 20; + register(new HorseStyleProperty(horseVariantIndex)); + register(new HorseColorProperty(horseVariantIndex)); + linkProperties("horse_style", "horse_color"); + + // Use chesteplate property for 1.14 and above + if (ver.isOlderThan(ServerVersion.V_1_14)) { + int horseArmorIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_11)) horseArmorIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) horseArmorIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) horseArmorIndex = 16; + else horseArmorIndex = 22; + register(new EncodedIntegerProperty<>("horse_armor", HorseArmor.NONE, horseArmorIndex, Enum::ordinal)); + } + + // Chested Horse + if (ver.isOlderThan(ServerVersion.V_1_11)) { + register(new BitsetProperty("has_chest", horseIndex, 0x08, false, legacyBooleans)); + linkProperties("is_tame", "is_saddled", "has_chest", "is_eating", "is_rearing", "has_mouth_open"); + } else { + register(new BooleanProperty("has_chest", horseVariantIndex, false, legacyBooleans)); + if (ver.isOlderThan(ServerVersion.V_1_21)){ + linkProperties("is_tame", "is_saddled", "is_eating", "is_rearing", "has_mouth_open"); + } else { + linkProperties("is_tame", "is_eating", "is_rearing", "has_mouth_open"); + } + } + + // Slime, Magma Cube and Phantom + int sizeIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) sizeIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) sizeIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) sizeIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) sizeIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) sizeIndex = 11; + else sizeIndex = 16; + register(new IntegerProperty("size", sizeIndex, 1, legacyBooleans)); + + // Ocelot + if (ver.isOlderThan(ServerVersion.V_1_14)) { + int ocelotIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) ocelotIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) ocelotIndex = 14; + else ocelotIndex = 18; + if (legacyBooleans) register(new EncodedByteProperty<>("ocelot_type", OcelotType.OCELOT, ocelotIndex, obj -> (byte) obj.ordinal())); + else register(new EncodedIntegerProperty<>("ocelot_type", OcelotType.OCELOT, ocelotIndex, Enum::ordinal)); + } + + // Pig + int pigIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) pigIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) pigIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) pigIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) pigIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) pigIndex = 12; + else pigIndex = 16; + register(new BooleanProperty("pig_saddled", pigIndex, false, legacyBooleans)); + + // Rabbit + int rabbitIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) rabbitIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) rabbitIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) rabbitIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) rabbitIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) rabbitIndex = 12; + else rabbitIndex = 18; + register(new RabbitTypeProperty(rabbitIndex, legacyBooleans, legacyNames, optionalComponents)); + + // Sheep + int sheepIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) sheepIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) sheepIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) sheepIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) sheepIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) sheepIndex = 12; + else sheepIndex = 16; + // noinspection deprecation + register(new EncodedByteProperty<>("sheep_color", DyeColor.WHITE, sheepIndex, DyeColor::getWoolData)); + register(new BitsetProperty("sheep_sheared", sheepIndex, 0x10, false, legacyBooleans)); // no need to link because sheep_sheared is only visible when sheep_color is WHITE + + // Villager + int villagerIndex; + if (ver.isOlderThan(ServerVersion.V_1_14)) { + if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) villagerIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) villagerIndex = 12; + else villagerIndex = 16; + register(new EncodedIntegerProperty<>("villager_profession", VillagerProfession.NONE, villagerIndex, VillagerProfession::getLegacyId)); + } + + // Wolf + int wolfIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) wolfIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) wolfIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) wolfIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) wolfIndex = 15; + else wolfIndex = 19; + register(new BooleanProperty("wolf_begging", wolfIndex++, false, legacyBooleans)); + if (legacyBooleans) { + // noinspection deprecation + register(new EncodedByteProperty<>("wolf_collar", DyeColor.BLUE, wolfIndex++, DyeColor::getDyeData)); + } else register(new EncodedIntegerProperty<>("wolf_collar", DyeColor.RED, wolfIndex++, Enum::ordinal)); + if (ver.isNewerThanOrEquals(ServerVersion.V_1_16)) { + register(new EncodedIntegerProperty<>("wolf_angry", false, wolfIndex++, b -> b ? 1 : 0)); + linkProperties("tamed", "sitting"); + } + else { + register(new BitsetProperty("wolf_angry", tameableIndex, 0x02)); + linkProperties("wolf_angry", "tamed", "sitting"); + } + + // Wither + int witherIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) witherIndex = 16; // using the first index, so we can add the other properties later if needed + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) witherIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) witherIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) witherIndex = 12; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) witherIndex = 11; + else witherIndex = 17; + witherIndex += 3; // skip the first 3 indexes, will be used for the other properties later + register(new IntegerProperty("invulnerable_time", witherIndex, 0, false)); + + // Skeleton + if (ver.isOlderThan(ServerVersion.V_1_11)) { + if (legacyBooleans) register(new EncodedByteProperty<>("skeleton_type", SkeletonType.NORMAL, 13, SkeletonType::getLegacyId)); + else register(new EncodedIntegerProperty<>("skeleton_type", SkeletonType.NORMAL, ver.isOlderThan(ServerVersion.V_1_10) ? 11 : 12, Enum::ordinal)); + } + + // Zombie + int zombieIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) zombieIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_16)) zombieIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) zombieIndex = 13; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_9)) zombieIndex = 12; + else zombieIndex = 13; + + if (ver.isOlderThan(ServerVersion.V_1_9)) { + register(new EncodedByteProperty<>("zombie_is_villager", false, zombieIndex++, b -> (byte) (b ? 1 : 0))); + } else if (ver.isOlderThan(ServerVersion.V_1_11)) { + register(new EncodedIntegerProperty<>("zombie_type", ZombieType.ZOMBIE, zombieIndex++, Enum::ordinal)); + } else { + zombieIndex++; // Not a mistake, this is field unused in 1.11+ + } + if (ver.isOlderThan(ServerVersion.V_1_9)) { + register(new EncodedByteProperty<>("is_converting", false, zombieIndex++, b -> (byte) (b ? 1 : 0))); + } else if (ver.isOlderThan(ServerVersion.V_1_11)) { + register(new BooleanProperty("is_converting", zombieIndex++, false, legacyBooleans)); + } + if (ver.isNewerThanOrEquals(ServerVersion.V_1_9) && ver.isOlderThan(ServerVersion.V_1_14)) { + register(new BooleanProperty("zombie_hands_held_up", zombieIndex++, false, legacyBooleans)); + } + if (ver.isNewerThanOrEquals(ServerVersion.V_1_13)) { + register(new BooleanProperty("zombie_becoming_drowned", zombieIndex++, false, legacyBooleans)); + } + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_9)) return; + // Shulker + int shulkerIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) shulkerIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) shulkerIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) shulkerIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) shulkerIndex = 12; + else shulkerIndex = 11; + register(new CustomTypeProperty<>("attach_direction", shulkerIndex++, AttachDirection.DOWN, EntityDataTypes.BLOCK_FACE, attachDir -> BlockFace.valueOf(attachDir.name()))); + register(new EncodedByteProperty<>("shield_height", 0, shulkerIndex++, value -> (byte) Math.max(0, Math.min(100, value)))); + // noinspection deprecation + register(new EncodedByteProperty<>("shulker_color", DyeColor.class, shulkerIndex, DyeColor::getWoolData)); + + // Snow Golem + int snowGolemIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) snowGolemIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) snowGolemIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) snowGolemIndex = 14; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_10)) snowGolemIndex = 12; + else snowGolemIndex = 10; + register(new CustomTypeProperty<>("derpy_snowgolem", snowGolemIndex, false, EntityDataTypes.BYTE, b -> (byte) (b ? 0x00 : 0x10))); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_10)) return; + // Polar Bear + int polarBearIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) polarBearIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) polarBearIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) polarBearIndex = 15; + else polarBearIndex = 13; + register(new BooleanProperty("polar_bear_standing", polarBearIndex, false, false)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_11)) return; + // Spellcaster Illager + int spellIndex = 12; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) spellIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) spellIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) spellIndex = 15; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_12)) spellIndex = 13; + register(new EncodedByteProperty<>("spell", SpellType.NONE, spellIndex, obj -> (byte) Math.min(obj.ordinal(), ver.isOlderThan(ServerVersion.V_1_13) ? 3 : 5))); + + // Llama + int llamaIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_18)) llamaIndex = 20; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) llamaIndex = 21; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) llamaIndex = 20; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) llamaIndex = 19; + else llamaIndex = 17; + + // Removed in 1.21 + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21)) register(new EncodedIntegerProperty("carpet_color", DyeColor.class, llamaIndex++, obj -> obj == null ? -1 : obj.ordinal())); + register(new EncodedIntegerProperty<>("llama_variant", LlamaVariant.CREAMY, llamaIndex, Enum::ordinal)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_12)) return; + // Parrot + int parrotIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) parrotIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) parrotIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) parrotIndex = 17; + else parrotIndex = 15; + register(new EncodedIntegerProperty<>("parrot_variant", ParrotVariant.RED_BLUE, parrotIndex, Enum::ordinal)); + + // Player + NBTProperty.NBTDecoder parrotVariantDecoder = (variant) -> { + NBTCompound compound = new NBTCompound(); + if (variant == null) return compound; + compound.setTag("id", new NBTString("minecraft:parrot")); + compound.setTag("Variant", new NBTInt(variant.ordinal())); + return compound; + }; + int shoulderIndex = skinLayersIndex+2; + register(new NBTProperty<>("shoulder_entity_left", ParrotVariant.class, shoulderIndex++, parrotVariantDecoder, true)); + register(new NBTProperty<>("shoulder_entity_right", ParrotVariant.class, shoulderIndex, parrotVariantDecoder, true)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_13)) return; + // Pufferfish + int pufferfishIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) pufferfishIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) pufferfishIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) pufferfishIndex = 15; + else pufferfishIndex = 13; + register(new EncodedIntegerProperty<>("puff_state", PuffState.DEFLATED, pufferfishIndex, Enum::ordinal)); + + // Tropical Fish + int tropicalFishIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) tropicalFishIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) tropicalFishIndex = 16; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_14)) tropicalFishIndex = 15; + else tropicalFishIndex = 13; + register(new TropicalFishVariantProperty<>("tropical_fish_pattern", TropicalFishVariant.TropicalFishPattern.KOB, tropicalFishIndex, TropicalFishVariant.Builder::pattern)); + register(new TropicalFishVariantProperty<>("tropical_fish_body_color", DyeColor.WHITE, tropicalFishIndex, TropicalFishVariant.Builder::bodyColor)); + register(new TropicalFishVariantProperty<>("tropical_fish_pattern_color", DyeColor.WHITE, tropicalFishIndex, TropicalFishVariant.Builder::patternColor)); + linkProperties("tropical_fish_pattern", "tropical_fish_body_color", "tropical_fish_pattern_color"); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_14)) return; + // Pose + register(new CustomTypeProperty<>("pose", 6, NpcPose.STANDING, EntityDataTypes.ENTITY_POSE, npcPose -> EntityPose.valueOf(npcPose.name()))); + + // Villager + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) villagerIndex = 18; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) villagerIndex = 17; + else villagerIndex = 16; + register(new VillagerTypeProperty("villager_type", villagerIndex, VillagerType.PLAINS)); + register(new VillagerProfessionProperty("villager_profession", villagerIndex, VillagerProfession.NONE)); + register(new VillagerLevelProperty("villager_level", villagerIndex, VillagerLevel.STONE)); + linkProperties("villager_type", "villager_profession", "villager_level"); + + // Cat + int catIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) catIndex = 19; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) catIndex = 18; + else catIndex = 17; + register(new EncodedIntegerProperty<>("cat_variant", CatVariant.BLACK, catIndex++, Enum::ordinal, EntityDataTypes.CAT_VARIANT)); + register(new BooleanProperty("cat_laying", catIndex++, false, legacyBooleans)); + register(new BooleanProperty("cat_relaxed", catIndex++, false, legacyBooleans)); + register(new EncodedIntegerProperty<>("cat_collar", DyeColor.RED, catIndex, Enum::ordinal)); + + // Fox + int foxIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) foxIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) foxIndex = 16; + else foxIndex = 15; + register(new EncodedIntegerProperty<>("fox_variant", FoxVariant.RED, foxIndex++, Enum::ordinal)); + register(new BitsetProperty("fox_sitting", foxIndex, 0x01)); + register(new BitsetProperty("fox_crouching", foxIndex, 0x04)); + register(new BitsetProperty("fox_sleeping", foxIndex, 0x20)); + linkProperties("fox_sitting", "fox_crouching", "fox_sleeping"); + + int mooshroomIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) mooshroomIndex = 17; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) mooshroomIndex = 16; + else mooshroomIndex = 15; + register(new EncodedStringProperty<>("mooshroom_variant", MooshroomVariant.RED, mooshroomIndex, MooshroomVariant::getVariantName)); + + // Panda + int pandaIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) pandaIndex = 20; + else if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) pandaIndex = 19; + else pandaIndex = 18; + register(new EncodedByteProperty<>("panda_main_gene", PandaGene.NORMAL, pandaIndex++, obj -> (byte) obj.ordinal())); + register(new EncodedByteProperty<>("panda_hidden_gene", PandaGene.NORMAL, pandaIndex++, obj -> (byte) obj.ordinal())); + if (ver.isNewerThanOrEquals(ServerVersion.V_1_15)) { + register(new BitsetProperty("panda_sneezing", pandaIndex, 0x02)); + register(new BitsetProperty("panda_rolling", pandaIndex, 0x04)); + register(new BitsetProperty("panda_sitting", pandaIndex, 0x08)); + register(new BitsetProperty("panda_on_back", pandaIndex, 0x10)); + linkProperties("panda_sneezing", "panda_rolling", "panda_sitting", "panda_on_back"); + } else { + register(new BitsetProperty("panda_sneezing", pandaIndex, 0x02)); + register(new BitsetProperty("panda_eating", pandaIndex, 0x04)); + linkProperties("panda_sneezing", "panda_eating"); + } + + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_15)) return; + + register(new BitsetProperty("fox_faceplanted", foxIndex, 0x40)); + linkProperties("fox_sitting", "fox_crouching", "fox_sleeping", "fox_faceplanted"); + + // Bee + int beeIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) beeIndex = 17; + else beeIndex = 18; + register(new BitsetProperty("has_nectar", beeIndex++, 0x08)); + register(new EncodedIntegerProperty<>("angry", false, beeIndex, enabled -> enabled ? 1 : 0)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_16)) return; + + // Hoglin and Piglin Zombification + final int zombificationIndex; + if (ver.isNewerThanOrEquals(ServerVersion.V_1_17)) zombificationIndex = 17; // Change piglinIndex, pillagerIndex, striderIndex and vindicatorIndex if you change this + else zombificationIndex = 16; + register(new BooleanProperty("hoglin_immune_to_zombification", zombificationIndex, false, legacyBooleans)); + register(new BooleanProperty("piglin_immune_to_zombification", zombificationIndex-1, false, legacyBooleans)); + + // Piglin + int piglinIndex = zombificationIndex; + register(new BooleanProperty("piglin_baby", piglinIndex++, false, legacyBooleans)); + register(new BooleanProperty("piglin_charging_crossbow", piglinIndex++, false, legacyBooleans)); + register(new BooleanProperty("piglin_dancing", piglinIndex, false, legacyBooleans)); + + // Pillager + register(new BooleanProperty("pillager_charging", zombificationIndex, false, legacyBooleans)); + + // Strider + int striderIndex = zombificationIndex + 1; + register(new BooleanProperty("strider_shaking", striderIndex++, false, legacyBooleans)); // TODO: Fix this, it needs to be set constantly i guess + register(new BooleanProperty("strider_saddled", striderIndex, false, legacyBooleans)); + + // Vindicator + int vindicatorIndex = zombificationIndex -1; + register(new BooleanProperty("celebrating", vindicatorIndex, false, legacyBooleans)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_17)) return; + // Axolotl + register(new EncodedIntegerProperty<>("axolotl_variant", AxolotlVariant.LUCY, 17, Enum::ordinal)); + register(new BooleanProperty("playing_dead", 18, false, legacyBooleans)); + + // Goat + register(new BooleanProperty("has_left_horn", 18, true, legacyBooleans)); + register(new BooleanProperty("has_right_horn", 19, true, legacyBooleans)); + + register(new EncodedIntegerProperty<>("shaking", false,7, enabled -> enabled ? 140 : 0)); + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_19)) return; + // Frog + register(new EncodedIntegerProperty<>("frog_variant", FrogVariant.TEMPERATE, 17, Enum::ordinal, EntityDataTypes.FROG_VARIANT)); + + // Warden + register(new EncodedIntegerProperty<>("warden_anger", 0, 16, b -> Math.min(150, Math.max(0, b)))); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_20)) return; + + // Camel + int camelIndex = 18; + register(new BooleanProperty("bashing", camelIndex++, false, legacyBooleans)); + register(new CamelSittingProperty(6, camelIndex)); + + // Sniffer + register(new CustomTypeProperty<>("sniffer_state", 17, SnifferState.IDLING, EntityDataTypes.SNIFFER_STATE, state -> com.github.retrooper.packetevents.protocol.entity.sniffer.SnifferState.valueOf(state.name()))); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_20_5)) return; + // Armadillo + register(new CustomTypeProperty<>("armadillo_state", 17, ArmadilloState.IDLE, EntityDataTypes.ARMADILLO_STATE, state -> + com.github.retrooper.packetevents.protocol.entity.armadillo.ArmadilloState.valueOf(state.name()))); + + // Wolf + register(new EncodedIntegerProperty<>("wolf_variant", WoldVariant.PALE, wolfIndex, WoldVariant::getId, EntityDataTypes.WOLF_VARIANT)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21)) return; + + register(new EquipmentProperty(packetFactory, "body", EquipmentSlot.BODY)); + + // Bogged + register(new BooleanProperty("bogged_sheared", 16, false, legacyBooleans)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21_2)) return; + + // Creaking + register(new BooleanProperty("creaking_active", 17, false, legacyBooleans)); + + if (!ver.isNewerThanOrEquals(ServerVersion.V_1_21_4)) return; + + // Creaking + register(new BooleanProperty("creaking_crumbling", 18, false, legacyBooleans)); + } + + private void registerSerializer(PropertySerializer serializer) { + serializerMap.put(serializer.getTypeClass(), serializer); + } + + private > void registerEnumSerializer(Class clazz) { + serializerMap.put(clazz, new EnumPropertySerializer<>(clazz)); + } + + private void registerPrimitiveSerializers(Class... classes) { + for (Class clazz : classes) { + registerPrimitiveSerializer(clazz); + } + } + + private void registerPrimitiveSerializer(Class clazz) { + serializerMap.put(clazz, new PrimitivePropertySerializer<>(clazz)); + } + + private void register(EntityPropertyImpl property) { + if (byName.containsKey(property.getName())) + throw new IllegalArgumentException("Duplicate property name: " + property.getName()); + byName.put(property.getName(), property); + } + + private void linkProperties(String... names) { + linkProperties(Arrays.stream(names) + .map(this::getByName) + .collect(Collectors.toSet())); + } + + private void linkProperties(Collection> properties) { + for (EntityPropertyImpl property : properties) for (EntityPropertyImpl dependency : properties) { + if (property.equals(dependency)) continue; + property.addDependency(dependency); + } + } + + public PropertySerializer getSerializer(Class type) { + return (PropertySerializer) serializerMap.get(type); + } + + @Override + public Collection> getAll() { + return Collections.unmodifiableCollection( + byName.values().stream() + .map(property -> (EntityProperty) property) + .collect(Collectors.toSet())); + } + + public EntityPropertyImpl getByName(String name, Class type) { + return (EntityPropertyImpl) getByName(name); + } + + @Override + public void registerDummy(String name, Class type, boolean playerModifiable) { + register(new DummyProperty<>(name, type, playerModifiable)); + } + + public EntityPropertyImpl getByName(String name) { + return byName.get(name.toLowerCase()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java new file mode 100644 index 0000000..d71fb07 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/EnumPropertySerializer.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity; + +public class EnumPropertySerializer> implements PropertySerializer { + + private final Class enumClass; + + public EnumPropertySerializer(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + public String serialize(T property) { + return property.name(); + } + + @Override + public T deserialize(String property) { + try { + return Enum.valueOf(enumClass, property.toUpperCase()); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public Class getTypeClass() { + return enumClass; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java new file mode 100644 index 0000000..e1cf15c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PacketEntity.java @@ -0,0 +1,224 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.reflection.Reflections; +import lol.pyr.znpcsplus.util.FutureUtil; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class PacketEntity implements PropertyHolder { + private final PacketFactory packetFactory; + + private final PropertyHolder properties; + private final Viewable viewable; + private final int entityId; + private final UUID uuid; + + private final EntityType type; + private NpcLocation location; + + private PacketEntity vehicle; + private Integer vehicleId; + private List passengers; + + public PacketEntity(PacketFactory packetFactory, PropertyHolder properties, Viewable viewable, EntityType type, NpcLocation location) { + this.packetFactory = packetFactory; + this.properties = properties; + this.viewable = viewable; + this.entityId = reserveEntityID(); + this.uuid = UUID.randomUUID(); + this.type = type; + this.location = location; + } + + public int getEntityId() { + return entityId; + } + + public NpcLocation getLocation() { + return location; + } + + public UUID getUuid() { + return uuid; + } + + public EntityType getType() { + return type; + } + + public void setLocation(NpcLocation location) { + this.location = location; + if (vehicle != null) { + vehicle.setLocation(location.withY(location.getY() - 0.9)); + return; + } + for (Player viewer : viewable.getViewers()) packetFactory.teleportEntity(viewer, this); + } + + public CompletableFuture spawn(Player player) { + return FutureUtil.exceptionPrintingRunAsync(() -> { + if (type == EntityTypes.PLAYER) packetFactory.spawnPlayer(player, this, properties).join(); + else packetFactory.spawnEntity(player, this, properties); + if (vehicle != null) { + setVehicle(vehicle); + } + if (vehicleId != null) { + packetFactory.setPassengers(player, vehicleId, this.getEntityId()); + } + if (passengers != null) { + packetFactory.setPassengers(player, this.getEntityId(), passengers.stream().mapToInt(Integer::intValue).toArray()); + } + }); + } + + public void setHeadRotation(Player player, float yaw, float pitch) { + packetFactory.sendHeadRotation(player, this, yaw, pitch); + } + + public PacketEntity getVehicle() { + return vehicle; + } + + public Viewable getViewable() { + return viewable; + } + + public void setVehicleId(Integer vehicleId) { + if (this.vehicle != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicle.getEntityId()); + this.vehicle.despawn(player); + packetFactory.teleportEntity(player, this); + } + } else if (this.vehicleId != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicleId); + } + } + this.vehicleId = vehicleId; + if (vehicleId == null) return; + + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.getEntityId(), vehicleId); + } + } + + public void setVehicle(PacketEntity vehicle) { + // remove old vehicle + if (this.vehicle != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicle.getEntityId()); + this.vehicle.despawn(player); + packetFactory.teleportEntity(player, this); + } + } else if (this.vehicleId != null) { + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.vehicleId); + } + } + + this.vehicle = vehicle; + if (this.vehicle == null) return; + + vehicle.setLocation(location.withY(location.getY() - 0.9)); + for (Player player : viewable.getViewers()) { + vehicle.spawn(player).thenRun(() -> { + packetFactory.setPassengers(player, vehicle.getEntityId(), this.getEntityId()); + }); + } + } + + public Integer getVehicleId() { + return vehicleId; + } + + public List getPassengers() { + return passengers == null ? Collections.emptyList() : passengers; + } + + public void addPassenger(int entityId) { + if (passengers == null) { + passengers = new ArrayList<>(); + } + passengers.add(entityId); + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.getEntityId(), passengers.stream().mapToInt(Integer::intValue).toArray()); + } + } + + public void removePassenger(int entityId) { + if (passengers == null) return; + passengers.remove(entityId); + for (Player player : viewable.getViewers()) { + packetFactory.setPassengers(player, this.getEntityId(), passengers.stream().mapToInt(Integer::intValue).toArray()); + } + if (passengers.isEmpty()) { + passengers = null; + } + } + + public void despawn(Player player) { + packetFactory.destroyEntity(player, this, properties); + if (vehicle != null) vehicle.despawn(player); + } + + public void refreshMeta(Player player) { + packetFactory.sendAllMetadata(player, this, properties); + } + + public void swingHand(Player player, boolean offhand) { + packetFactory.sendHandSwing(player, this, offhand); + } + + private static int reserveEntityID() { + if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14)) { + return Reflections.ATOMIC_ENTITY_ID_FIELD.get().incrementAndGet(); + } else { + int id = Reflections.ENTITY_ID_MODIFIER.get(); + Reflections.ENTITY_ID_MODIFIER.set(id + 1); + return id; + } + } + + @Override + public T getProperty(EntityProperty key) { + return properties.getProperty(key); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return properties.hasProperty(key); + } + + @Override + public void setProperty(EntityProperty key, T value) { + properties.setProperty(key, value); + } + + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + properties.setItemProperty(key, value); + } + + @Override + public ItemStack getItemProperty(EntityProperty key) { + return properties.getItemProperty(key); + } + + @Override + public Set> getAppliedProperties() { + return properties.getAppliedProperties(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java new file mode 100644 index 0000000..13eacf9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/ParrotNBTCompound.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.entity; + +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.nbt.NBTInt; +import com.github.retrooper.packetevents.protocol.nbt.NBTString; +import lol.pyr.znpcsplus.util.ParrotVariant; + +// Not sure where to put this or even if it's needed +public class ParrotNBTCompound { + private final NBTCompound tag = new NBTCompound(); + + public ParrotNBTCompound(ParrotVariant variant) { + tag.setTag("id", new NBTString("minecraft:parrot")); + tag.setTag("Variant", new NBTInt(variant.ordinal())); + // other tags if needed, idk + } + + public NBTCompound getTag() { + return tag; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java new file mode 100644 index 0000000..ad18eaf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PrimitivePropertySerializer.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.entity; + +import java.lang.reflect.InvocationTargetException; + +public class PrimitivePropertySerializer implements PropertySerializer { + private final Class clazz; + + public PrimitivePropertySerializer(Class clazz) { + this.clazz = clazz; + } + + @Override + public String serialize(T property) { + return String.valueOf(property); + } + + @Override + public T deserialize(String property) { + try { + return clazz.getConstructor(String.class).newInstance(property); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { + throw new NullPointerException("Failed to deserialize property " + property + " of type " + clazz.getName() + "!"); + } + } + + @Override + public Class getTypeClass() { + return clazz; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java new file mode 100644 index 0000000..abd2f39 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/PropertySerializer.java @@ -0,0 +1,12 @@ +package lol.pyr.znpcsplus.entity; + +public interface PropertySerializer { + String serialize(T property); + T deserialize(String property); + Class getTypeClass(); + + @SuppressWarnings("unchecked") + default String UNSAFE_serialize(Object property) { + return serialize((T) property); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java new file mode 100644 index 0000000..eb01483 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BitsetProperty.java @@ -0,0 +1,52 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class BitsetProperty extends EntityPropertyImpl { + private final int index; + private final int bitmask; + private final boolean inverted; + private boolean integer = false; + + public BitsetProperty(String name, int index, int bitmask, boolean inverted, boolean integer) { + this(name, index, bitmask, inverted); + this.integer = integer; + } + + public BitsetProperty(String name, int index, int bitmask, boolean inverted) { + super(name, inverted, Boolean.class); + this.index = index; + this.bitmask = bitmask; + this.inverted = inverted; + } + + public BitsetProperty(String name, int index, int bitmask) { + this(name, index, bitmask, false); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + boolean enabled = entity.getProperty(this); + if (inverted) enabled = !enabled; + if (integer) { + int oldValue = 0; + if (oldData != null && oldData.getValue() instanceof Number) { + oldValue = ((Number) oldData.getValue()).intValue(); + } + properties.put(index, newEntityData(index, EntityDataTypes.INT, oldValue | (enabled ? bitmask : 0))); + } else { + byte oldValue = 0; + if (oldData != null && oldData.getValue() instanceof Number) { + oldValue = ((Number) oldData.getValue()).byteValue(); + } + properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (oldValue | (enabled ? bitmask : 0)))); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java new file mode 100644 index 0000000..bddc0bf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/BooleanProperty.java @@ -0,0 +1,34 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class BooleanProperty extends EntityPropertyImpl { + private final int index; + private final boolean legacy; + private final boolean inverted; + + public BooleanProperty(String name, int index, boolean defaultValue, boolean legacy) { + this(name, index, defaultValue, legacy, false); + } + + public BooleanProperty(String name, int index, boolean defaultValue, boolean legacy, boolean inverted) { + super(name, defaultValue, Boolean.class); + this.index = index; + this.legacy = legacy; + this.inverted = inverted; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean enabled = entity.getProperty(this); + if (inverted) enabled = !enabled; + if (legacy) properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (enabled ? 1 : 0))); + else properties.put(index, newEntityData(index, EntityDataTypes.BOOLEAN, enabled)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java new file mode 100644 index 0000000..0afa31d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CamelSittingProperty.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.pose.EntityPose; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class CamelSittingProperty extends EntityPropertyImpl { + private final int poseIndex; + private final int lastPoseTickIndex; + + public CamelSittingProperty(int poseIndex, int lastPoseTickIndex) { + super("camel_sitting", false, Boolean.class); + this.poseIndex = poseIndex; + this.lastPoseTickIndex = lastPoseTickIndex; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean value = entity.getProperty(this); + if (value) { + properties.put(poseIndex, newEntityData(poseIndex, EntityDataTypes.ENTITY_POSE, EntityPose.SITTING)); + properties.put(lastPoseTickIndex, newEntityData(lastPoseTickIndex, EntityDataTypes.LONG, -1L)); + } else { + properties.put(poseIndex, newEntityData(poseIndex, EntityDataTypes.ENTITY_POSE, EntityPose.STANDING)); + properties.put(lastPoseTickIndex, newEntityData(lastPoseTickIndex, EntityDataTypes.LONG, 0L)); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java new file mode 100644 index 0000000..0d142d2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/CustomTypeProperty.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class CustomTypeProperty extends EntityPropertyImpl { + private final int index; + private final EntityDataType type; + private final TypeDecoder decoder; + + @SuppressWarnings("unchecked") + public CustomTypeProperty(String name, int index, T def, EntityDataType type, TypeDecoder decoder) { + super(name, def, (Class) def.getClass()); + this.index = index; + this.type = type; + this.decoder = decoder; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + properties.put(index, newEntityData(index, type, decoder.decode(entity.getProperty(this)))); + } + + public interface TypeDecoder { + U decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java new file mode 100644 index 0000000..9df06f1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DinnerboneProperty.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.util.adventure.AdventureSerializer; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class DinnerboneProperty extends EntityPropertyImpl { + private final boolean optional; + private final Object serialized; + + public DinnerboneProperty(boolean legacy, boolean optional) { + super("dinnerbone", false, Boolean.class); + this.optional = optional; + Component name = Component.text("Dinnerbone"); + this.serialized = legacy ? AdventureSerializer.serializer().legacy().serialize(name) : + optional ? name : LegacyComponentSerializer.legacySection().serialize(name); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + if (optional) { + properties.put(2, new EntityData<>(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, entity.getProperty(this) ? Optional.of((Component) serialized) : Optional.empty())); + } else { + properties.put(2, new EntityData<>(2, EntityDataTypes.STRING, entity.getProperty(this) ? (String) serialized : "")); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java new file mode 100644 index 0000000..78d3308 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/DummyProperty.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class DummyProperty extends EntityPropertyImpl { + public DummyProperty(String name, T defaultValue) { + this(name, defaultValue, true); + } + + public DummyProperty(String name, Class clazz) { + this(name, clazz, true); + } + + @SuppressWarnings("unchecked") + public DummyProperty(String name, T defaultValue, boolean playerModifiable) { + super(name, defaultValue, (Class) defaultValue.getClass()); + setPlayerModifiable(playerModifiable); + } + + public DummyProperty(String name, Class clazz, boolean playerModifiable) { + super(name, null, clazz); + setPlayerModifiable(playerModifiable); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java new file mode 100644 index 0000000..19fa125 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedByteProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EncodedByteProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final ByteDecoder decoder; + private final int index; + + protected EncodedByteProperty(String name, T defaultValue, Class clazz, int index, ByteDecoder decoder, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.type = type; + } + + @SuppressWarnings("unchecked") + public EncodedByteProperty(String name, T defaultValue, int index, ByteDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, EntityDataTypes.BYTE); + } + + @SuppressWarnings("unchecked") + public EncodedByteProperty(String name, T defaultValue, int index, ByteDecoder decoder, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, type); + } + + public EncodedByteProperty(String name, Class clazz, int index, ByteDecoder decoder) { + this(name, null, clazz, index, decoder, EntityDataTypes.BYTE); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface ByteDecoder { + byte decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java new file mode 100644 index 0000000..896faa1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedIntegerProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EncodedIntegerProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final IntegerDecoder decoder; + private final int index; + + protected EncodedIntegerProperty(String name, T defaultValue, Class clazz, int index, IntegerDecoder decoder, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.type = type; + } + + @SuppressWarnings("unchecked") + public EncodedIntegerProperty(String name, T defaultValue, int index, IntegerDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, EntityDataTypes.INT); + } + + @SuppressWarnings("unchecked") + public EncodedIntegerProperty(String name, T defaultValue, int index, IntegerDecoder decoder, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, type); + } + + public EncodedIntegerProperty(String name, Class clazz, int index, IntegerDecoder decoder) { + this(name, null, clazz, index, decoder, EntityDataTypes.INT); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface IntegerDecoder { + int decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java new file mode 100644 index 0000000..2ebc088 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EncodedStringProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EncodedStringProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final EncodedStringProperty.StringDecoder decoder; + private final int index; + + public EncodedStringProperty(String name, T defaultValue, Class clazz, int index, StringDecoder decoder, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.type = type; + } + + @SuppressWarnings("unchecked") + public EncodedStringProperty(String name, T defaultValue, int index, StringDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, EntityDataTypes.STRING); + } + + @SuppressWarnings("unchecked") + public EncodedStringProperty(String name, T defaultValue, int index, StringDecoder decoder, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, type); + } + + public EncodedStringProperty(String name, Class clazz, int index, StringDecoder decoder) { + this(name, null, clazz, index, decoder, EntityDataTypes.STRING); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface StringDecoder { + String decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java new file mode 100644 index 0000000..a3e6f82 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EntitySittingProperty.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.entity.ArmorStandVehicleProperties; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EntitySittingProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + private final EntityPropertyRegistryImpl propertyRegistry; + + public EntitySittingProperty(PacketFactory packetFactory, EntityPropertyRegistryImpl propertyRegistry) { + super("entity_sitting", false, Boolean.class); + this.packetFactory = packetFactory; + this.propertyRegistry = propertyRegistry; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean sitting = entity.getProperty(this); + if (sitting) { + if (entity.getVehicle() == null) { + PacketEntity vehiclePacketEntity = new PacketEntity(packetFactory, new ArmorStandVehicleProperties(propertyRegistry), + entity.getViewable(), EntityTypes.ARMOR_STAND, entity.getLocation().withY(entity.getLocation().getY() - 0.9)); + entity.setVehicle(vehiclePacketEntity); + } + } else if (entity.getVehicle() != null) { + entity.setVehicle(null); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java new file mode 100644 index 0000000..1bef980 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/EquipmentProperty.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class EquipmentProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + private final EquipmentSlot slot; + + public EquipmentProperty(PacketFactory packetFactory, String name, EquipmentSlot slot) { + super(name, null, ItemStack.class); + this.packetFactory = packetFactory; + this.slot = slot; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + packetFactory.sendEquipment(player, entity, new Equipment(slot, entity.getProperty(this))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java new file mode 100644 index 0000000..56654b5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/ForceBodyRotationProperty.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class ForceBodyRotationProperty extends DummyProperty { + private final TaskScheduler scheduler; + + public ForceBodyRotationProperty(TaskScheduler scheduler) { + super("force_body_rotation", false); + this.scheduler = scheduler; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + if (entity.getProperty(this)) { + scheduler.runLaterAsync(() -> entity.swingHand(player, false), 2L); + scheduler.runLaterAsync(() -> entity.swingHand(player, false), 6L); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java new file mode 100644 index 0000000..87c15bf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/GlowProperty.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NamedColor; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class GlowProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + + public GlowProperty(PacketFactory packetFactory) { + super("glow", null, NamedColor.class); + this.packetFactory = packetFactory; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + NamedColor value = entity.getProperty(this); + EntityData oldData = properties.get(0); +// byte oldValue = oldData == null ? 0 : (byte) oldData.getValue(); + byte oldValue = 0; + if (oldData != null && oldData.getValue() instanceof Number) { + oldValue = ((Number) oldData.getValue()).byteValue(); + } + properties.put(0, newEntityData(0, EntityDataTypes.BYTE, (byte) (oldValue | (value == null ? 0 : 0x40)))); + // the team is already created with the right glow color in the packet factory if the npc isnt spawned yet + if (isSpawned) { + packetFactory.removeTeam(player, entity); + packetFactory.createTeam(player, entity, value); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java new file mode 100644 index 0000000..0e6aede --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HealthProperty.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.attribute.Attributes; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HealthProperty extends EntityPropertyImpl { + private final int index; + + public HealthProperty(int index) { + super("health", 20f, Float.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + float health = entity.getProperty(this); + health = (float) Attributes.MAX_HEALTH.sanitizeValue(health); + properties.put(index, new EntityData<>(index, EntityDataTypes.FLOAT, health)); + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java new file mode 100644 index 0000000..778a2ba --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HologramItemProperty.java @@ -0,0 +1,24 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HologramItemProperty extends EntityPropertyImpl { + + public HologramItemProperty() { + super("holo_item", null, ItemStack.class); + setPlayerModifiable(false); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + properties.put(8, newEntityData(8, EntityDataTypes.ITEMSTACK, entity.getProperty(this))); + properties.put(5, newEntityData(5, EntityDataTypes.BOOLEAN, true)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java new file mode 100644 index 0000000..fed043d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseColorProperty.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.HorseColor; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HorseColorProperty extends EntityPropertyImpl { + private final int index; + + public HorseColorProperty(int index) { + super("horse_color", HorseColor.WHITE, HorseColor.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + HorseColor value = entity.getProperty(this); + int oldValue = (oldData != null && oldData.getValue() instanceof Integer) ? (Integer) oldData.getValue() : 0; + + int newValue = value.ordinal() | (oldValue & 0xFF00); + properties.put(index, newEntityData(index, EntityDataTypes.INT, newValue)); + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java new file mode 100644 index 0000000..ce0acc3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/HorseStyleProperty.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.HorseStyle; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class HorseStyleProperty extends EntityPropertyImpl { + private final int index; + + public HorseStyleProperty(int index) { + super("horse_style", HorseStyle.NONE, HorseStyle.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + HorseStyle value = entity.getProperty(this); + + int oldValue = (oldData != null && oldData.getValue() instanceof Integer) ? (Integer) oldData.getValue() : 0; + int newValue = (oldValue & 0x00FF) | (value.ordinal() << 8); + properties.put(index, newEntityData(index, EntityDataTypes.INT, newValue)); + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java new file mode 100644 index 0000000..47607ad --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/IntegerProperty.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class IntegerProperty extends EntityPropertyImpl { + private final int index; + private final boolean legacy; + + public IntegerProperty(String name, int index, Integer defaultValue) { + this(name, index, defaultValue, false); + } + + public IntegerProperty(String name, int index, Integer defaultValue, boolean legacy) { + super(name, defaultValue, Integer.class); + this.index = index; + this.legacy = legacy; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + properties.put(index, legacy ? + newEntityData(index, EntityDataTypes.BYTE, (byte) entity.getProperty(this).intValue()) : + newEntityData(index, EntityDataTypes.INT, entity.getProperty(this))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java new file mode 100644 index 0000000..3d9f0e7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/LegacyBabyProperty.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class LegacyBabyProperty extends EntityPropertyImpl { + private final int index; + + public LegacyBabyProperty(int index) { + super("baby", false, Boolean.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + boolean isBaby = entity.getProperty(this); + if (entity.getType().equals(EntityTypes.ZOMBIE)) { + properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (isBaby ? 1 : 0))); + } else { + properties.put(index, newEntityData(index, EntityDataTypes.BYTE, (byte) (isBaby ? -1 : 0))); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java new file mode 100644 index 0000000..28bd952 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NBTProperty.java @@ -0,0 +1,60 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class NBTProperty extends EntityPropertyImpl { + private final EntityDataType type; + private final NBTDecoder decoder; + private final int index; + private final boolean allowNull; // This means that the decoder can have null input, not that the property can be null + + public NBTProperty(String name, T defaultValue, Class clazz, int index, NBTDecoder decoder, boolean allowNull, EntityDataType type) { + super(name, defaultValue, clazz); + this.decoder = decoder; + this.index = index; + this.allowNull = allowNull; + this.type = type; + } + + @SuppressWarnings("unchecked") + public NBTProperty(String name, T defaultValue, int index, NBTDecoder decoder, boolean allowNull) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, allowNull, EntityDataTypes.NBT); + } + + @SuppressWarnings("unchecked") + public NBTProperty(String name, T defaultValue, int index, NBTDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, false, EntityDataTypes.NBT); + } + + @SuppressWarnings("unchecked") + public NBTProperty(String name, T defaultValue, int index, NBTDecoder decoder, boolean allowNull, EntityDataType type) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder, allowNull, type); + } + + public NBTProperty(String name, Class clazz, int index, NBTDecoder decoder, boolean allowNull) { + this(name, null, clazz, index, decoder, allowNull, EntityDataTypes.NBT); + } + + public NBTProperty(String name, Class clazz, int index, NBTDecoder decoder) { + this(name, null, clazz, index, decoder, false, EntityDataTypes.NBT); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null && !allowNull) return; + properties.put(index, newEntityData(index, type, decoder.decode(value))); + } + + public interface NBTDecoder { + NBTCompound decode(T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java new file mode 100644 index 0000000..d0e9400 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/NameProperty.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.util.adventure.AdventureSerializer; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class NameProperty extends EntityPropertyImpl { + private final LegacyComponentSerializer legacySerializer; + private final boolean legacySerialization; + private final boolean optional; + + public NameProperty(LegacyComponentSerializer legacySerializer, boolean legacySerialization, boolean optional) { + super("name", null, Component.class); + this.legacySerializer = legacySerializer; + + this.legacySerialization = legacySerialization; + this.optional = optional; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + Component value = entity.getProperty(this); + if (value != null) { + value = PapiUtil.set(legacySerializer, player, value); + if (legacySerialization) { + properties.put(2, newEntityData(2, EntityDataTypes.STRING, AdventureSerializer.serializer().asJson(value))); + } else if (optional) { + properties.put(2, newEntityData(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, Optional.of(value))); + } else { + properties.put(2, newEntityData(2, EntityDataTypes.STRING, LegacyComponentSerializer.legacySection().serialize(value))); + } + } + + if (legacySerialization) properties.put(3, newEntityData(3, EntityDataTypes.BYTE, (byte) (value != null ? 1 : 0))); + else properties.put(3, newEntityData(3, EntityDataTypes.BOOLEAN, value != null)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java new file mode 100644 index 0000000..8b233b2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/OptionalBlockPosProperty.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.Vector3i; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class OptionalBlockPosProperty extends EntityPropertyImpl { + private final int index; + + public OptionalBlockPosProperty(String name, Vector3i defaultValue, int index) { + super(name, defaultValue, Vector3i.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + Vector3i value = entity.getProperty(this); + if (value == null) properties.put(index, new EntityData<>(index, EntityDataTypes.OPTIONAL_BLOCK_POSITION, Optional.empty())); + else properties.put(index, new EntityData<>(index, EntityDataTypes.OPTIONAL_BLOCK_POSITION, + Optional.of(new com.github.retrooper.packetevents.util.Vector3i(value.getX(), value.getY(), value.getZ())))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java new file mode 100644 index 0000000..a5b6ead --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RabbitTypeProperty.java @@ -0,0 +1,53 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.util.adventure.AdventureSerializer; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.RabbitType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Optional; + +public class RabbitTypeProperty extends EntityPropertyImpl { + private final int index; + private final boolean legacyBooleans; + private final boolean optional; + private final Object serialized; + + public RabbitTypeProperty(int index, boolean legacyBooleans, boolean legacyNames, boolean optional) { + super("rabbit_type", RabbitType.BROWN, RabbitType.class); + this.index = index; + this.legacyBooleans = legacyBooleans; + this.optional = optional; + Component name = Component.text("Toast"); + this.serialized = legacyNames ? AdventureSerializer.serializer().legacy().serialize(name) : + optional ? name : LegacyComponentSerializer.legacySection().serialize(name); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + RabbitType rabbitType = entity.getProperty(this); + if (rabbitType == null) return; + if (!rabbitType.equals(RabbitType.TOAST)) { + properties.put(index, legacyBooleans ? + newEntityData(index, EntityDataTypes.BYTE, (byte) rabbitType.getId()) : + newEntityData(index, EntityDataTypes.INT, rabbitType.getId())); + if (optional) { + properties.put(2, new EntityData<>(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, Optional.empty())); + } else { + properties.put(2, new EntityData<>(2, EntityDataTypes.STRING, "")); + } + } else { + if (optional) { + properties.put(2, newEntityData(2, EntityDataTypes.OPTIONAL_ADV_COMPONENT, Optional.of((Component) serialized))); + } else { + properties.put(2, newEntityData(2, EntityDataTypes.STRING, (String) serialized)); + } + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java new file mode 100644 index 0000000..2754a2d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/RotationProperty.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.Vector3f; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class RotationProperty extends EntityPropertyImpl { + private final int index; + + public RotationProperty(String name, int index, Vector3f defaultValue) { + super(name, defaultValue, Vector3f.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + Vector3f vec = entity.getProperty(this); + properties.put(index, newEntityData(index, EntityDataTypes.ROTATION, new com.github.retrooper.packetevents.util.Vector3f(vec.getX(), vec.getY(), vec.getZ()))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java new file mode 100644 index 0000000..5fc53c4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TargetNpcProperty.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class TargetNpcProperty extends EntityPropertyImpl { + private final int index; + + public TargetNpcProperty(String name, int index, NpcEntryImpl defaultValue) { + super(name, defaultValue, NpcEntryImpl.class); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + NpcEntryImpl value = entity.getProperty(this); + if (value == null) return; + if (value.getNpc().getEntity().getEntityId() == entity.getEntityId()) return; + if (value.getNpc().isVisibleTo(player)) { + properties.put(index, newEntityData(index, EntityDataTypes.INT, value.getNpc().getEntity().getEntityId())); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java new file mode 100644 index 0000000..aa83c15 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/TropicalFishVariantProperty.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.entity.properties; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.TropicalFishVariant; +import org.bukkit.entity.Player; + +import java.util.Map; + +public class TropicalFishVariantProperty extends EntityPropertyImpl { + private final int index; + private final BuilderDecoder decoder; + + public TropicalFishVariantProperty(String name, T defaultValue, Class type, int index, BuilderDecoder decoder) { + super(name, defaultValue, type); + this.index = index; + this.decoder = decoder; + } + + @SuppressWarnings("unchecked") + public TropicalFishVariantProperty(String name, T defaultValue, int index, BuilderDecoder decoder) { + this(name, defaultValue, (Class) defaultValue.getClass(), index, decoder); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + T value = entity.getProperty(this); + if (value == null) return; + + EntityData oldData = properties.get(index); + TropicalFishVariant.Builder builder; + if (oldData != null && oldData.getType() == EntityDataTypes.INT && oldData.getValue() instanceof Integer) { + int oldVal = (Integer) oldData.getValue(); + builder = TropicalFishVariant.Builder.fromInt(oldVal); + } else { + builder = new TropicalFishVariant.Builder(); + } + builder = decoder.decode(builder, value); + int variant = builder.build().getVariant(); + properties.put(index, newEntityData(index, EntityDataTypes.INT, variant)); + } + + public interface BuilderDecoder { + TropicalFishVariant.Builder decode(TropicalFishVariant.Builder builder, T obj); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java new file mode 100644 index 0000000..e1aba38 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/attributes/AttributeProperty.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.entity.properties.attributes; + +import com.github.retrooper.packetevents.protocol.attribute.Attribute; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import org.bukkit.entity.Player; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class AttributeProperty extends EntityPropertyImpl { + private final PacketFactory packetFactory; + private final Attribute attribute; + + public AttributeProperty(PacketFactory packetFactory, String name, Attribute attribute) { + super(name, attribute.getDefaultValue(), Double.class); + this.packetFactory = packetFactory; + this.attribute = attribute; + } + + public double getMinValue() { + return attribute.getMinValue(); + } + + public double getMaxValue() { + return attribute.getMaxValue(); + } + + public double sanitizeValue(double value) { + return attribute.sanitizeValue(value); + } + + @Override + public List> applyStandalone(Player player, PacketEntity packetEntity, boolean isSpawned) { + apply(player, packetEntity, isSpawned, Collections.emptyList()); + return Collections.emptyList(); + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + } + + public void apply(Player player, PacketEntity entity, boolean isSpawned, List properties) { + Double value = entity.getProperty(this); + if (value == null) { + return; + } + value = attribute.sanitizeValue(value); + if (isSpawned) { + packetFactory.sendAttribute(player, entity, new WrapperPlayServerUpdateAttributes.Property(attribute, value, Collections.emptyList())); + } else { + properties.add(new WrapperPlayServerUpdateAttributes.Property(attribute, value, Collections.emptyList())); + } + } + + public Attribute getAttribute() { + return attribute; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java new file mode 100644 index 0000000..36d55ad --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerDataProperty.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import com.github.retrooper.packetevents.protocol.entity.villager.profession.VillagerProfessions; +import com.github.retrooper.packetevents.protocol.entity.villager.type.VillagerTypes; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import org.bukkit.entity.Player; + +import java.util.Map; + +public abstract class VillagerDataProperty extends EntityPropertyImpl { + private final int index; + + @SuppressWarnings("unchecked") + public VillagerDataProperty(String name, int index, T def) { + super(name, def, (Class) def.getClass()); + this.index = index; + } + + @Override + public void apply(Player player, PacketEntity entity, boolean isSpawned, Map> properties) { + EntityData oldData = properties.get(index); + VillagerData old = oldData == null ? new VillagerData(VillagerTypes.PLAINS, VillagerProfessions.NONE, 1) : (VillagerData) oldData.getValue(); + properties.put(index, newEntityData(index, EntityDataTypes.VILLAGER_DATA, apply(old, entity.getProperty(this)))); + } + + protected abstract VillagerData apply(VillagerData data, T value); +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java new file mode 100644 index 0000000..af81f40 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerLevelProperty.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import lol.pyr.znpcsplus.util.VillagerLevel; + +public class VillagerLevelProperty extends VillagerDataProperty { + public VillagerLevelProperty(String name, int index, VillagerLevel def) { + super(name, index, def); + } + + @Override + protected VillagerData apply(VillagerData data, VillagerLevel value) { + data.setLevel(value.ordinal() + 1); + return data; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java new file mode 100644 index 0000000..6571676 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerProfessionProperty.java @@ -0,0 +1,17 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import com.github.retrooper.packetevents.protocol.entity.villager.profession.VillagerProfessions; +import lol.pyr.znpcsplus.util.VillagerProfession; + +public class VillagerProfessionProperty extends VillagerDataProperty { + public VillagerProfessionProperty(String name, int index, VillagerProfession def) { + super(name, index, def); + } + + @Override + protected VillagerData apply(VillagerData data, VillagerProfession value) { + data.setProfession(VillagerProfessions.getById(value.getId())); + return data; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java new file mode 100644 index 0000000..a3d2448 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/properties/villager/VillagerTypeProperty.java @@ -0,0 +1,17 @@ +package lol.pyr.znpcsplus.entity.properties.villager; + +import com.github.retrooper.packetevents.protocol.entity.villager.VillagerData; +import com.github.retrooper.packetevents.protocol.entity.villager.type.VillagerTypes; +import lol.pyr.znpcsplus.util.VillagerType; + +public class VillagerTypeProperty extends VillagerDataProperty { + public VillagerTypeProperty(String name, int index, VillagerType def) { + super(name, index, def); + } + + @Override + protected VillagerData apply(VillagerData data, VillagerType value) { + data.setType(VillagerTypes.getById(value.getId())); + return data; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java new file mode 100644 index 0000000..c29f9b2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BlockStatePropertySerializer.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.BlockState; + +public class BlockStatePropertySerializer implements PropertySerializer { + @Override + public String serialize(BlockState property) { + return String.valueOf(property.getGlobalId()); + } + + @Override + public BlockState deserialize(String property) { + try { + int id = Integer.parseInt(property); + return new BlockState(id); + } catch (Exception e) { + e.printStackTrace(); + } + return new BlockState(0); + } + + @Override + public Class getTypeClass() { + return BlockState.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java new file mode 100644 index 0000000..149bcdb --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/BooleanPropertySerializer.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; + +public class BooleanPropertySerializer implements PropertySerializer { + @Override + public String serialize(Boolean property) { + return String.valueOf(property); + } + + @Override + public Boolean deserialize(String property) { + return Boolean.valueOf(property); + } + + @Override + public Class getTypeClass() { + return Boolean.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java new file mode 100644 index 0000000..e0946a6 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ColorPropertySerializer.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import org.bukkit.Color; + +public class ColorPropertySerializer implements PropertySerializer { + @Override + public String serialize(Color property) { + return String.valueOf(property.asRGB()); + } + + @Override + public Color deserialize(String property) { + return Color.fromRGB(Integer.parseInt(property)); + } + + @Override + public Class getTypeClass() { + return Color.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java new file mode 100644 index 0000000..c7e3f75 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ComponentPropertySerializer.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class ComponentPropertySerializer implements PropertySerializer { + @Override + public String serialize(Component property) { + return Base64.getEncoder().encodeToString(MiniMessage.miniMessage().serialize(property).getBytes(StandardCharsets.UTF_8)); + } + + @Override + public Component deserialize(String property) { + return MiniMessage.miniMessage().deserialize(new String(Base64.getDecoder().decode(property), StandardCharsets.UTF_8)); + } + + @Override + public Class getTypeClass() { + return Component.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java new file mode 100644 index 0000000..3ad3a5d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/GenericSerializer.java @@ -0,0 +1,32 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; + +import java.util.function.Function; + +public class GenericSerializer implements PropertySerializer { + private final Function encoder; + private final Function decoder; + private final Class typeClass; + + public GenericSerializer(Function encoder, Function decoder, Class typeClass) { + this.encoder = encoder; + this.decoder = decoder; + this.typeClass = typeClass; + } + + @Override + public String serialize(T property) { + return encoder.apply(property); + } + + @Override + public T deserialize(String property) { + return decoder.apply(property); + } + + @Override + public Class getTypeClass() { + return typeClass; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java new file mode 100644 index 0000000..c6f1cb5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/ItemStackPropertySerializer.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.ItemSerializationUtil; + +public class ItemStackPropertySerializer implements PropertySerializer { + @Override + public String serialize(ItemStack property) { + return ItemSerializationUtil.itemToB64(SpigotConversionUtil.toBukkitItemStack(property)); + } + + @Override + public ItemStack deserialize(String property) { + return SpigotConversionUtil.fromBukkitItemStack(ItemSerializationUtil.itemFromB64(property)); + } + + @Override + public Class getTypeClass() { + return ItemStack.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java new file mode 100644 index 0000000..0d06d9a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/LookTypeSerializer.java @@ -0,0 +1,26 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.LookType; + +public class LookTypeSerializer implements PropertySerializer { + @Override + public String serialize(LookType property) { + return property.name(); + } + + @Override + public LookType deserialize(String property) { + if (property.equals("true")) return LookType.CLOSEST_PLAYER; + try { + return LookType.valueOf(property); + } catch (IllegalArgumentException ignored) { + return LookType.FIXED; + } + } + + @Override + public Class getTypeClass() { + return LookType.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java new file mode 100644 index 0000000..a227ae7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/NamedColorPropertySerializer.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.NamedColor; + +public class NamedColorPropertySerializer implements PropertySerializer { + @Override + public String serialize(NamedColor property) { + return property.name(); + } + + @Override + public NamedColor deserialize(String property) { + try { + return NamedColor.valueOf(property.toUpperCase()); + } catch (IllegalArgumentException exception) { + return NamedColor.WHITE; + } + } + + @Override + public Class getTypeClass() { + return NamedColor.class; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java new file mode 100644 index 0000000..df59038 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/SkinDescriptorSerializer.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; + +public class SkinDescriptorSerializer implements PropertySerializer { + private final MojangSkinCache skinCache; + + public SkinDescriptorSerializer(MojangSkinCache skinCache) { + this.skinCache = skinCache; + } + + @Override + public String serialize(SkinDescriptor property) { + return ((BaseSkinDescriptor) property).serialize(); + } + + @Override + public SkinDescriptor deserialize(String property) { + return BaseSkinDescriptor.deserialize(skinCache, property); + } + + @Override + public Class getTypeClass() { + return SkinDescriptor.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java new file mode 100644 index 0000000..7cd3654 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/TargetNpcPropertySerializer.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; + +public class TargetNpcPropertySerializer implements PropertySerializer { + @Override + public String serialize(NpcEntryImpl property) { + return property.getId(); + } + + @Override + public NpcEntryImpl deserialize(String property) { + return null; // TODO: find a way to do this + } + + @Override + public Class getTypeClass() { + return NpcEntryImpl.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java new file mode 100644 index 0000000..efecbd2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/entity/serializers/Vector3fPropertySerializer.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.entity.serializers; + +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.util.Vector3f; + +public class Vector3fPropertySerializer implements PropertySerializer { + + @Override + public String serialize(Vector3f property) { + return property.toString(); + } + + @Override + public Vector3f deserialize(String property) { + return new Vector3f(property); + } + + @Override + public Class getTypeClass() { + return Vector3f.class; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java new file mode 100644 index 0000000..c0194c7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramImpl.java @@ -0,0 +1,197 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.hologram.Hologram; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.FutureUtil; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class HologramImpl extends Viewable implements Hologram { + private final ConfigManager configManager; + private final PacketFactory packetFactory; + private final LegacyComponentSerializer textSerializer; + private final EntityPropertyRegistryImpl propertyRegistry; + + private double offset = 0.0; + private long refreshDelay = -1; + private long lastRefresh = System.currentTimeMillis(); + private NpcLocation location; + private final List> lines = new ArrayList<>(); + + public HologramImpl(EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, NpcLocation location) { + this.propertyRegistry = propertyRegistry; + this.configManager = configManager; + this.packetFactory = packetFactory; + this.textSerializer = textSerializer; + this.location = location; + } + + public void addTextLineComponent(Component line) { + HologramText newLine = new HologramText(this, propertyRegistry, packetFactory, null, line); + lines.add(newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void addTextLine(String line) { + Component component = line.contains("§") ? Component.text(line) : MiniMessage.miniMessage().deserialize(line); + addTextLineComponent(textSerializer.deserialize(textSerializer.serialize(component))); + } + + public void addItemLineStack(org.bukkit.inventory.ItemStack item) { + addItemLinePEStack(SpigotConversionUtil.fromBukkitItemStack(item)); + } + + public void addItemLine(String serializedItem) { + addItemLinePEStack(HologramItem.deserialize(serializedItem)); + } + + public void addItemLinePEStack(ItemStack item) { + HologramItem newLine = new HologramItem(this, propertyRegistry, packetFactory, null, item); + lines.add(newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void addLine(String line) { + if (line.toLowerCase().startsWith("item:")) { + addItemLine(line.substring(5)); + } else { + addTextLine(line); + } + } + + public Component getLineTextComponent(int index) { + return ((HologramText) lines.get(index)).getValue(); + } + + public String getLine(int index) { + if (lines.get(index) instanceof HologramItem) { + return ((HologramItem) lines.get(index)).serialize(); + } else { + return textSerializer.serialize(getLineTextComponent(index)); + } + } + + public void removeLine(int index) { + HologramLine line = lines.remove(index); + for (Player viewer : getViewers()) line.hide(viewer); + relocateLines(); + } + + public List> getLines() { + return Collections.unmodifiableList(lines); + } + + public void clearLines() { + UNSAFE_hideAll(); + lines.clear(); + } + + public void insertTextLineComponent(int index, Component line) { + HologramText newLine = new HologramText(this, propertyRegistry, packetFactory, null, line); + lines.add(index, newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void insertTextLine(int index, String line) { + insertTextLineComponent(index, textSerializer.deserialize(textSerializer.serialize(MiniMessage.miniMessage().deserialize(line)))); + } + + public void insertItemLineStack(int index, org.bukkit.inventory.ItemStack item) { + insertItemLinePEStack(index, SpigotConversionUtil.fromBukkitItemStack(item)); + } + + public void insertItemLinePEStack(int index, ItemStack item) { + HologramItem newLine = new HologramItem(this, propertyRegistry, packetFactory, null, item); + lines.add(index, newLine); + relocateLines(); + for (Player viewer : getViewers()) newLine.show(viewer.getPlayer()); + } + + public void insertItemLine(int index, String item) { + insertItemLinePEStack(index, HologramItem.deserialize(item)); + } + + public void insertLine(int index, String line) { + if (line.toLowerCase().startsWith("item:")) { + insertItemLine(index, line.substring(5)); + } else { + insertTextLine(index, line); + } + } + + @Override + public int lineCount() { + return lines.size(); + } + + @Override + protected CompletableFuture UNSAFE_show(Player player) { + return FutureUtil.allOf(lines.stream() + .map(line -> line.show(player)) + .collect(Collectors.toList())); + } + + @Override + protected void UNSAFE_hide(Player player) { + for (HologramLine line : lines) line.hide(player); + } + + @Override + public long getRefreshDelay() { + return refreshDelay; + } + + @Override + public void setRefreshDelay(long refreshDelay) { + this.refreshDelay = refreshDelay; + } + + public boolean shouldRefresh() { + return refreshDelay != -1 && (System.currentTimeMillis() - lastRefresh) > refreshDelay; + } + + public void refresh() { + lastRefresh = System.currentTimeMillis(); + for (HologramLine line : lines) for (Player viewer : getViewers()) line.refreshMeta(viewer); + } + + public void setLocation(NpcLocation location) { + this.location = location; + relocateLines(); + } + + private void relocateLines() { + final double lineSpacing = configManager.getConfig().lineSpacing(); + double height = location.getY() + (lines.size() - 1) * lineSpacing + getOffset(); + for (HologramLine line : lines) { + line.setLocation(location.withY(height)); + height -= lineSpacing; + } + } + + public void setOffset(double offset) { + this.offset = offset; + relocateLines(); + } + + public double getOffset() { + return offset; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java new file mode 100644 index 0000000..da17da4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramItem.java @@ -0,0 +1,109 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.item.type.ItemType; +import com.github.retrooper.packetevents.protocol.item.type.ItemTypes; +import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; +import com.github.retrooper.packetevents.protocol.nbt.NBTInt; +import com.github.retrooper.packetevents.protocol.nbt.NBTNumber; +import com.github.retrooper.packetevents.protocol.nbt.codec.NBTCodec; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; + +public class HologramItem extends HologramLine { + public HologramItem(Viewable viewable, EntityPropertyRegistryImpl propertyRegistry, PacketFactory packetFactory, NpcLocation location, ItemStack item) { + super(viewable, item, packetFactory, EntityTypes.ITEM, location); + addProperty(propertyRegistry.getByName("holo_item")); + } + + @SuppressWarnings("unchecked") + @Override + public T getProperty(EntityProperty key) { + if (key.getName().equalsIgnoreCase("holo_item")) return (T) getValue(); + return super.getProperty(key); + } + + @Override + public void setLocation(NpcLocation location) { + super.setLocation(location.withY(location.getY() + 2.05)); + } + + public static boolean ensureValidItemInput(String in) { + if (in == null || in.isEmpty()) { + return false; + } + + int indexOfNbt = in.indexOf("{"); + if (indexOfNbt != -1) { + String typeName = in.substring(0, indexOfNbt); + ItemType type = ItemTypes.getByName("minecraft:" + typeName.toLowerCase()); + if (type == null) { + return false; + } + String nbtString = in.substring(indexOfNbt); + return ensureValidNbt(nbtString); + } else { + ItemType type = ItemTypes.getByName("minecraft:" + in.toLowerCase()); + return type != null; + } + } + + private static boolean ensureValidNbt(String nbtString) { + JsonElement nbtJson; + try { + nbtJson = JsonParser.parseString(nbtString); + } catch (JsonSyntaxException e) { + return false; + } + try { + NBTCodec.jsonToNBT(nbtJson); + } catch (Exception ignored) { + return false; + } + return true; + } + + public static ItemStack deserialize(String serializedItem) { + int indexOfNbt = serializedItem.indexOf("{"); + String typeName = serializedItem; + int amount = 1; + NBTCompound nbt = new NBTCompound(); + if (indexOfNbt != -1) { + typeName = serializedItem.substring(0, indexOfNbt); + String nbtString = serializedItem.substring(indexOfNbt); + JsonElement nbtJson = null; + try { + nbtJson = JsonParser.parseString(nbtString); + } catch (Exception ignored) { + } + if (nbtJson != null) { + nbt = (NBTCompound) NBTCodec.jsonToNBT(nbtJson); + NBTNumber nbtAmount = nbt.getNumberTagOrNull("Count"); + if (nbtAmount != null) { + nbt.removeTag("Count"); + amount = nbtAmount.getAsInt(); + if (amount <= 0) amount = 1; + if (amount > 127) amount = 127; + } + } + } + ItemType type = ItemTypes.getByName("minecraft:" + typeName.toLowerCase()); + if (type == null) type = ItemTypes.STONE; + return ItemStack.builder().type(type).amount(amount).nbt(nbt).build(); + } + + public String serialize() { + NBTCompound nbt = getValue().getNBT(); + if (nbt == null) nbt = new NBTCompound(); + if (getValue().getAmount() > 1) nbt.setTag("Count", new NBTInt(getValue().getAmount())); + if (nbt.isEmpty()) return "item:" + getValue().getType().getName().toString().replace("minecraft:", ""); + return "item:" + getValue().getType().getName().toString().replace("minecraft:", "") + NBTCodec.nbtToJson(nbt, true); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java new file mode 100644 index 0000000..553a5e2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramLine.java @@ -0,0 +1,91 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class HologramLine implements PropertyHolder { + private M value; + private final PacketEntity entity; + private final Set> properties; + + public HologramLine(Viewable viewable, M value, PacketFactory packetFactory, EntityType type, NpcLocation location) { + this.value = value; + this.entity = new PacketEntity(packetFactory, this, viewable, type, location); + this.properties = new HashSet<>(); + } + + public M getValue() { + return value; + } + + public void setValue(M value) { + this.value = value; + } + + public void refreshMeta(Player player) { + entity.refreshMeta(player); + } + + protected CompletableFuture show(Player player) { + return entity.spawn(player); + } + + protected void hide(Player player) { + entity.despawn(player); + } + + public void setLocation(NpcLocation location) { + entity.setLocation(location); + } + + public int getEntityId() { + return entity.getEntityId(); + } + + public void addProperty(EntityProperty property) { + properties.add(property); + } + + @Override + public T getProperty(EntityProperty key) { + return key.getDefaultValue(); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return properties.contains(key); + } + + @Override + public void setProperty(EntityProperty key, T value) { + throw new UnsupportedOperationException("Can't set properties on a hologram line"); + } + + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + throw new UnsupportedOperationException("Can't set properties on a hologram line"); + } + + @SuppressWarnings("unchecked") + @Override + public ItemStack getItemProperty(EntityProperty key) { + return SpigotConversionUtil.toBukkitItemStack(((EntityProperty) key).getDefaultValue()); + } + + @Override + public Set> getAppliedProperties() { + return properties; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java new file mode 100644 index 0000000..23f3428 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/hologram/HologramText.java @@ -0,0 +1,42 @@ +package lol.pyr.znpcsplus.hologram; + +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class HologramText extends HologramLine { + + private static final Component BLANK = Component.text("%blank%"); + + public HologramText(Viewable viewable, EntityPropertyRegistryImpl propertyRegistry, PacketFactory packetFactory, NpcLocation location, Component text) { + super(viewable, text, packetFactory, EntityTypes.ARMOR_STAND, location); + addProperty(propertyRegistry.getByName("name")); + addProperty(propertyRegistry.getByName("invisible")); + } + + @Override + public CompletableFuture show(Player player) { + if (getValue().equals(BLANK)) return CompletableFuture.completedFuture(null); + return super.show(player); + } + + @SuppressWarnings("unchecked") + @Override + public T getProperty(EntityProperty key) { + if (key.getName().equalsIgnoreCase("invisible")) return (T) Boolean.TRUE; + if (key.getName().equalsIgnoreCase("name")) return (T) getValue(); + return super.getProperty(key); + } + + @Override + public boolean hasProperty(EntityProperty key) { + return key.getName().equalsIgnoreCase("name") || key.getName().equalsIgnoreCase("invisible"); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java new file mode 100644 index 0000000..1e58967 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionFactoryImpl.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.znpcsplus.api.interaction.ActionFactory; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandAction; +import lol.pyr.znpcsplus.interaction.message.MessageAction; +import lol.pyr.znpcsplus.interaction.playerchat.PlayerChatAction; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandAction; +import lol.pyr.znpcsplus.interaction.switchserver.SwitchServerAction; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.BungeeConnector; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +public class ActionFactoryImpl implements ActionFactory { + private final TaskScheduler scheduler; + private final BukkitAudiences adventure; + private final LegacyComponentSerializer textSerializer; + private final BungeeConnector bungeeConnector; + + public ActionFactoryImpl(TaskScheduler scheduler, BukkitAudiences adventure, LegacyComponentSerializer textSerializer, BungeeConnector bungeeConnector) { + this.scheduler = scheduler; + this.adventure = adventure; + this.textSerializer = textSerializer; + this.bungeeConnector = bungeeConnector; + } + + public InteractionAction createConsoleCommandAction(String command, InteractionType interactionType, long cooldown, long delay) { + return new ConsoleCommandAction(this.scheduler, command, interactionType, cooldown, delay); + } + + public InteractionAction createMessageAction(String message, InteractionType interactionType, long cooldown, long delay) { + return new MessageAction(this.adventure, textSerializer, message, interactionType, cooldown, delay); + } + + public InteractionAction createPlayerChatAction(String message, InteractionType interactionType, long cooldown, long delay) { + return new PlayerChatAction(this.scheduler, message, interactionType, cooldown, delay); + } + + public InteractionAction createPlayerCommandAction(String command, InteractionType interactionType, long cooldown, long delay) { + return new PlayerCommandAction(this.scheduler, command, interactionType, cooldown, delay); + } + + public InteractionAction createSwitchServerAction(String server, InteractionType interactionType, long cooldown, long delay) { + return new SwitchServerAction(bungeeConnector, server, interactionType, cooldown, delay); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java new file mode 100644 index 0000000..b08ae1c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/ActionRegistryImpl.java @@ -0,0 +1,65 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.znpcsplus.api.interaction.*; +import lol.pyr.znpcsplus.interaction.consolecommand.ConsoleCommandActionType; +import lol.pyr.znpcsplus.interaction.message.MessageActionType; +import lol.pyr.znpcsplus.interaction.playerchat.PlayerChatActionType; +import lol.pyr.znpcsplus.interaction.playercommand.PlayerCommandActionType; +import lol.pyr.znpcsplus.interaction.switchserver.SwitchServerActionType; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.BungeeConnector; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ActionRegistryImpl implements ActionRegistry { + private final Map, InteractionActionType> serializerMap = new HashMap<>(); + + public void registerTypes(TaskScheduler taskScheduler, BukkitAudiences adventure, LegacyComponentSerializer textSerializer, BungeeConnector bungeeConnector) { + register(new ConsoleCommandActionType(taskScheduler)); + register(new PlayerCommandActionType(taskScheduler)); + register(new SwitchServerActionType(bungeeConnector)); + register(new MessageActionType(adventure, textSerializer)); + register(new PlayerChatActionType(taskScheduler)); + } + + public void register(InteractionActionType type) { + serializerMap.put(type.getActionClass(), type); + } + + public void unregister(Class clazz) { + serializerMap.remove(clazz); + } + + public List getCommands() { + return serializerMap.values().stream() + .filter(type -> type instanceof InteractionCommandHandler) + .map(type -> (InteractionCommandHandler) type) + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + public T deserialize(String str) { + try { + String[] split = str.split(";"); + Class clazz = Class.forName(split[0]); + InteractionActionType serializer = (InteractionActionType) serializerMap.get(clazz); + if (serializer == null) return null; + return serializer.deserialize(String.join(";", Arrays.copyOfRange(split, 1, split.length))); + } catch (ClassNotFoundException e) { + return null; + } + } + + @SuppressWarnings("unchecked") + public String serialize(T action) { + InteractionActionType serializer = (InteractionActionType) serializerMap.get(action.getClass()); + if (serializer == null) return null; + return action.getClass().getName() + ";" + serializer.serialize(action); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java new file mode 100644 index 0000000..ed7539e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionActionImpl.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import net.kyori.adventure.text.Component; + +public abstract class InteractionActionImpl extends InteractionAction { + protected InteractionActionImpl(long cooldown, long delay, InteractionType interactionType) { + super(cooldown, delay, interactionType); + } + + public abstract Component getInfo(String id, int index, CommandContext context); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java new file mode 100644 index 0000000..93d1fbc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionCommandHandler.java @@ -0,0 +1,25 @@ +package lol.pyr.znpcsplus.interaction; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.command.CommandHandler; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +public interface InteractionCommandHandler extends CommandHandler { + String getSubcommandName(); + + InteractionAction parse(CommandContext context) throws CommandExecutionException; + void appendUsage(CommandContext context); + + @Override + default void run(CommandContext context) throws CommandExecutionException { + appendUsage(context); + NpcImpl npc = context.parse(NpcEntryImpl.class).getNpc(); + npc.addAction(parse(context)); + context.send(Component.text("Added action to npc", NamedTextColor.GREEN)); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java new file mode 100644 index 0000000..8734c0e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/InteractionPacketListener.java @@ -0,0 +1,87 @@ +package lol.pyr.znpcsplus.interaction; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketListener; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.item.ItemStack; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.EquipmentSlot; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEquipment; +import lol.pyr.znpcsplus.api.event.NpcInteractEvent; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.user.User; +import lol.pyr.znpcsplus.user.UserManager; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Collections; + +public class InteractionPacketListener implements PacketListener { + private final UserManager userManager; + private final NpcRegistryImpl npcRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final TaskScheduler scheduler; + + public InteractionPacketListener(UserManager userManager, NpcRegistryImpl npcRegistry, NpcTypeRegistryImpl typeRegistry, TaskScheduler scheduler) { + this.userManager = userManager; + this.npcRegistry = npcRegistry; + this.typeRegistry = typeRegistry; + this.scheduler = scheduler; + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + if (event.getPacketType() != PacketType.Play.Client.INTERACT_ENTITY) return; + Player player = (Player) event.getPlayer(); + if (player == null) return; + + WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event); + + NpcEntryImpl entry = npcRegistry.getByEntityId(packet.getEntityId()); + if (entry == null || !entry.isProcessed()) return; + NpcImpl npc = entry.getNpc(); + + if ((packet.getAction().equals(WrapperPlayClientInteractEntity.InteractAction.INTERACT) + || packet.getAction().equals(WrapperPlayClientInteractEntity.InteractAction.INTERACT_AT)) + && npc.getType().equals(typeRegistry.getByName("allay"))) { + PacketEvents.getAPI().getPlayerManager().sendPacket(player, + new WrapperPlayServerEntityEquipment(packet.getEntityId(), Collections.singletonList( + new Equipment(EquipmentSlot.MAIN_HAND, ItemStack.EMPTY)))); + player.updateInventory(); + } + + InteractionType type = wrapClickType(packet.getAction()); + + User user = userManager.get(player); + if (!user.canInteract()) return; + + NpcInteractEvent interactEvent = new NpcInteractEvent(player, entry, type); + Bukkit.getPluginManager().callEvent(interactEvent); + if (interactEvent.isCancelled()) return; + + for (InteractionAction action : npc.getActions()) { + if (action.getInteractionType() != InteractionType.ANY_CLICK && action.getInteractionType() != type) continue; + if (action.getCooldown() > 0 && !user.actionCooldownCheck(action)) continue; + scheduler.runLaterAsync(() -> action.run(player), action.getDelay()); + } + } + + private InteractionType wrapClickType(WrapperPlayClientInteractEntity.InteractAction action) { + switch (action) { + case ATTACK: + return InteractionType.LEFT_CLICK; + case INTERACT: + case INTERACT_AT: + return InteractionType.RIGHT_CLICK; + } + throw new IllegalStateException(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java new file mode 100644 index 0000000..1b0b040 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandAction.java @@ -0,0 +1,55 @@ +package lol.pyr.znpcsplus.interaction.consolecommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class ConsoleCommandAction extends InteractionActionImpl { + private final TaskScheduler scheduler; + private final String command; + + public ConsoleCommandAction(TaskScheduler scheduler, String command, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.scheduler = scheduler; + this.command = command; + } + + @Override + public void run(Player player) { + String cmd = command.replace("{player}", player.getName()).replace("{uuid}", player.getUniqueId().toString()); + scheduler.runSyncGlobal(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), PapiUtil.set(player, cmd))); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " consolecommand " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + command)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Console Command: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(command, NamedTextColor.WHITE))); + } + + public String getCommand() { + return command; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java new file mode 100644 index 0000000..931256c --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/consolecommand/ConsoleCommandActionType.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.interaction.consolecommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class ConsoleCommandActionType implements InteractionActionType, InteractionCommandHandler { + private final TaskScheduler scheduler; + + public ConsoleCommandActionType(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public String serialize(ConsoleCommandAction obj) { + return Base64.getEncoder().encodeToString(obj.getCommand().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public ConsoleCommandAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new ConsoleCommandAction(scheduler, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return ConsoleCommandAction.class; + } + + @Override + public String getSubcommandName() { + return "consolecommand"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String command = context.dumpAllArgs(); + return new ConsoleCommandAction(scheduler, command, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java new file mode 100644 index 0000000..051bea5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageAction.java @@ -0,0 +1,58 @@ +package lol.pyr.znpcsplus.interaction.message; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; + +public class MessageAction extends InteractionActionImpl { + private final BukkitAudiences adventure; + private final String message; + private final LegacyComponentSerializer textSerializer; + + public MessageAction(BukkitAudiences adventure, LegacyComponentSerializer textSerializer, String message, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.adventure = adventure; + this.message = message; + this.textSerializer = textSerializer; + } + + @Override + public void run(Player player) { + String msg = message.replace("{player}", player.getName()) + .replace("{uuid}", player.getUniqueId().toString()); + adventure.player(player).sendMessage(textSerializer.deserialize(PapiUtil.set(player, msg))); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " message " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + message)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Message: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(message, NamedTextColor.WHITE))); + } + + public String getMessage() { + return message; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java new file mode 100644 index 0000000..54dbfae --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/message/MessageActionType.java @@ -0,0 +1,69 @@ +package lol.pyr.znpcsplus.interaction.message; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class MessageActionType implements InteractionActionType, InteractionCommandHandler { + private final BukkitAudiences adventure; + private final LegacyComponentSerializer textSerializer; + + public MessageActionType(BukkitAudiences adventure, LegacyComponentSerializer textSerializer) { + this.adventure = adventure; + this.textSerializer = textSerializer; + } + + @Override + public String serialize(MessageAction obj) { + return Base64.getEncoder().encodeToString(obj.getMessage().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public MessageAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new MessageAction(adventure, textSerializer, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return MessageAction.class; + } + + @Override + public String getSubcommandName() { + return "message"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String message = context.dumpAllArgs(); + return new MessageAction(adventure, textSerializer, message, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java new file mode 100644 index 0000000..ab3ffe0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatAction.java @@ -0,0 +1,53 @@ +package lol.pyr.znpcsplus.interaction.playerchat; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class PlayerChatAction extends InteractionActionImpl { + private final String message; + private final TaskScheduler scheduler; + + public PlayerChatAction(TaskScheduler scheduler, String message, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.message = message; + this.scheduler = scheduler; + } + + @Override + public void run(Player player) { + scheduler.schedulePlayerChat(player, message.replace("{player}", player.getName()) + .replace("{uuid}", player.getUniqueId().toString())); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " playerchat " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + message)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Player Chat: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(message, NamedTextColor.WHITE))); + } + + public String getMessage() { + return message; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java new file mode 100644 index 0000000..99a292d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playerchat/PlayerChatActionType.java @@ -0,0 +1,65 @@ +package lol.pyr.znpcsplus.interaction.playerchat; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class PlayerChatActionType implements InteractionActionType, InteractionCommandHandler { + private final TaskScheduler scheduler; + + public PlayerChatActionType(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public String serialize(PlayerChatAction obj) { + return Base64.getEncoder().encodeToString(obj.getMessage().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public PlayerChatAction deserialize(String str) { + String[] split = str.split(";"); + return new PlayerChatAction(scheduler, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), InteractionType.valueOf(split[2]), Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return PlayerChatAction.class; + } + + @Override + public String getSubcommandName() { + return "playerchat"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionAction parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String message = context.dumpAllArgs(); + return new PlayerChatAction(scheduler, message, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java new file mode 100644 index 0000000..65ab64f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandAction.java @@ -0,0 +1,54 @@ +package lol.pyr.znpcsplus.interaction.playercommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.PapiUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class PlayerCommandAction extends InteractionActionImpl { + private final TaskScheduler scheduler; + private final String command; + + public PlayerCommandAction(TaskScheduler scheduler, String command, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.scheduler = scheduler; + this.command = command; + } + + @Override + public void run(Player player) { + String cmd = command.replace("{player}", player.getName()).replace("{uuid}", player.getUniqueId().toString()); + scheduler.schedulePlayerCommand(player, PapiUtil.set(player, cmd)); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " playercommand " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + command)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Player Command: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(command, NamedTextColor.WHITE))); + } + + public String getCommand() { + return command; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java new file mode 100644 index 0000000..d640683 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/playercommand/PlayerCommandActionType.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.interaction.playercommand; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class PlayerCommandActionType implements InteractionActionType, InteractionCommandHandler { + private final TaskScheduler scheduler; + + public PlayerCommandActionType(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public String serialize(PlayerCommandAction obj) { + return Base64.getEncoder().encodeToString(obj.getCommand().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public PlayerCommandAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new PlayerCommandAction(scheduler, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return PlayerCommandAction.class; + } + + @Override + public String getSubcommandName() { + return "playercommand"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String command = context.dumpAllArgs(); + return new PlayerCommandAction(scheduler, command, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java new file mode 100644 index 0000000..ca89839 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerAction.java @@ -0,0 +1,52 @@ +package lol.pyr.znpcsplus.interaction.switchserver; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.util.BungeeConnector; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class SwitchServerAction extends InteractionActionImpl { + private final String server; + private final BungeeConnector bungeeConnector; + + public SwitchServerAction(BungeeConnector bungeeConnector, String server, InteractionType interactionType, long cooldown, long delay) { + super(cooldown, delay, interactionType); + this.server = server; + this.bungeeConnector = bungeeConnector; + } + + @Override + public void run(Player player) { + bungeeConnector.connectPlayer(player, server); + } + + @Override + public Component getInfo(String id, int index, CommandContext context) { + return Component.text(index + ") ", NamedTextColor.GOLD) + .append(Component.text("[EDIT]", NamedTextColor.DARK_GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to edit this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action edit " + id + " " + index + " switchserver " + getInteractionType().name() + " " + getCooldown()/1000 + " " + getDelay() + " " + server)) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("[DELETE]", NamedTextColor.RED) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click to delete this action", NamedTextColor.GRAY))) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, + "/" + context.getLabel() + " action delete " + id + " " + index))) + .append(Component.text(" | ", NamedTextColor.GRAY)) + .append(Component.text("Switch Server: ", NamedTextColor.GREEN) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.text("Click Type: " + getInteractionType().name() + " Cooldown: " + getCooldown()/1000 + " Delay: " + getDelay(), NamedTextColor.GRAY)))) + .append(Component.text(server, NamedTextColor.WHITE))); + } + + public String getServer() { + return server; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java new file mode 100644 index 0000000..ead4fa3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/interaction/switchserver/SwitchServerActionType.java @@ -0,0 +1,66 @@ +package lol.pyr.znpcsplus.interaction.switchserver; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.znpcsplus.api.interaction.InteractionType; +import lol.pyr.znpcsplus.interaction.InteractionActionImpl; +import lol.pyr.znpcsplus.api.interaction.InteractionActionType; +import lol.pyr.znpcsplus.interaction.InteractionCommandHandler; +import lol.pyr.znpcsplus.util.BungeeConnector; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public class SwitchServerActionType implements InteractionActionType, InteractionCommandHandler { + private final BungeeConnector bungeeConnector; + + public SwitchServerActionType(BungeeConnector bungeeConnector) { + this.bungeeConnector = bungeeConnector; + } + + @Override + public String serialize(SwitchServerAction obj) { + return Base64.getEncoder().encodeToString(obj.getServer().getBytes(StandardCharsets.UTF_8)) + ";" + obj.getCooldown() + ";" + obj.getInteractionType().name() + ";" + obj.getDelay(); + } + + @Override + public SwitchServerAction deserialize(String str) { + String[] split = str.split(";"); + InteractionType type = split.length > 2 ? InteractionType.valueOf(split[2]) : InteractionType.ANY_CLICK; + return new SwitchServerAction(bungeeConnector, new String(Base64.getDecoder().decode(split[0]), StandardCharsets.UTF_8), type, Long.parseLong(split[1]), Long.parseLong(split.length > 3 ? split[3] : "0")); + } + + @Override + public Class getActionClass() { + return SwitchServerAction.class; + } + + @Override + public String getSubcommandName() { + return "switchserver"; + } + + @Override + public void appendUsage(CommandContext context) { + context.setUsage(context.getUsage() + " " + getSubcommandName() + " "); + } + + @Override + public InteractionActionImpl parse(CommandContext context) throws CommandExecutionException { + InteractionType type = context.parse(InteractionType.class); + long cooldown = (long) (context.parse(Double.class) * 1000D); + long delay = (long) (context.parse(Integer.class) * 1D); + String server = context.dumpAllArgs(); + return new SwitchServerAction(bungeeConnector, server, type, cooldown, delay); + } + + @Override + public List suggest(CommandContext context) throws CommandExecutionException { + if (context.argSize() == 1) return context.suggestEnum(InteractionType.values()); + if (context.argSize() == 2) return context.suggestLiteral("1"); + if (context.argSize() == 3) return context.suggestLiteral("0"); + return Collections.emptyList(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java new file mode 100644 index 0000000..e944dc0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcEntryImpl.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.npc; + +import lol.pyr.znpcsplus.api.npc.NpcEntry; + +public class NpcEntryImpl implements NpcEntry { + private final String id; + private final NpcImpl npc; + + private boolean process = false; + private boolean save = false; + private boolean modify = false; + + public NpcEntryImpl(String id, NpcImpl npc) { + this.id = id.toLowerCase(); + this.npc = npc; + } + + @Override + public NpcImpl getNpc() { + return npc; + } + + @Override + public boolean isProcessed() { + return process; + } + + @Override + public void setProcessed(boolean value) { + if (process && !value) npc.delete(); + process = value; + } + + @Override + public boolean isSave() { + return save; + } + + @Override + public void setSave(boolean value) { + save = value; + } + + @Override + public boolean isAllowCommandModification() { + return modify; + } + + @Override + public void setAllowCommandModification(boolean value) { + modify = value; + } + + public void enableEverything() { + setSave(true); + setProcessed(true); + setAllowCommandModification(true); + } + + public String getId() { + return id; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java new file mode 100644 index 0000000..97c82f5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcImpl.java @@ -0,0 +1,296 @@ +package lol.pyr.znpcsplus.npc; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.npc.Npc; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import lol.pyr.znpcsplus.util.Viewable; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class NpcImpl extends Viewable implements Npc { + private final PacketFactory packetFactory; + private String worldName; + private PacketEntity entity; + private NpcLocation location; + private NpcTypeImpl type; + private boolean enabled = true; + private final HologramImpl hologram; + private final UUID uuid; + + private final Map, Object> propertyMap = new HashMap<>(); + private final List actions = new ArrayList<>(); + + private final Map playerLookMap = new ConcurrentHashMap<>(); + + protected NpcImpl(UUID uuid, EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager, LegacyComponentSerializer textSerializer, World world, NpcTypeImpl type, NpcLocation location, PacketFactory packetFactory) { + this(uuid, propertyRegistry, configManager, packetFactory, textSerializer, world.getName(), type, location); + } + + public NpcImpl(UUID uuid, EntityPropertyRegistryImpl propertyRegistry, ConfigManager configManager, PacketFactory packetFactory, LegacyComponentSerializer textSerializer, String world, NpcTypeImpl type, NpcLocation location) { + this.packetFactory = packetFactory; + this.worldName = world; + this.type = type; + this.location = location; + this.uuid = uuid; + entity = new PacketEntity(packetFactory, this, this, type.getType(), location); + hologram = new HologramImpl(propertyRegistry, configManager, packetFactory, textSerializer, location.withY(location.getY() + type.getHologramOffset())); + } + + public void setType(NpcTypeImpl type) { + UNSAFE_hideAll(); + this.type = type; + entity = new PacketEntity(packetFactory, this, this, type.getType(), entity.getLocation()); + hologram.setLocation(location.withY(location.getY() + type.getHologramOffset())); + UNSAFE_showAll(); + } + + public void setType(NpcType type) { + if (type == null) throw new IllegalArgumentException("Npc Type cannot be null"); + setType((NpcTypeImpl) type); + } + + public NpcTypeImpl getType() { + return type; + } + + public PacketEntity getEntity() { + return entity; + } + + public NpcLocation getLocation() { + return location; + } + + public @Nullable Location getBukkitLocation() { + World world = getWorld(); + if (world == null) return null; + return location.toBukkitLocation(world); + } + + public void setLocation(NpcLocation location) { + this.location = location; + playerLookMap.clear(); + playerLookMap.putAll(getViewers().stream().collect(Collectors.toMap(Player::getUniqueId, player -> new float[]{location.getYaw(), location.getPitch()}))); + entity.setLocation(location); + hologram.setLocation(location.withY(location.getY() + type.getHologramOffset())); + } + + public void setHeadRotation(Player player, float yaw, float pitch) { + if (getHeadYaw(player) == yaw && getHeadPitch(player) == pitch) return; + playerLookMap.put(player.getUniqueId(), new float[]{yaw, pitch}); + entity.setHeadRotation(player, yaw, pitch); + } + + public void setHeadRotation(float yaw, float pitch) { + for (Player player : getViewers()) { + if (getHeadYaw(player) == yaw && getHeadPitch(player) == pitch) continue; + playerLookMap.put(player.getUniqueId(), new float[]{yaw, pitch}); + entity.setHeadRotation(player, yaw, pitch); + } + } + + public float getHeadYaw(Player player) { + return playerLookMap.getOrDefault(player.getUniqueId(), new float[]{location.getYaw(), location.getPitch()})[0]; + } + + public float getHeadPitch(Player player) { + return playerLookMap.getOrDefault(player.getUniqueId(), new float[]{location.getYaw(), location.getPitch()})[1]; + } + + public HologramImpl getHologram() { + return hologram; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (!enabled) delete(); + } + + public boolean isEnabled() { + return enabled; + } + + public UUID getUuid() { + return uuid; + } + + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + public String getWorldName() { + return worldName; + } + + @Override + protected CompletableFuture UNSAFE_show(Player player) { + playerLookMap.put(player.getUniqueId(), new float[]{location.getYaw(), location.getPitch()}); + return CompletableFuture.allOf(entity.spawn(player), hologram.show(player)); + } + + @Override + protected void UNSAFE_hide(Player player) { + playerLookMap.remove(player.getUniqueId()); + entity.despawn(player); + hologram.hide(player); + } + + private void UNSAFE_refreshProperty(EntityPropertyImpl property) { + if (!type.isAllowedProperty(property)) return; + for (Player viewer : getViewers()) { + List> data = property.applyStandalone(viewer, entity, true); + if (!data.isEmpty()) packetFactory.sendMetadata(viewer, entity, data); + } + } + + @SuppressWarnings("unchecked") + public T getProperty(EntityProperty key) { + return hasProperty(key) ? (T) propertyMap.get((EntityPropertyImpl) key) : key.getDefaultValue(); + } + + public boolean hasProperty(EntityProperty key) { + return propertyMap.containsKey((EntityPropertyImpl) key); + } + + @SuppressWarnings("unchecked") + @Override + public void setProperty(EntityProperty key, T value) { + // See https://github.com/Pyrbu/ZNPCsPlus/pull/129#issuecomment-1948777764 + Object val = value; + if (val instanceof ItemStack) val = SpigotConversionUtil.fromBukkitItemStack((ItemStack) val); + + setProperty((EntityPropertyImpl) key, (T) val); + } + + @SuppressWarnings("unchecked") + @Override + public void setItemProperty(EntityProperty key, ItemStack value) { + setProperty((EntityPropertyImpl) key, SpigotConversionUtil.fromBukkitItemStack(value)); + } + + @SuppressWarnings("unchecked") + @Override + public ItemStack getItemProperty(EntityProperty key) { + return SpigotConversionUtil.toBukkitItemStack(getProperty((EntityProperty) key)); + } + + public void setProperty(EntityPropertyImpl key, T value) { + if (key == null) return; + if (value == null || value.equals(key.getDefaultValue())) propertyMap.remove(key); + else propertyMap.put(key, value); + UNSAFE_refreshProperty(key); + } + + @SuppressWarnings("unchecked") + public void UNSAFE_setProperty(EntityPropertyImpl property, Object value) { + setProperty((EntityPropertyImpl) property, (T) value); + } + + @SuppressWarnings("unchecked") + public void UNSAFE_setProperty(EntityProperty property, Object value) { + setProperty((EntityPropertyImpl) property, (T) value); + } + + public Set> getAllProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()); + } + + @Override + public Set> getAppliedProperties() { + return Collections.unmodifiableSet(propertyMap.keySet()).stream().filter(type::isAllowedProperty).collect(Collectors.toSet()); + } + + @Override + public List getActions() { + return Collections.unmodifiableList(actions); + } + + @Override + public void removeAction(int index) { + actions.remove(index); + } + + @Override + public void addAction(InteractionAction action) throws IllegalArgumentException { + if (action == null) throw new IllegalArgumentException("action can not be null"); + actions.add(action); + } + + @Override + public void clearActions() { + actions.clear(); + } + + @Override + public void editAction(int index, InteractionAction action) throws IllegalArgumentException { + if (action == null) throw new IllegalArgumentException("action can not be null"); + actions.set(index, action); + } + + @Override + public int getPacketEntityId() { + return entity.getEntityId(); + } + + public void setWorld(World world) { + if (world == null) throw new IllegalArgumentException("world can not be null"); + delete(); + this.worldName = world.getName(); + } + + public void setWorld(String name) { + if (name == null) throw new IllegalArgumentException("world name can not be null"); + delete(); + this.worldName = name; + } + + public void swingHand(boolean offHand) { + for (Player viewer : getViewers()) entity.swingHand(viewer, offHand); + } + + @Override + public @NotNull List getPassengers() { + return entity.getPassengers(); + } + + @Override + public void addPassenger(int entityId) { + entity.addPassenger(entityId); + } + + @Override + public void removePassenger(int entityId) { + entity.removePassenger(entityId); + } + + @Override + public @Nullable Integer getVehicleId() { + return entity.getVehicleId(); + } + + @Override + public void setVehicleId(Integer vehicleId) { + entity.setVehicleId(vehicleId); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java new file mode 100644 index 0000000..43f0a9f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcRegistryImpl.java @@ -0,0 +1,231 @@ +package lol.pyr.znpcsplus.npc; + +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import lol.pyr.znpcsplus.api.npc.NpcRegistry; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.hologram.HologramItem; +import lol.pyr.znpcsplus.hologram.HologramLine; +import lol.pyr.znpcsplus.hologram.HologramText; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.storage.NpcStorageType; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.util.*; +import java.util.stream.Collectors; + +public class NpcRegistryImpl implements NpcRegistry { + private NpcStorage storage; + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final LegacyComponentSerializer textSerializer; + private final EntityPropertyRegistryImpl propertyRegistry; + + private final List npcList = new ArrayList<>(); + private final Map npcIdLookupMap = new HashMap<>(); + private final Map npcUuidLookupMap = new HashMap<>(); + + public NpcRegistryImpl(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, TaskScheduler scheduler, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, NpcSerializerRegistryImpl serializerRegistry, LegacyComponentSerializer textSerializer) { + this.textSerializer = textSerializer; + this.propertyRegistry = propertyRegistry; + storage = configManager.getConfig().storageType().create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + if (storage == null) { + Bukkit.getLogger().warning("Failed to initialize storage, falling back to YAML"); + storage = NpcStorageType.YAML.create(configManager, plugin, packetFactory, actionRegistry, typeRegistry, propertyRegistry, textSerializer, serializerRegistry); + } + this.packetFactory = packetFactory; + this.configManager = configManager; + + if (configManager.getConfig().autoSaveEnabled()) { + long delay = configManager.getConfig().autoSaveInterval() * 20L; + scheduler.runDelayedTimerAsync(this::save, delay, delay); + } + } + + @Override + public void register(NpcEntry entry) { + register((NpcEntryImpl) entry); + } + + private void register(NpcEntryImpl entry) { + if (entry == null) throw new NullPointerException(); + unregister(npcIdLookupMap.put(entry.getId(), entry)); + unregister(npcUuidLookupMap.put(entry.getNpc().getUuid(), entry)); + npcList.add(entry); + } + + private void unregister(NpcEntryImpl entry) { + if (entry == null) return; + npcList.remove(entry); + NpcImpl one = npcIdLookupMap.remove(entry.getId()).getNpc(); + NpcImpl two = npcUuidLookupMap.remove(entry.getNpc().getUuid()).getNpc(); + if (one != null) one.delete(); + if (two != null && !Objects.equals(one, two)) two.delete(); + } + + private void unregisterAll() { + for (NpcEntryImpl entry : getAll()) { + if (entry.isSave()) entry.getNpc().delete(); + } + npcList.clear(); + npcIdLookupMap.clear(); + npcUuidLookupMap.clear(); + } + + public void registerAll(Collection entries) { + for (NpcEntryImpl entry : entries) register(entry); + } + + public void reload() { + unregisterAll(); + registerAll(storage.loadNpcs()); + } + + public void save() { + storage.saveNpcs(npcList.stream().filter(NpcEntryImpl::isSave).collect(Collectors.toList())); + } + + @Override + public NpcEntryImpl getById(String id) { + return npcIdLookupMap.get(id.toLowerCase()); + } + + @Override + public NpcEntry getByUuid(UUID uuid) { + return npcUuidLookupMap.get(uuid); + } + + public Collection getAll() { + return Collections.unmodifiableCollection(npcList); + } + + public Collection getProcessable() { + return Collections.unmodifiableCollection(npcList.stream() + .filter(NpcEntryImpl::isProcessed) + .collect(Collectors.toList())); + } + + public Collection getAllModifiable() { + return Collections.unmodifiableCollection(npcList.stream() + .filter(NpcEntryImpl::isAllowCommandModification) + .collect(Collectors.toList())); + } + + public NpcEntryImpl getByEntityId(int id) { + return npcList.stream().filter(entry -> entry.getNpc().getEntity().getEntityId() == id || + entry.getNpc().getHologram().getLines().stream().anyMatch(line -> line.getEntityId() == id)) // Also match the holograms of npcs + .findFirst().orElse(null); + } + + public Collection getAllIds() { + return Collections.unmodifiableSet(npcIdLookupMap.keySet()); + } + + @Override + public Collection getAllPlayerMade() { + return getAllModifiable(); + } + + @Override + public Collection getAllPlayerMadeIds() { + return getAllModifiable().stream() + .map(NpcEntryImpl::getId) + .collect(Collectors.toSet()); + } + + public Collection getModifiableIds() { + return Collections.unmodifiableSet(npcIdLookupMap.entrySet().stream() + .filter(entry -> entry.getValue().isAllowCommandModification()) + .map(Map.Entry::getKey) + .collect(Collectors.toSet())); + } + + public NpcEntryImpl create(String id, World world, NpcType type, NpcLocation location) { + return create(id, world, (NpcTypeImpl) type, location); + } + + public NpcEntryImpl create(String id, World world, NpcTypeImpl type, NpcLocation location) { + id = id.toLowerCase(); + if (npcIdLookupMap.containsKey(id)) throw new IllegalArgumentException("An npc with the id " + id + " already exists!"); + NpcImpl npc = new NpcImpl(UUID.randomUUID(), propertyRegistry, configManager, textSerializer, world, type, location, packetFactory); + type.applyDefaultProperties(npc); + NpcEntryImpl entry = new NpcEntryImpl(id, npc); + register(entry); + return entry; + } + + public NpcEntryImpl clone(String id, String newId, World newWorld, NpcLocation newLocation) { + NpcEntryImpl oldNpc = getById(id); + if (oldNpc == null) return null; + NpcEntryImpl newNpc = create(newId, newWorld, oldNpc.getNpc().getType(), newLocation); + newNpc.enableEverything(); + + for (EntityProperty property : oldNpc.getNpc().getAllProperties()) { + newNpc.getNpc().UNSAFE_setProperty(property, oldNpc.getNpc().getProperty(property)); + } + + for (InteractionAction action : oldNpc.getNpc().getActions()) { + newNpc.getNpc().addAction(action); + } + + for (HologramLine line : oldNpc.getNpc().getHologram().getLines()) { + if (line instanceof HologramText) { + HologramText text = (HologramText) line; + newNpc.getNpc().getHologram().addTextLineComponent(text.getValue()); + } + else if (line instanceof HologramItem) { + HologramItem item = (HologramItem) line; + newNpc.getNpc().getHologram().addItemLinePEStack(item.getValue()); + } + else throw new IllegalArgumentException("Unknown hologram line type during clone"); + } + + return newNpc; + } + + @Override + public void delete(String id) { + NpcEntryImpl entry = npcIdLookupMap.get(id.toLowerCase()); + if (entry == null) return; + unregister(entry); + storage.deleteNpc(entry); + } + + @Override + public void delete(UUID uuid) { + NpcEntryImpl entry = npcUuidLookupMap.get(uuid); + if (entry == null) return; + unregister(entry); + storage.deleteNpc(entry); + } + + public void switchIds(String oldId, String newId) { + NpcEntryImpl entry = getById(oldId); + delete(oldId); + NpcEntryImpl newEntry = new NpcEntryImpl(newId, entry.getNpc()); + newEntry.setSave(entry.isSave()); + newEntry.setProcessed(entry.isProcessed()); + newEntry.setAllowCommandModification(entry.isAllowCommandModification()); + register(newEntry); + } + + public void unload() { + npcList.forEach(npcEntry -> npcEntry.getNpc().delete()); + storage.close(); + } + + public NpcStorage getStorage() { + return storage; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java new file mode 100644 index 0000000..a599306 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeImpl.java @@ -0,0 +1,217 @@ +package lol.pyr.znpcsplus.npc; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; + +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class NpcTypeImpl implements NpcType { + private final EntityType type; + private final Set> allowedProperties; + private final Map, Object> defaultProperties; + private final String name; + private final double hologramOffset; + + private NpcTypeImpl(String name, EntityType type, double hologramOffset, Set> allowedProperties, Map, Object> defaultProperties) { + this.name = name.toLowerCase(); + this.type = type; + this.hologramOffset = hologramOffset; + this.allowedProperties = allowedProperties; + this.defaultProperties = defaultProperties; + } + + public String getName() { + return name; + } + + public EntityType getType() { + return type; + } + + public double getHologramOffset() { + return hologramOffset; + } + + public Set> getAllowedProperties() { + return allowedProperties.stream().map(property -> (EntityProperty) property).collect(Collectors.toSet()); + } + + public void applyDefaultProperties(NpcImpl npc) { + for (Map.Entry, Object> entry : defaultProperties.entrySet()) { + npc.UNSAFE_setProperty(entry.getKey(), entry.getValue()); + } + } + + public boolean isAllowedProperty(EntityPropertyImpl entityProperty) { + return !entityProperty.isPlayerModifiable() || allowedProperties.contains(entityProperty); + } + + protected static final class Builder { + private final static Logger logger = Logger.getLogger("NpcTypeBuilder"); + + private final EntityPropertyRegistryImpl propertyRegistry; + private final String name; + private final EntityType type; + private final List> allowedProperties = new ArrayList<>(); + private final Map, Object> defaultProperties = new HashMap<>(); + private double hologramOffset = 0; + + Builder(EntityPropertyRegistryImpl propertyRegistry, String name, EntityType type) { + this.propertyRegistry = propertyRegistry; + this.name = name; + this.type = type; + } + + public Builder addEquipmentProperties() { + addProperties("helmet", "chestplate", "leggings", "boots"); + return addHandProperties(); + } + + public Builder addHandProperties() { + if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)) { + return addProperties("hand", "offhand"); + } else { + return addProperties("hand"); + } + } + + public Builder addProperties(String... names) { + for (String name : names) { + if (propertyRegistry.getByName(name) == null) { + // Only for use in development, please comment this out in production because some properties are version-dependent + // logger.warning("Tried to register the non-existent \"" + name + "\" property to the \"" + this.name + "\" npc type"); + continue; + } + allowedProperties.add(propertyRegistry.getByName(name)); + } + return this; + } + + @SuppressWarnings("unchecked") + public Builder addDefaultProperty(String name, T value) { + EntityPropertyImpl property = (EntityPropertyImpl) propertyRegistry.getByName(name); + if (property == null) { + // Only for use in development, please comment this out in production because some properties are version-dependent + // logger.warning("Tried to register the non-existent \"" + name + "\" default property to the \"" + this.name + "\" npc type"); + return this; + } + defaultProperties.put(property, value); + return this; + } + + public Builder setHologramOffset(double hologramOffset) { + this.hologramOffset = hologramOffset; + return this; + } + + public NpcTypeImpl build() { + ServerVersion version = PacketEvents.getAPI().getServerManager().getVersion(); + addProperties("fire", "invisible", "silent", "look", "look_distance", "look_return", "view_distance", + "potion_color", "potion_ambient", "display_name", "permission_required", + "player_knockback", "player_knockback_exempt_permission", "player_knockback_distance", "player_knockback_vertical", + "player_knockback_horizontal", "player_knockback_cooldown", "player_knockback_sound", "player_knockback_sound_name", + "player_knockback_sound_volume", "player_knockback_sound_pitch"); + if (!type.equals(EntityTypes.PLAYER)) addProperties("dinnerbone"); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.LIVINGENTITY)) { + addProperties("health", "attribute_max_health"); + } + // TODO: make this look nicer after completing the rest of the properties + if (version.isNewerThanOrEquals(ServerVersion.V_1_9)) addProperties("glow"); + if (version.isNewerThanOrEquals(ServerVersion.V_1_14)) { + addProperties("pose"); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.HORSE)) { + addProperties("chestplate"); + } + } + if (version.isNewerThanOrEquals(ServerVersion.V_1_17)) addProperties("shaking"); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_AGEABLE) || EntityTypes.isTypeInstanceOf(type, EntityTypes.ZOMBIE) || EntityTypes.isTypeInstanceOf(type, EntityTypes.ZOGLIN)) { + addProperties("baby"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_HORSE)) { + addProperties("is_saddled", "is_eating", "is_rearing", "has_mouth_open"); + } + if (type.equals(EntityTypes.HORSE) && version.isOlderThan(ServerVersion.V_1_14)) { + addProperties("horse_armor"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.CHESTED_HORSE)) { + addProperties("has_chest"); + } else if (version.isOlderThan(ServerVersion.V_1_11) && type.equals(EntityTypes.HORSE)) { + addProperties("has_chest"); + } + if (version.isOlderThan(ServerVersion.V_1_11) && EntityTypes.isTypeInstanceOf(type, EntityTypes.SKELETON)) { + addProperties("skeleton_type"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_EVO_ILLU_ILLAGER)) { + addProperties("spell"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_PIGLIN)) { + addProperties("piglin_immune_to_zombification"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.SLIME) || EntityTypes.isTypeInstanceOf(type, EntityTypes.PHANTOM)) { + addProperties("size"); + } + if (version.isOlderThan(ServerVersion.V_1_14)) { + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.OCELOT)) { + addProperties("ocelot_type"); + } + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.PANDA)) { + if (version.isNewerThanOrEquals(ServerVersion.V_1_15)) { + addProperties("panda_rolling", "panda_sitting", "panda_on_back", "hand"); + } else { + addProperties("panda_eating"); + } + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_TAMEABLE_ANIMAL) && + !(version.isNewerThanOrEquals(ServerVersion.V_1_14) && type.equals(EntityTypes.OCELOT))) { + addProperties("tamed", "sitting"); + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.GUARDIAN)) { + addProperties("is_retracting_spikes"); + } + if (version.isNewerThanOrEquals(ServerVersion.V_1_20_5)) { + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.WOLF)) { + addProperties("wolf_variant"); + if (version.isNewerThanOrEquals(ServerVersion.V_1_21)) { + addProperties("body"); + } else { + addProperties("chestplate"); + } + } + } + if (version.isNewerThanOrEquals(ServerVersion.V_1_21_4)) { + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.CREAKING)) { + addProperties("creaking_crumbling"); + } + } + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ZOMBIE)) { + if (version.isOlderThan(ServerVersion.V_1_9)) { + addProperties("zombie_is_villager"); + } else if (version.isOlderThan(ServerVersion.V_1_11)) { + addProperties("zombie_type"); + } + + if (version.isOlderThan(ServerVersion.V_1_11)) { + addProperties("is_converting"); + } + + if (version.isNewerThanOrEquals(ServerVersion.V_1_9) && version.isOlderThan(ServerVersion.V_1_14)) { + addProperties("zombie_hands_held_up"); + } + + if (version.isNewerThanOrEquals(ServerVersion.V_1_13)) { + addProperties("zombie_becoming_drowned"); + } + } + return new NpcTypeImpl(name, type, hologramOffset, new HashSet<>(allowedProperties), defaultProperties); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java new file mode 100644 index 0000000..8b877a3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/npc/NpcTypeRegistryImpl.java @@ -0,0 +1,423 @@ +package lol.pyr.znpcsplus.npc; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import lol.pyr.znpcsplus.api.npc.NpcType; +import lol.pyr.znpcsplus.api.npc.NpcTypeRegistry; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import org.bukkit.plugin.Plugin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class NpcTypeRegistryImpl implements NpcTypeRegistry { + private final List types = new ArrayList<>(); + + private NpcTypeImpl register(NpcTypeImpl.Builder builder) { + return register(builder.build()); + } + + private NpcTypeImpl register(NpcTypeImpl type) { + types.add(type); + return type; + } + + private NpcTypeImpl.Builder builder(EntityPropertyRegistryImpl propertyRegistry, String name, EntityType type) { + return new NpcTypeImpl.Builder(propertyRegistry, name, type); + } + + public void registerDefault(PacketEventsAPI packetEvents, EntityPropertyRegistryImpl p /* propertyRegistry */) { + ServerVersion version = packetEvents.getServerManager().getVersion(); + + register(builder(p, "player", EntityTypes.PLAYER) + .setHologramOffset(-0.15D) + .addEquipmentProperties() + .addProperties("skin_cape", "skin_jacket", "skin_left_sleeve", "skin_right_sleeve", "skin_left_leg", "skin_right_leg", "skin_hat", "shoulder_entity_left", "shoulder_entity_right", "force_body_rotation", "entity_sitting") + .addDefaultProperty("skin_cape", true) + .addDefaultProperty("skin_jacket", true) + .addDefaultProperty("skin_left_sleeve", true) + .addDefaultProperty("skin_right_sleeve", true) + .addDefaultProperty("skin_left_leg", true) + .addDefaultProperty("skin_right_leg", true) + .addDefaultProperty("skin_hat", true)); + + // Most hologram offsets generated using Entity#getHeight() in 1.19.4 + + register(builder(p, "armor_stand", EntityTypes.ARMOR_STAND) + .setHologramOffset(0) + .addEquipmentProperties() + .addProperties("small", "arms", "base_plate", "head_rotation", "body_rotation", "left_arm_rotation", "right_arm_rotation", "left_leg_rotation", "right_leg_rotation")); + + register(builder(p, "bat", EntityTypes.BAT) + .setHologramOffset(-1.075) + .addProperties("hanging")); + + register(builder(p, "blaze", EntityTypes.BLAZE) + .setHologramOffset(-0.175) + .addProperties("blaze_on_fire")); + + register(builder(p, "cave_spider", EntityTypes.CAVE_SPIDER) + .setHologramOffset(-1.475)); + + register(builder(p, "chicken", EntityTypes.CHICKEN) + .setHologramOffset(-1.275)); + + register(builder(p, "cow", EntityTypes.COW) + .setHologramOffset(-0.575)); + + register(builder(p, "creeper", EntityTypes.CREEPER) + .setHologramOffset(-0.275) + .addProperties("creeper_state", "creeper_charged")); + + register(builder(p, "end_crystal", EntityTypes.END_CRYSTAL) + .setHologramOffset(0.025) + .addProperties("beam_target", "show_base")); + + register(builder(p, "ender_dragon", EntityTypes.ENDER_DRAGON) + .setHologramOffset(6.0245)); + + register(builder(p, "enderman", EntityTypes.ENDERMAN) + .setHologramOffset(0.925) + .addProperties("enderman_held_block", "enderman_screaming", "enderman_staring", "entity_sitting")); + + register(builder(p, "endermite", EntityTypes.ENDERMITE) + .setHologramOffset(-1.675)); + + register(builder(p, "ghast", EntityTypes.GHAST) + .setHologramOffset(2.025) + .addProperties("attacking")); + + register(builder(p, "giant", EntityTypes.GIANT) + .setHologramOffset(10.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "guardian", EntityTypes.GUARDIAN) + .setHologramOffset(-1.125) + .addProperties("is_elder")); + + register(builder(p, "horse", EntityTypes.HORSE) + .setHologramOffset(-0.375) + .addProperties("horse_type", "horse_style", "horse_color", "horse_armor")); + + register(builder(p, "iron_golem", EntityTypes.IRON_GOLEM) + .setHologramOffset(0.725)); + + register(builder(p, "magma_cube", EntityTypes.MAGMA_CUBE) + .setHologramOffset(-1.455)); // TODO: Hologram offset scaling with size property + + register(builder(p, "mooshroom", EntityTypes.MOOSHROOM) + .setHologramOffset(-0.575) + .addProperties("mooshroom_variant")); + + register(builder(p, "ocelot", EntityTypes.OCELOT) + .setHologramOffset(-1.275)); + + register(builder(p, "pig", EntityTypes.PIG) + .setHologramOffset(-1.075) + .addProperties("pig_saddled")); + + register(builder(p, "rabbit", EntityTypes.RABBIT) + .setHologramOffset(-1.475) + .addProperties("rabbit_type")); + + register(builder(p, "sheep", EntityTypes.SHEEP) + .setHologramOffset(-0.675) + .addProperties("sheep_color", "sheep_sheared")); + + register(builder(p, "silverfish", EntityTypes.SILVERFISH) + .setHologramOffset(-1.675)); + + register(builder(p, "skeleton", EntityTypes.SKELETON) + .setHologramOffset(0.015) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "skeleton_horse", EntityTypes.SKELETON_HORSE) + .setHologramOffset(-0.375)); + + register(builder(p, "slime", EntityTypes.SLIME) + .setHologramOffset(-1.455)); // TODO: Hologram offset scaling with size property + + register(builder(p, "snow_golem", EntityTypes.SNOW_GOLEM) + .setHologramOffset(-0.075) + .addProperties("derpy_snowgolem")); + + register(builder(p, "spider", EntityTypes.SPIDER) + .setHologramOffset(-1.075)); + + register(builder(p, "squid", EntityTypes.SQUID) + .setHologramOffset(-1.175)); + + register(builder(p, "villager", EntityTypes.VILLAGER) + .setHologramOffset(-0.025) + .addProperties("hand", "villager_type", "villager_profession", "villager_level")); + + register(builder(p, "witch", EntityTypes.WITCH) + .setHologramOffset(-0.025) + .addProperties("hand")); + + register(builder(p, "wither", EntityTypes.WITHER) + .setHologramOffset(1.525) + .addProperties("invulnerable_time")); + + register(builder(p, "wolf", EntityTypes.WOLF) + .setHologramOffset(-1.125) + .addProperties("wolf_begging", "wolf_collar", "wolf_angry")); + + register(builder(p, "zombie", EntityTypes.ZOMBIE) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "zombie_horse", EntityTypes.ZOMBIE_HORSE) + .setHologramOffset(-0.375)); + + register(builder(p, "zombified_piglin", EntityTypes.ZOMBIFIED_PIGLIN) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_9)) return; + + register(builder(p, "shulker", EntityTypes.SHULKER) + .setHologramOffset(-0.975) + .addProperties("attach_direction", "shield_height", "shulker_color")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_10)) return; + + register(builder(p, "polar_bear", EntityTypes.POLAR_BEAR) + .setHologramOffset(-0.575) + .addProperties("polar_bear_standing")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_11)) return; + + register(builder(p, "donkey", EntityTypes.DONKEY) + .setHologramOffset(-0.475)); + + register(builder(p, "mule", EntityTypes.MULE) + .setHologramOffset(-0.375)); + + register(builder(p, "elder_guardian", EntityTypes.ELDER_GUARDIAN) + .setHologramOffset(0.0225)); + + register(builder(p, "husk", EntityTypes.HUSK) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "stray", EntityTypes.STRAY) + .setHologramOffset(0.015) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "evoker", EntityTypes.EVOKER) + .setHologramOffset(-0.025) + .addProperties("entity_sitting")); + + register(builder(p, "llama", EntityTypes.LLAMA) + .setHologramOffset(-0.105) + .addProperties("carpet_color", "llama_variant", "body")); + + register(builder(p, "vex", EntityTypes.VEX) + .setHologramOffset(-1.175) + .addHandProperties()); + + register(builder(p, "vindicator", EntityTypes.VINDICATOR) + .setHologramOffset(-0.025) + .addProperties("celebrating", "entity_sitting")); + + register(builder(p, "wither_skeleton", EntityTypes.WITHER_SKELETON) + .setHologramOffset(0.425) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "zombie_villager", EntityTypes.ZOMBIE_VILLAGER) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_12)) return; + + register(builder(p, "illusioner", EntityTypes.ILLUSIONER) + .setHologramOffset(-0.025) + .addProperties("entity_sitting")); + + register(builder(p, "parrot", EntityTypes.PARROT) + .setHologramOffset(-1.075) + .addProperties("parrot_variant")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_13)) return; + + register(builder(p, "cod", EntityTypes.COD) + .setHologramOffset(-1.675)); + + register(builder(p, "dolphin", EntityTypes.DOLPHIN) + .setHologramOffset(-1.375) + .addProperties("hand")); + + register(builder(p, "drowned", EntityTypes.DROWNED) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "phantom", EntityTypes.PHANTOM) + .setHologramOffset(-1.475)); + + register(builder(p, "pufferfish", EntityTypes.PUFFERFISH) + .setHologramOffset(-1.625) + .addProperties("puff_state")); + + register(builder(p, "salmon", EntityTypes.SALMON) + .setHologramOffset(-1.575)); + + register(builder(p, "tropical_fish", EntityTypes.TROPICAL_FISH) + .setHologramOffset(-1.575) + .addProperties("tropical_fish_pattern", "tropical_fish_body_color", "tropical_fish_pattern_color")); + + register(builder(p, "turtle", EntityTypes.TURTLE) + .setHologramOffset(-1.575)); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_14)) return; + + register(builder(p, "cat", EntityTypes.CAT) + .setHologramOffset(-1.275) + .addProperties("cat_variant", "cat_laying", "cat_relaxed", "cat_collar")); + + register(builder(p, "fox", EntityTypes.FOX) + .setHologramOffset(-1.275) + .addProperties("hand", "fox_variant", "fox_sitting", "fox_crouching", "fox_sleeping", "fox_faceplanted")); + + register(builder(p, "panda", EntityTypes.PANDA) + .setHologramOffset(-0.725) + .addProperties("panda_main_gene", "panda_hidden_gene", "panda_sneezing")); + + register(builder(p, "pillager", EntityTypes.PILLAGER) + .setHologramOffset(-0.025) + .addHandProperties() + .addProperties("pillager_charging", "entity_sitting")); + + register(builder(p, "ravager", EntityTypes.RAVAGER) + .setHologramOffset(0.225)); + + register(builder(p, "trader_llama", EntityTypes.TRADER_LLAMA) + .setHologramOffset(-0.105) + .addProperties("carpet_color", "llama_variant", "body")); + + register(builder(p, "wandering_trader", EntityTypes.WANDERING_TRADER) + .setHologramOffset(-0.025) + .addProperties("hand")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_15)) return; + + register(builder(p, "bee", EntityTypes.BEE) + .setHologramOffset(-1.375) + .addProperties("angry", "has_nectar")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_16)) return; + + register(builder(p, "hoglin", EntityTypes.HOGLIN) + .setHologramOffset(-0.575) + .addProperties("hoglin_immune_to_zombification")); + + register(builder(p, "piglin", EntityTypes.PIGLIN) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("piglin_baby", "piglin_charging_crossbow", "piglin_dancing", "entity_sitting")); + + register(builder(p, "piglin_brute", EntityTypes.PIGLIN_BRUTE) + .setHologramOffset(-0.025) + .addEquipmentProperties() + .addProperties("entity_sitting")); + + register(builder(p, "strider", EntityTypes.STRIDER) + .setHologramOffset(-0.275) + .addProperties("strider_shaking", "strider_saddled")); + + register(builder(p, "zoglin", EntityTypes.ZOGLIN) + .setHologramOffset(-0.575)); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_17)) return; + + register(builder(p, "axolotl", EntityTypes.AXOLOTL) + .setHologramOffset(-1.555) + .addProperties("axolotl_variant", "playing_dead")); + + register(builder(p, "glow_squid", EntityTypes.GLOW_SQUID) + .setHologramOffset(-1.175)); + + register(builder(p, "goat", EntityTypes.GOAT) + .setHologramOffset(-0.675) + .addProperties("has_left_horn", "has_right_horn")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_19)) return; + + register(builder(p, "allay", EntityTypes.ALLAY) + .setHologramOffset(-1.375) + .addHandProperties()); + + register(builder(p, "frog", EntityTypes.FROG) + .setHologramOffset(-1.475) + .addProperties("frog_variant", "frog_target_npc")); + + register(builder(p, "tadpole", EntityTypes.TADPOLE) + .setHologramOffset(-1.675)); + + register(builder(p, "warden", EntityTypes.WARDEN) + .setHologramOffset(0.925) + .addProperties("warden_anger")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_20)) return; + + register(builder(p, "sniffer", EntityTypes.SNIFFER) + .setHologramOffset(0.075) + .addProperties("sniffer_state")); + + register(builder(p, "camel", EntityTypes.CAMEL) + .setHologramOffset(0.4) + .addProperties("bashing", "camel_sitting")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_20_5)) return; + + register(builder(p, "armadillo", EntityTypes.ARMADILLO) + .setHologramOffset(-1.325) + .addProperties("armadillo_state")); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_21)) return; + + register(builder(p, "bogged", EntityTypes.BOGGED) + .setHologramOffset(0.015) + .addProperties("bogged_sheared", "entity_sitting")); + + register(builder(p, "breeze", EntityTypes.BREEZE) + .setHologramOffset(-0.205)); + + if (!version.isNewerThanOrEquals(ServerVersion.V_1_21_2)) return; + + register(builder(p, "creaking", EntityTypes.CREAKING) + .setHologramOffset(0.725) + .addProperties("creaking_active")); + } + + public Collection getAll() { + return Collections.unmodifiableList(types); + } + + public Collection getAllImpl() { + return Collections.unmodifiableList(types); + } + + public NpcTypeImpl getByName(String name) { + for (NpcTypeImpl type : types) if (type.getName().equalsIgnoreCase(name)) return type; + return null; + } + + public NpcTypeImpl getByEntityType(EntityType entityType) { + for (NpcTypeImpl type : types) if (type.getType() == entityType) return type; + return null; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java new file mode 100644 index 0000000..7cfdc93 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/PacketFactory.java @@ -0,0 +1,31 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.util.NamedColor; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface PacketFactory { + CompletableFuture spawnPlayer(Player player, PacketEntity entity, PropertyHolder properties); + void spawnEntity(Player player, PacketEntity entity, PropertyHolder properties); + void destroyEntity(Player player, PacketEntity entity, PropertyHolder properties); + void teleportEntity(Player player, PacketEntity entity); + CompletableFuture addTabPlayer(Player player, PacketEntity entity, PropertyHolder properties); + void removeTabPlayer(Player player, PacketEntity entity); + void createTeam(Player player, PacketEntity entity, NamedColor namedColor); + void removeTeam(Player player, PacketEntity entity); + void sendAllMetadata(Player player, PacketEntity entity, PropertyHolder properties); + void sendEquipment(Player player, PacketEntity entity, Equipment equipment); + void sendMetadata(Player player, PacketEntity entity, List> data); + void sendHeadRotation(Player player, PacketEntity entity, float yaw, float pitch); + void sendHandSwing(Player player, PacketEntity entity, boolean offHand); + void setPassengers(Player player, int vehicle, int... passengers); + void sendAllAttributes(Player player, PacketEntity entity, PropertyHolder properties); + void sendAttribute(Player player, PacketEntity entity, WrapperPlayServerUpdateAttributes.Property property); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java new file mode 100644 index 0000000..5a97fbf --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_17PacketFactory.java @@ -0,0 +1,34 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.Optional; + +public class V1_17PacketFactory extends V1_8PacketFactory { + public V1_17PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } + + @Override + public void spawnEntity(Player player, PacketEntity entity, PropertyHolder properties) { + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnEntity(entity.getEntityId(), Optional.of(entity.getUuid()), entity.getType(), + npcLocationToVector(location), location.getPitch(), location.getYaw(), location.getYaw(), 0, Optional.of(new Vector3d()))); + sendAllMetadata(player, entity, properties); + if (EntityTypes.isTypeInstanceOf(entity.getType(), EntityTypes.LIVINGENTITY)) sendAllAttributes(player, entity, properties); + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java new file mode 100644 index 0000000..22fe62d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_19_3PacketFactory.java @@ -0,0 +1,47 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.player.GameMode; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoRemove; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoUpdate; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.EnumSet; +import java.util.concurrent.CompletableFuture; + +public class V1_19_3PacketFactory extends V1_17PacketFactory { + public V1_19_3PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } + + @Override + public CompletableFuture addTabPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + if (entity.getType() != EntityTypes.PLAYER) return CompletableFuture.completedFuture(null); + CompletableFuture future = new CompletableFuture<>(); + skinned(player, properties, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))).thenAccept(profile -> { + WrapperPlayServerPlayerInfoUpdate.PlayerInfo info = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo( + profile, false, 1, GameMode.CREATIVE, + Component.text(configManager.getConfig().tabDisplayName().replace("{id}", Integer.toString(entity.getEntityId()))), null); + sendPacket(player, new WrapperPlayServerPlayerInfoUpdate(EnumSet.of(WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER, + WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_LISTED), info, info)); + future.complete(null); + }); + return future; + } + + @Override + public void removeTabPlayer(Player player, PacketEntity entity) { + if (entity.getType() != EntityTypes.PLAYER) return; + sendPacket(player, new WrapperPlayServerPlayerInfoRemove(entity.getUuid())); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java new file mode 100644 index 0000000..4a760e4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_20_2PacketFactory.java @@ -0,0 +1,43 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityHeadLook; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class V1_20_2PacketFactory extends V1_19_3PacketFactory { + + protected ConfigManager configManager; + + public V1_20_2PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + this.configManager = configManager; + } + + @Override + public CompletableFuture spawnPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + return addTabPlayer(player, entity, properties).thenAccept(ignored -> { + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnEntity(entity.getEntityId(), Optional.of(entity.getUuid()), entity.getType(), + npcLocationToVector(location), location.getPitch(), location.getYaw(), location.getYaw(), 0, Optional.of(new Vector3d()))); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + sendAllMetadata(player, entity, properties); + sendAllAttributes(player, entity, properties); + scheduler.runLaterAsync(() -> removeTabPlayer(player, entity), configManager.getConfig().tabHideDelay()); + }); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java new file mode 100644 index 0000000..7b7ddc5 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_3PacketFactory.java @@ -0,0 +1,29 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.EntityPositionData; +import com.github.retrooper.packetevents.protocol.teleport.RelativeFlag; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityHeadLook; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class V1_21_3PacketFactory extends V1_20_2PacketFactory { + public V1_21_3PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } + + @Override + public void teleportEntity(Player player, PacketEntity entity) { + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerEntityTeleport(entity.getEntityId(), new EntityPositionData(npcLocationToVector(location), new Vector3d(0, 0, 0), location.getYaw(), location.getPitch()), RelativeFlag.NONE, false)); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java new file mode 100644 index 0000000..934a2ce --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_21_7PacketFactory.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.plugin.Plugin; + +public class V1_21_7PacketFactory extends V1_21_3PacketFactory { + public V1_21_7PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + super(scheduler, packetEvents, propertyRegistry, textSerializer, configManager); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java new file mode 100644 index 0000000..085f338 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/packets/V1_8PacketFactory.java @@ -0,0 +1,208 @@ +package lol.pyr.znpcsplus.packets; + +import com.github.retrooper.packetevents.PacketEventsAPI; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.player.Equipment; +import com.github.retrooper.packetevents.protocol.player.GameMode; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.server.*; +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.entity.PropertyHolder; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PacketEntity; +import lol.pyr.znpcsplus.entity.properties.attributes.AttributeProperty; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.util.NamedColor; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class V1_8PacketFactory implements PacketFactory { + protected final TaskScheduler scheduler; + protected final PacketEventsAPI packetEvents; + protected final EntityPropertyRegistryImpl propertyRegistry; + protected final LegacyComponentSerializer textSerializer; + protected ConfigManager configManager; + + public V1_8PacketFactory(TaskScheduler scheduler, PacketEventsAPI packetEvents, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, ConfigManager configManager) { + this.scheduler = scheduler; + this.packetEvents = packetEvents; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.configManager = configManager; + } + + @Override + public CompletableFuture spawnPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + return addTabPlayer(player, entity, properties).thenAccept(ignored -> { + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerSpawnPlayer(entity.getEntityId(), + entity.getUuid(), npcLocationToVector(location), location.getYaw(), location.getPitch(), Collections.emptyList())); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + sendAllMetadata(player, entity, properties); + sendAllAttributes(player, entity, properties); + scheduler.runLaterAsync(() -> removeTabPlayer(player, entity), configManager.getConfig().tabHideDelay()); + }); + } + + @Override + public void spawnEntity(Player player, PacketEntity entity, PropertyHolder properties) { + NpcLocation location = entity.getLocation(); + EntityType type = entity.getType(); + ClientVersion clientVersion = packetEvents.getServerManager().getVersion().toClientVersion(); + sendPacket(player, type.getLegacyId(clientVersion) == -1 ? + new WrapperPlayServerSpawnLivingEntity(entity.getEntityId(), entity.getUuid(), type, npcLocationToVector(location), + location.getYaw(), location.getPitch(), location.getYaw(), new Vector3d(), Collections.emptyList()) : + new WrapperPlayServerSpawnEntity(entity.getEntityId(), Optional.of(entity.getUuid()), entity.getType(), npcLocationToVector(location), + location.getPitch(), location.getYaw(), location.getYaw(), 0, Optional.empty())); + sendAllMetadata(player, entity, properties); + if (EntityTypes.isTypeInstanceOf(type, EntityTypes.LIVINGENTITY)) sendAllAttributes(player, entity, properties); + createTeam(player, entity, properties.getProperty(propertyRegistry.getByName("glow", NamedColor.class))); + } + + protected Vector3d npcLocationToVector(NpcLocation location) { + return new Vector3d(location.getX(), location.getY(), location.getZ()); + } + + @Override + public void destroyEntity(Player player, PacketEntity entity, PropertyHolder properties) { + sendPacket(player, new WrapperPlayServerDestroyEntities(entity.getEntityId())); + removeTeam(player, entity); + } + + @Override + public void teleportEntity(Player player, PacketEntity entity) { + NpcLocation location = entity.getLocation(); + sendPacket(player, new WrapperPlayServerEntityTeleport(entity.getEntityId(), npcLocationToVector(location), location.getYaw(), location.getPitch(), true)); + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(), location.getYaw())); + } + + @Override + public CompletableFuture addTabPlayer(Player player, PacketEntity entity, PropertyHolder properties) { + if (entity.getType() != EntityTypes.PLAYER) return CompletableFuture.completedFuture(null); + CompletableFuture future = new CompletableFuture<>(); + skinned(player, properties, new UserProfile(entity.getUuid(), Integer.toString(entity.getEntityId()))).thenAccept(profile -> { + sendPacket(player, new WrapperPlayServerPlayerInfo( + WrapperPlayServerPlayerInfo.Action.ADD_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData( + Component.text(configManager.getConfig().tabDisplayName().replace("{id}", Integer.toString(entity.getEntityId()))), + profile, GameMode.CREATIVE, 1))); + future.complete(null); + }); + return future; + } + + @Override + public void removeTabPlayer(Player player, PacketEntity entity) { + if (entity.getType() != EntityTypes.PLAYER) return; + sendPacket(player, new WrapperPlayServerPlayerInfo( + WrapperPlayServerPlayerInfo.Action.REMOVE_PLAYER, new WrapperPlayServerPlayerInfo.PlayerData(null, + new UserProfile(entity.getUuid(), null), null, -1))); + } + + @Override + public void createTeam(Player player, PacketEntity entity, NamedColor namedColor) { + sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.CREATE, new WrapperPlayServerTeams.ScoreBoardTeamInfo( + Component.text(" "), null, null, + WrapperPlayServerTeams.NameTagVisibility.NEVER, + WrapperPlayServerTeams.CollisionRule.NEVER, + namedColor == null ? NamedTextColor.WHITE : NamedTextColor.NAMES.value(namedColor.name().toLowerCase()), + WrapperPlayServerTeams.OptionData.NONE + ))); + sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.ADD_ENTITIES, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null, + entity.getType() == EntityTypes.PLAYER ? Integer.toString(entity.getEntityId()) : entity.getUuid().toString())); + } + + @Override + public void removeTeam(Player player, PacketEntity entity) { + sendPacket(player, new WrapperPlayServerTeams("npc_team_" + entity.getEntityId(), WrapperPlayServerTeams.TeamMode.REMOVE, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null)); + } + + @Override + public void sendAllMetadata(Player player, PacketEntity entity, PropertyHolder properties) { + Map> datas = new HashMap<>(); + for (EntityProperty property : properties.getAppliedProperties()) ((EntityPropertyImpl) property).apply(player, entity, false, datas); + sendMetadata(player, entity, new ArrayList<>(datas.values())); + } + + @Override + public void sendMetadata(Player player, PacketEntity entity, List> data) { + sendPacket(player, new WrapperPlayServerEntityMetadata(entity.getEntityId(), data)); + } + + @Override + public void sendHeadRotation(Player player, PacketEntity entity, float yaw, float pitch) { + sendPacket(player, new WrapperPlayServerEntityHeadLook(entity.getEntityId(),yaw)); + sendPacket(player, new WrapperPlayServerEntityRotation(entity.getEntityId(), yaw, pitch, true)); + } + + @Override + public void sendEquipment(Player player, PacketEntity entity, Equipment equipment) { + sendPacket(player, new WrapperPlayServerEntityEquipment(entity.getEntityId(), Collections.singletonList(equipment))); + } + + @Override + public void setPassengers(Player player, int vehicleEntityId, int... passengers) { + sendPacket(player, new WrapperPlayServerSetPassengers(vehicleEntityId, passengers)); + } + + protected void sendPacket(Player player, PacketWrapper packet) { + packetEvents.getPlayerManager().sendPacket(player, packet); + } + + protected CompletableFuture skinned(Player player, PropertyHolder properties, UserProfile profile) { + if (!properties.hasProperty(propertyRegistry.getByName("skin"))) return CompletableFuture.completedFuture(profile); + BaseSkinDescriptor descriptor = (BaseSkinDescriptor) properties.getProperty(propertyRegistry.getByName("skin", SkinDescriptor.class)); + if (descriptor.supportsInstant(player)) { + descriptor.fetchInstant(player).apply(profile); + return CompletableFuture.completedFuture(profile); + } + CompletableFuture future = new CompletableFuture<>(); + descriptor.fetch(player).thenAccept(skin -> { + if (skin != null) skin.apply(profile); + future.complete(profile); + }); + return future; + } + + protected void add(Map> map, EntityData data) { + map.put(data.getIndex(), data); + } + + @Override + public void sendHandSwing(Player player, PacketEntity entity, boolean offHand) { + sendPacket(player, new WrapperPlayServerEntityAnimation(entity.getEntityId(), offHand ? + WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_OFF_HAND : + WrapperPlayServerEntityAnimation.EntityAnimationType.SWING_MAIN_ARM)); + } + + @Override + public void sendAllAttributes(Player player, PacketEntity entity, PropertyHolder properties) { + List attributesList = new ArrayList<>(); + properties.getAppliedProperties() + .stream() + .filter(property -> property instanceof AttributeProperty) + .forEach(property -> ((AttributeProperty) property).apply(player, entity, false, attributesList)); + sendPacket(player, new WrapperPlayServerUpdateAttributes(entity.getEntityId(), attributesList)); + } + + @Override + public void sendAttribute(Player player, PacketEntity entity, WrapperPlayServerUpdateAttributes.Property property) { + sendPacket(player, new WrapperPlayServerUpdateAttributes(entity.getEntityId(), Collections.singletonList(property))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java new file mode 100644 index 0000000..65d0d44 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/ColorParser.java @@ -0,0 +1,28 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import org.bukkit.Color; + +import java.util.Deque; + +public class ColorParser extends ParserType { + public ColorParser(Message message) { + super(message); + } + + @Override + public Color parse(Deque deque) throws CommandExecutionException { + String color = deque.pop(); + if (color.startsWith("0x")) color = color.substring(2); + if (color.startsWith("&")) color = color.substring(1); + if (color.startsWith("#")) color = color.substring(1); + try { + return Color.fromRGB(Integer.parseInt(color, 16)); + } catch (IllegalArgumentException exception) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java new file mode 100644 index 0000000..75dac21 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EntityPropertyParser.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; + +import java.util.Deque; + +@SuppressWarnings("rawtypes") +public class EntityPropertyParser extends ParserType*/> { + private final EntityPropertyRegistryImpl propertyRegistry; + + public EntityPropertyParser(Message message, EntityPropertyRegistryImpl propertyRegistry) { + super(message); + this.propertyRegistry = propertyRegistry; + } + + @Override + public EntityPropertyImpl parse(Deque deque) throws CommandExecutionException { + EntityPropertyImpl property = propertyRegistry.getByName(deque.pop()); + if (property == null) throw new CommandExecutionException(); + return property; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java new file mode 100644 index 0000000..344c1b9 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/EnumParser.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; + +import java.util.Deque; + +public class EnumParser> extends ParserType { + + private final Class enumClass; + + public EnumParser(Class enumClass, Message message) { + super(message); + this.enumClass = enumClass; + } + + @Override + public T parse(Deque deque) throws CommandExecutionException { + try { + return Enum.valueOf(enumClass, deque.pop().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java new file mode 100644 index 0000000..68adcaa --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/InteractionTypeParser.java @@ -0,0 +1,24 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.api.interaction.InteractionType; + +import java.util.Deque; + +public class InteractionTypeParser extends ParserType { + public InteractionTypeParser(Message message) { + super(message); + } + + @Override + public InteractionType parse(Deque deque) throws CommandExecutionException { + try { + return InteractionType.valueOf(deque.pop().toUpperCase()); + } catch (IllegalArgumentException ignored) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java new file mode 100644 index 0000000..33451a0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NamedColorParser.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.NamedColor; +import java.util.Deque; + +public class NamedColorParser extends ParserType { + public NamedColorParser(Message message) { + super(message); + } + + @Override + public NamedColor parse(Deque deque) throws CommandExecutionException { + try { + return NamedColor.valueOf(deque.pop()); + } catch (IllegalArgumentException exception) { + throw new CommandExecutionException(); + } + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java new file mode 100644 index 0000000..bb452fa --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcEntryParser.java @@ -0,0 +1,26 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; + +import java.util.Deque; + +public class NpcEntryParser extends ParserType { + private final NpcRegistryImpl npcRegistry; + + public NpcEntryParser(NpcRegistryImpl npcRegistry, Message message) { + super(message); + this.npcRegistry = npcRegistry; + } + + @Override + public NpcEntryImpl parse(Deque deque) throws CommandExecutionException { + NpcEntryImpl entry = npcRegistry.getById(deque.pop()); + if (entry == null || !entry.isAllowCommandModification()) throw new CommandExecutionException(); + return entry; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java new file mode 100644 index 0000000..c0c4f17 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/NpcTypeParser.java @@ -0,0 +1,26 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.npc.NpcTypeImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; + +import java.util.Deque; + +public class NpcTypeParser extends ParserType { + private final NpcTypeRegistryImpl typeRegistry; + + public NpcTypeParser(Message message, NpcTypeRegistryImpl typeRegistry) { + super(message); + this.typeRegistry = typeRegistry; + } + + @Override + public NpcTypeImpl parse(Deque deque) throws CommandExecutionException { + NpcTypeImpl type = typeRegistry.getByName(deque.pop()); + if (type == null) throw new CommandExecutionException(); + return type; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java new file mode 100644 index 0000000..c47ee6d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/StringParser.java @@ -0,0 +1,19 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; + +import java.util.Deque; + +public class StringParser extends ParserType { + public StringParser(Message message) { + super(message); + } + + @Override + public String parse(Deque deque) throws CommandExecutionException { + return String.join(" ", deque); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java new file mode 100644 index 0000000..f2b9fb0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3fParser.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.Vector3f; + +import java.util.Deque; + +public class Vector3fParser extends ParserType { + public Vector3fParser(Message message) { + super(message); + } + + @Override + public Vector3f parse(Deque deque) throws CommandExecutionException { + try { + return new Vector3f( + Float.parseFloat(deque.pop()), + Float.parseFloat(deque.pop()), + Float.parseFloat(deque.pop())); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java new file mode 100644 index 0000000..286729d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/parsers/Vector3iParser.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.parsers; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.adventure.parse.ParserType; +import lol.pyr.director.common.command.CommandExecutionException; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.util.Vector3i; + +import java.util.Deque; + +public class Vector3iParser extends ParserType { + public Vector3iParser(Message message) { + super(message); + } + + @Override + public Vector3i parse(Deque deque) throws CommandExecutionException { + if (deque.size() == 0) { + return null; + } + try { + return new Vector3i( + Integer.parseInt(deque.pop()), + Integer.parseInt(deque.pop()), + Integer.parseInt(deque.pop())); + } catch (NumberFormatException e) { + throw new CommandExecutionException(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java new file mode 100644 index 0000000..c31b8ef --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionBuilder.java @@ -0,0 +1,126 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.google.common.collect.ImmutableList; +import lol.pyr.znpcsplus.reflection.types.ClassReflection; +import lol.pyr.znpcsplus.reflection.types.FieldReflection; +import lol.pyr.znpcsplus.reflection.types.MethodReflection; + +import java.util.ArrayList; + +public class ReflectionBuilder { + private final String reflectionPackage; + private String fieldName; + private String additionalData; + private final ArrayList className = new ArrayList<>(); + private final ArrayList methods = new ArrayList<>(); + private final ArrayList[]> parameterTypes = new ArrayList<>(); + private Class expectType; + private boolean strict = true; + + public ReflectionBuilder() { + this(""); + } + + public ReflectionBuilder(Class clazz) { + this(""); + withClassName(clazz); + } + + public ReflectionBuilder(String reflectionPackage) { + this(reflectionPackage, "", "", null); + } + + protected ReflectionBuilder(String reflectionPackage, String fieldName, String additionalData, Class expectType) { + this.reflectionPackage = reflectionPackage; + this.fieldName = fieldName; + this.additionalData = additionalData; + this.expectType = expectType; + } + + public ReflectionBuilder withClassName(String className) { + this.className.add(ReflectionPackage.joinWithDot(reflectionPackage, PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_17) ? additionalData : "", className)); + return this; + } + + public ReflectionBuilder withRawClassName(String className) { + this.className.add(className); + return this; + } + + public ReflectionBuilder withClassName(Class clazz) { + if (clazz != null) className.add(clazz.getName()); + return this; + } + + public ReflectionBuilder withMethodName(String methodName) { + this.methods.add(methodName); + return this; + } + + public ReflectionBuilder withFieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + public ReflectionBuilder withSubClass(String additionalData) { + this.additionalData = additionalData; + return this; + } + + public ReflectionBuilder withParameterTypes(Class... types) { + this.parameterTypes.add(types); + return this; + } + + public ReflectionBuilder withExpectResult(Class expectType) { + this.expectType = expectType; + return this; + } + + public ReflectionBuilder setStrict(boolean strict) { + this.strict = strict; + return this; + } + + public boolean isStrict() { + return strict; + } + + public Class getExpectType() { + return expectType; + } + + public ImmutableList[]> getParameterTypes() { + return ImmutableList.copyOf(this.parameterTypes); + } + + public ImmutableList getClassNames() { + return ImmutableList.copyOf(this.className); + } + + public ImmutableList getMethods() { + return ImmutableList.copyOf(this.methods); + } + + public String getPackage() { + return reflectionPackage; + } + + public String getFieldName() { + return fieldName; + } + + public MethodReflection toMethodReflection() { + return new MethodReflection(this); + } + + public ClassReflection toClassReflection() { + return new ClassReflection(this); + } + + public FieldReflection toFieldReflection() { + return new FieldReflection(this); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java new file mode 100644 index 0000000..01c4eaa --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionLazyLoader.java @@ -0,0 +1,55 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Logger; + +public abstract class ReflectionLazyLoader { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Reflection"); + protected final List possibleClassNames; + protected List> reflectionClasses = new ArrayList<>(); + protected final boolean strict; + private T cached; + private boolean loaded = false; + + protected ReflectionLazyLoader(ReflectionBuilder builder) { + this(builder.getClassNames(), builder.isStrict()); + } + + protected ReflectionLazyLoader(List possibleClassNames, boolean strict) { + this.possibleClassNames = possibleClassNames; + this.strict = strict; + for (String name : possibleClassNames) try { + reflectionClasses.add(Class.forName(name)); + } catch (ClassNotFoundException ignored) {} + } + + public T get() { + if (this.loaded) return this.cached; + try { + if (this.reflectionClasses.size() == 0) throw new ClassNotFoundException("No class found: " + possibleClassNames); + T eval = (this.cached != null) ? this.cached : (this.cached = load()); + if (eval == null) throw new RuntimeException("Returned value is null"); + } catch (Throwable throwable) { + if (strict) { + logger.warning(" ----- REFLECTION FAILURE DEBUG INFORMATION, REPORT THIS ON THE ZNPCSPLUS GITHUB ----- "); + logger.warning(getClass().getSimpleName() + " failed!"); + logger.warning("Class Names: " + possibleClassNames); + logger.warning("Reflection Type: " + getClass().getCanonicalName()); + logger.warning("Server Version: " + PacketEvents.getAPI().getServerManager().getVersion().name()); + printDebugInfo(logger::warning); + logger.warning("Exception:"); + throwable.printStackTrace(); + logger.warning(" ----- REFLECTION FAILURE DEBUG INFORMATION, REPORT THIS ON THE ZNPCSPLUS GITHUB ----- "); + } + } + this.loaded = true; + return this.cached; + } + + protected abstract T load() throws Exception; + protected void printDebugInfo(Consumer logger) {} +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java new file mode 100644 index 0000000..4fb169f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/ReflectionPackage.java @@ -0,0 +1,40 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import org.bukkit.Bukkit; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A class containing getAll of the packages of the server jar that we import classes from. + * Every line has a check for the "flattened" variable due to the fact that server jars + * pre-1.17 had all of their classes "flattened" into one package. + */ +public class ReflectionPackage { + private static final String VERSION = generateVersion(); + public static final String BUKKIT = "org.bukkit.craftbukkit" + VERSION; + private static final boolean flattened = !PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_17); + + /** + * Check if the classes are flattened, if so we need to add the version string into the + * package string which is another quirk of the old server jars. + */ + public static final String MINECRAFT = joinWithDot("net.minecraft", flattened ? "server" + VERSION : ""); + public static final String ENTITY = flattened ? MINECRAFT : joinWithDot(MINECRAFT, "world.entity"); + + public static String joinWithDot(String... parts) { + return Arrays.stream(parts) + .filter(Objects::nonNull) + .filter(p -> !p.isEmpty()) + .collect(Collectors.joining(".")); + } + + private static String generateVersion() { + String[] parts = Bukkit.getServer().getClass().getPackage().getName().split("\\."); + if (parts.length > 3) return "." + parts[3]; + return ""; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java new file mode 100644 index 0000000..b6e2dde --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/Reflections.java @@ -0,0 +1,253 @@ +package lol.pyr.znpcsplus.reflection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import lol.pyr.znpcsplus.reflection.types.FieldReflection; +import lol.pyr.znpcsplus.util.FoliaUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * Class containing getAll of the lazy-loaded reflections that the plugin + * uses to access inaccessible components of the server jar. + */ +public final class Reflections { + + /* + * Game profile methods used for obtaining raw skin data of online players + */ + + public static final Class GAME_PROFILE_CLASS = + new ReflectionBuilder() + .withRawClassName("com.mojang.authlib.GameProfile") + .toClassReflection().get(); + + public static final Class ENTITY_HUMAN_CLASS = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withSubClass("player") + .withClassName("EntityHuman") + .toClassReflection().get(); + + public static final ReflectionLazyLoader GET_PLAYER_HANDLE_METHOD = + new ReflectionBuilder(ReflectionPackage.BUKKIT) + .withClassName("entity.CraftPlayer") + .withClassName("entity.CraftHumanEntity") + .withMethodName("getHandle") + .withExpectResult(ENTITY_HUMAN_CLASS) + .toMethodReflection(); + + public static final ReflectionLazyLoader GET_PROFILE_METHOD = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName(ENTITY_HUMAN_CLASS) + .withExpectResult(GAME_PROFILE_CLASS) + .toMethodReflection(); + + public static final Class PROPERTY_MAP_CLASS = + new ReflectionBuilder() + .withRawClassName("com.mojang.authlib.properties.PropertyMap") + .toClassReflection().get(); + + public static final ReflectionLazyLoader GET_PROPERTY_MAP_METHOD = + new ReflectionBuilder(GAME_PROFILE_CLASS) + .withMethodName("getProperties") + .withExpectResult(PROPERTY_MAP_CLASS) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_MAP_VALUES_METHOD = + new ReflectionBuilder() + .withClassName(PROPERTY_MAP_CLASS.getSuperclass()) + .withMethodName("values") + .withExpectResult(Collection.class) + .toMethodReflection(); + + public static final Class PROPERTY_CLASS = + new ReflectionBuilder() + .withRawClassName("com.mojang.authlib.properties.Property") + .toClassReflection().get(); + + private static final boolean v1_20_2 = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2); + + public static final ReflectionLazyLoader PROPERTY_GET_NAME_METHOD = + new ReflectionBuilder(PROPERTY_CLASS) + .withMethodName("getName") + .withExpectResult(String.class) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_NAME_FIELD = + new ReflectionBuilder(PROPERTY_CLASS) + .withFieldName("name") + .withExpectResult(String.class) + .setStrict(v1_20_2) + .toFieldReflection(); + + public static final ReflectionLazyLoader PROPERTY_GET_VALUE_METHOD = + new ReflectionBuilder(PROPERTY_CLASS) + .withMethodName("getValue") + .withExpectResult(String.class) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_VALUE_FIELD = + new ReflectionBuilder(PROPERTY_CLASS) + .withFieldName("value") + .withExpectResult(String.class) + .setStrict(v1_20_2) + .toFieldReflection(); + + public static final ReflectionLazyLoader PROPERTY_GET_SIGNATURE_METHOD = + new ReflectionBuilder(PROPERTY_CLASS) + .withMethodName("getSignature") + .withExpectResult(String.class) + .toMethodReflection(); + + public static final ReflectionLazyLoader PROPERTY_SIGNATURE_FIELD = + new ReflectionBuilder(PROPERTY_CLASS) + .withFieldName("signature") + .withExpectResult(String.class) + .setStrict(v1_20_2) + .toFieldReflection(); + + /* + * These methods are used for reserving entity ids so regular Minecraft + * entity packets don't interfere with our packet-based entities + */ + + public static final Class ENTITY_CLASS = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName("Entity") + .toClassReflection().get(); + public static final FieldReflection.ValueModifier ENTITY_ID_MODIFIER = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName(ENTITY_CLASS) + .withFieldName("entityCount") + .setStrict(!PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14)) + .toFieldReflection() + .toStaticValueModifier(int.class); + + public static final ReflectionLazyLoader ATOMIC_ENTITY_ID_FIELD = + new ReflectionBuilder(ReflectionPackage.ENTITY) + .withClassName(ENTITY_CLASS) + .withFieldName("entityCount") + .withFieldName("d") + .withFieldName("c") + .withExpectResult(AtomicInteger.class) + .setStrict(PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14)) + .toFieldReflection() + .toStaticValueLoader(AtomicInteger.class); + + /* + * All of these folia methods need to be reflected because folia is strictly + * available on the newest java versions but we need to keep support for Java 8 + */ + + public static final Class ASYNC_SCHEDULER_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("AsyncScheduler") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final Class GLOBAL_REGION_SCHEDULER_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("GlobalRegionScheduler") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final Class REGION_SCHEDULER_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("RegionScheduler") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final Class SCHEDULED_TASK_CLASS = + new ReflectionBuilder("io.papermc.paper.threadedregions.scheduler") + .withClassName("ScheduledTask") + .setStrict(FoliaUtil.isFolia()) + .toClassReflection().get(); + + public static final ReflectionLazyLoader FOLIA_GET_ASYNC_SCHEDULER = + new ReflectionBuilder(Bukkit.class) + .withMethodName("getAsyncScheduler") + .withExpectResult(ASYNC_SCHEDULER_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_GET_GLOBAL_REGION_SCHEDULER = + new ReflectionBuilder(Bukkit.class) + .withMethodName("getGlobalRegionScheduler") + .withExpectResult(GLOBAL_REGION_SCHEDULER_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_GET_REGION_SCHEDULER = + new ReflectionBuilder(Bukkit.class) + .withMethodName("getRegionScheduler") + .withExpectResult(REGION_SCHEDULER_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_NOW_ASYNC = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("runNow") + .withParameterTypes(Plugin.class, Consumer.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_DELAYED_ASYNC = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("runDelayed") + .withParameterTypes(Plugin.class, Consumer.class, long.class, TimeUnit.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_AT_FIXED_RATE_ASYNC = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("runAtFixedRate") + .withParameterTypes(Plugin.class, Consumer.class, long.class, long.class, TimeUnit.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_RUN_NOW_GLOBAL = + new ReflectionBuilder(GLOBAL_REGION_SCHEDULER_CLASS) + .withMethodName("runNow") + .withParameterTypes(Plugin.class, Consumer.class) + .withExpectResult(SCHEDULED_TASK_CLASS) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_EXECUTE_REGION = + new ReflectionBuilder(REGION_SCHEDULER_CLASS) + .withMethodName("execute") + .withParameterTypes(Plugin.class, Location.class, Runnable.class) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_CANCEL_ASYNC_TASKS = + new ReflectionBuilder(ASYNC_SCHEDULER_CLASS) + .withMethodName("cancelTasks") + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_CANCEL_GLOBAL_TASKS = + new ReflectionBuilder(GLOBAL_REGION_SCHEDULER_CLASS) + .withMethodName("cancelTasks") + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); + + public static final ReflectionLazyLoader FOLIA_TELEPORT_ASYNC = + new ReflectionBuilder(Entity.class) + .withMethodName("teleportAsync") + .withParameterTypes(Location.class) + .setStrict(FoliaUtil.isFolia()) + .toMethodReflection(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java new file mode 100644 index 0000000..de3f106 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/ClassReflection.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.reflection.types; + +import lol.pyr.znpcsplus.reflection.ReflectionBuilder; +import lol.pyr.znpcsplus.reflection.ReflectionLazyLoader; + +public class ClassReflection extends ReflectionLazyLoader> { + public ClassReflection(ReflectionBuilder reflectionBuilder) { + super(reflectionBuilder); + } + + protected Class load() { + return this.reflectionClasses.size() > 0 ? this.reflectionClasses.get(0) : null; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java new file mode 100644 index 0000000..67c39fe --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/FieldReflection.java @@ -0,0 +1,123 @@ +package lol.pyr.znpcsplus.reflection.types; + +import lol.pyr.znpcsplus.reflection.ReflectionLazyLoader; +import lol.pyr.znpcsplus.reflection.ReflectionBuilder; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Consumer; + +public class FieldReflection extends ReflectionLazyLoader { + private final String fieldName; + private final Class expectType; + + public FieldReflection(ReflectionBuilder builder) { + super(builder); + this.fieldName = builder.getFieldName(); + this.expectType = builder.getExpectType(); + } + + protected Field load() { + if (fieldName != null && fieldName.length() > 0) for (Class clazz : this.reflectionClasses) { + Field field = loadByName(clazz); + if (field != null) return field; + } + if (expectType != null) for (Class clazz : this.reflectionClasses) { + Field field = loadByType(clazz); + if (field != null) return field; + } + return null; + } + + private Field loadByName(Class clazz) { + try { + Field field = clazz.getDeclaredField(fieldName); + if (expectType != null && !field.getType().equals(expectType)) return null; + field.setAccessible(true); + return field; + } catch (NoSuchFieldException ignored) {} + return null; + } + + private Field loadByType(Class clazz) { + for (Field field : clazz.getDeclaredFields()) if (field.getType() == expectType) { + field.setAccessible(true); + return field; + } + return null; + } + + @Override + protected void printDebugInfo(Consumer logger) { + logger.accept("Field Name: " + fieldName); + logger.accept("Field Type: " + expectType); + } + + public ValueReflection toStaticValueLoader() { + return toStaticValueLoader(Object.class); + } + + @SuppressWarnings("unused") + public ValueReflection toStaticValueLoader(Class valueType) { + return new ValueReflection<>(this, possibleClassNames, null, strict); + } + + @SuppressWarnings("unused") + public ValueReflection toValueLoader(Object obj, Class valueType) { + return new ValueReflection<>(this, possibleClassNames, obj, strict); + } + + @SuppressWarnings("unused") + public ValueModifier toStaticValueModifier(Class valueType) { + return new ValueModifier<>(this, possibleClassNames, null, strict); + } + + @SuppressWarnings("unused") + public ValueModifier toValueModifier(Object obj, Class valueType) { + return new ValueModifier<>(this, possibleClassNames, obj, strict); + } + + public static class ValueReflection extends ReflectionLazyLoader { + protected final Object obj; + protected final FieldReflection fieldReflection; + + private ValueReflection(FieldReflection fieldReflection, List className, Object obj, boolean strict) { + super(className, strict); + this.obj = obj; + this.fieldReflection = fieldReflection; + } + + @SuppressWarnings("unchecked") + protected T load() throws IllegalAccessException, NoSuchFieldException, ClassCastException { + return (T) this.fieldReflection.get().get(obj); + } + + @Override + protected void printDebugInfo(Consumer logger) { + fieldReflection.printDebugInfo(logger); + } + } + + public static class ValueModifier extends ValueReflection { + private ValueModifier(FieldReflection fieldReflection, List className, Object obj, boolean strict) { + super(fieldReflection, className, obj, strict); + } + + @Override + public T get() { + try { + return load(); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + public void set(T value) { + try { + fieldReflection.get().set(obj, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java new file mode 100644 index 0000000..c758727 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/reflection/types/MethodReflection.java @@ -0,0 +1,63 @@ +package lol.pyr.znpcsplus.reflection.types; + +import com.google.common.collect.ImmutableList; +import lol.pyr.znpcsplus.reflection.ReflectionBuilder; +import lol.pyr.znpcsplus.reflection.ReflectionLazyLoader; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Consumer; + +public class MethodReflection extends ReflectionLazyLoader { + private final ImmutableList methods; + private final ImmutableList[]> parameterTypes; + private final Class expectType; + + public MethodReflection(ReflectionBuilder builder) { + super(builder); + this.methods = builder.getMethods(); + this.expectType = builder.getExpectType(); + this.parameterTypes = builder.getParameterTypes(); + } + + protected Method load() { + Map> imperfectMatches = new HashMap<>(); + for (Class clazz : this.reflectionClasses) { + Method method = load(clazz, imperfectMatches); + if (method != null) return method; + } + for (int i = 2; i > 0; i--) if (imperfectMatches.containsKey(i)) { + return imperfectMatches.get(i).get(0); + } + return null; + } + + private Method load(Class clazz, Map> imperfectMatches) { + for (Method method : clazz.getDeclaredMethods()) { + int matches = 0; + if (expectType != null) { + if (!method.getReturnType().equals(expectType)) continue; + matches++; + } + if (parameterTypes.size() > 0) out: for (Class[] possible : parameterTypes) { + if (method.getParameterCount() != possible.length) continue; + for (int i = 0; i < possible.length; i++) if (!method.getParameterTypes()[i].equals(possible[i])) continue out; + matches++; + } + if (methods.contains(method.getName())) { + matches++; + } + if (matches == 3) return method; + else imperfectMatches.computeIfAbsent(matches, i -> new ArrayList<>()).add(method); + } + return null; + } + + @Override + protected void printDebugInfo(Consumer logger) { + logger.accept("Expected Return Type: " + expectType); + logger.accept("Possible method names: " + methods); + logger.accept("Possible Parameter Type Combinations:"); + for (Class[] possible : parameterTypes) logger.accept(Arrays.toString(possible)); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java new file mode 100644 index 0000000..0436b39 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/FoliaScheduler.java @@ -0,0 +1,92 @@ +package lol.pyr.znpcsplus.scheduling; + +import lol.pyr.znpcsplus.reflection.Reflections; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class FoliaScheduler extends TaskScheduler { + public FoliaScheduler(Plugin plugin) { + super(plugin); + } + + @Override + public void schedulePlayerChat(Player player, String chat) { + try { + Object scheduler = Reflections.FOLIA_GET_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_EXECUTE_REGION.get().invoke(scheduler, plugin, player.getLocation(), (Runnable) () -> player.chat(chat)); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public void schedulePlayerCommand(Player player, String command) { + try { + Object scheduler = Reflections.FOLIA_GET_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_EXECUTE_REGION.get().invoke(scheduler, plugin, player.getLocation(), (Runnable) () -> Bukkit.dispatchCommand(player, command)); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public void runSyncGlobal(Runnable runnable) { + try { + Object scheduler = Reflections.FOLIA_GET_GLOBAL_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_NOW_GLOBAL.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void runAsyncGlobal(Runnable runnable) { + try { + Object scheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_NOW_ASYNC.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void runLaterAsync(Runnable runnable, long delay) { + try { + Object scheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_DELAYED_ASYNC.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run(), delay * 50, TimeUnit.MILLISECONDS); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void runDelayedTimerAsync(Runnable runnable, long delay, long interval) { + try { + Object scheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_RUN_AT_FIXED_RATE_ASYNC.get().invoke(scheduler, plugin, (Consumer) o -> runnable.run(), delay * 50, interval * 50, TimeUnit.MILLISECONDS); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void cancelAll() { + try { + Object asyncScheduler = Reflections.FOLIA_GET_ASYNC_SCHEDULER.get().invoke(null); + Reflections.FOLIA_CANCEL_ASYNC_TASKS.get().invoke(asyncScheduler, plugin); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + try { + Object globalScheduler = Reflections.FOLIA_GET_GLOBAL_REGION_SCHEDULER.get().invoke(null); + Reflections.FOLIA_CANCEL_GLOBAL_TASKS.get().invoke(globalScheduler, plugin); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} + diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java new file mode 100644 index 0000000..949218a --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/SpigotScheduler.java @@ -0,0 +1,46 @@ +package lol.pyr.znpcsplus.scheduling; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class SpigotScheduler extends TaskScheduler { + public SpigotScheduler(Plugin plugin) { + super(plugin); + } + + @Override + public void schedulePlayerChat(Player player, String chat) { + runSyncGlobal(() -> player.chat(chat)); + } + + @Override + public void schedulePlayerCommand(Player player, String command) { + runSyncGlobal(() -> Bukkit.dispatchCommand(player, command)); + } + + @Override + public void runSyncGlobal(Runnable runnable) { + Bukkit.getScheduler().runTask(plugin, runnable); + } + + @Override + public void runAsyncGlobal(Runnable runnable) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable); + } + + @Override + public void runLaterAsync(Runnable runnable, long delay) { + Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay); + } + + @Override + public void runDelayedTimerAsync(Runnable runnable, long delay, long interval) { + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, interval); + } + + @Override + public void cancelAll() { + Bukkit.getScheduler().cancelTasks(plugin); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java new file mode 100644 index 0000000..fca0bf0 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/scheduling/TaskScheduler.java @@ -0,0 +1,20 @@ +package lol.pyr.znpcsplus.scheduling; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public abstract class TaskScheduler { + protected final Plugin plugin; + + public TaskScheduler(Plugin plugin) { + this.plugin = plugin; + } + + public abstract void schedulePlayerChat(Player player, String message); + public abstract void schedulePlayerCommand(Player player, String command); + public abstract void runSyncGlobal(Runnable runnable); + public abstract void runAsyncGlobal(Runnable runnable); + public abstract void runLaterAsync(Runnable runnable, long delay); + public abstract void runDelayedTimerAsync(Runnable runnable, long delay, long interval); + public abstract void cancelAll(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java new file mode 100644 index 0000000..71a7a95 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/NpcSerializerRegistryImpl.java @@ -0,0 +1,33 @@ +package lol.pyr.znpcsplus.serialization; + +import lol.pyr.znpcsplus.api.serialization.NpcSerializer; +import lol.pyr.znpcsplus.api.serialization.NpcSerializerRegistry; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.HashMap; +import java.util.Map; + +public class NpcSerializerRegistryImpl implements NpcSerializerRegistry { + private final Map, NpcSerializer> serializerMap = new HashMap<>(); + + public NpcSerializerRegistryImpl(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + registerSerializer(YamlConfiguration.class, new YamlSerializer(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer)); + } + + @SuppressWarnings("unchecked") + @Override + public NpcSerializer getSerializer(Class clazz) { + return (NpcSerializer) serializerMap.get(clazz); + } + + @Override + public void registerSerializer(Class clazz, NpcSerializer serializer) { + serializerMap.put(clazz, serializer); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java new file mode 100644 index 0000000..094a493 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/serialization/YamlSerializer.java @@ -0,0 +1,154 @@ +package lol.pyr.znpcsplus.serialization; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import lol.pyr.znpcsplus.api.serialization.NpcSerializer; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class YamlSerializer implements NpcSerializer { + private final static Logger logger = Logger.getLogger("YamlSerializer"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + + public YamlSerializer(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + } + + @Override + public YamlConfiguration serialize(NpcEntry entry) { + YamlConfiguration config = new YamlConfiguration(); + config.set("id", entry.getId()); + config.set("is-processed", entry.isProcessed()); + config.set("allow-commands", entry.isAllowCommandModification()); + config.set("save", entry.isSave()); + + NpcImpl npc = (NpcImpl) entry.getNpc(); + config.set("enabled", npc.isEnabled()); + config.set("uuid", npc.getUuid().toString()); + config.set("world", npc.getWorldName()); + config.set("location", serializeLocation(npc.getLocation())); + config.set("type", npc.getType().getName()); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + Bukkit.getLogger().log(Level.WARNING, "Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + config.set("properties." + property.getName(), serializer.UNSAFE_serialize(npc.getProperty(property))); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + HologramImpl hologram = npc.getHologram(); + if (hologram.getOffset() != 0.0) config.set("hologram.offset", hologram.getOffset()); + if (hologram.getRefreshDelay() != -1) config.set("hologram.refresh-delay", hologram.getRefreshDelay()); + List lines = new ArrayList<>(npc.getHologram().getLines().size()); + for (int i = 0; i < hologram.getLines().size(); i++) { + lines.add(hologram.getLine(i)); + } + config.set("hologram.lines", lines); + config.set("actions", npc.getActions().stream() + .map(actionRegistry::serialize) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + + return config; + } + + @Override + public NpcEntry deserialize(YamlConfiguration config) { + UUID uuid = config.contains("uuid") ? UUID.fromString(config.getString("uuid")) : UUID.randomUUID(); + NpcImpl npc = new NpcImpl(uuid, propertyRegistry, configManager, packetFactory, textSerializer, config.getString("world"), + typeRegistry.getByName(config.getString("type")), deserializeLocation(config.getConfigurationSection("location"))); + + if (config.isBoolean("enabled")) npc.setEnabled(config.getBoolean("enabled")); + + ConfigurationSection properties = config.getConfigurationSection("properties"); + if (properties != null) { + for (String key : properties.getKeys(false)) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + Bukkit.getLogger().log(Level.WARNING, "Unknown property '" + key + "' for npc '" + config.getString("id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + Bukkit.getLogger().log(Level.WARNING, "Unknown serializer for property '" + key + "' for npc '" + config.getString("id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(properties.getString(key)); + if (value == null) { + Bukkit.getLogger().log(Level.WARNING, "Failed to deserialize property '" + key + "' for npc '" + config.getString("id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + npc.UNSAFE_setProperty(property, value); + } + } + HologramImpl hologram = npc.getHologram(); + hologram.setOffset(config.getDouble("hologram.offset", 0.0)); + hologram.setRefreshDelay(config.getLong("hologram.refresh-delay", -1)); + for (String line : config.getStringList("hologram.lines")) hologram.addLine(line); + for (String s : config.getStringList("actions")) npc.addAction(actionRegistry.deserialize(s)); + + NpcEntryImpl entry = new NpcEntryImpl(config.getString("id"), npc); + entry.setProcessed(config.getBoolean("is-processed")); + entry.setAllowCommandModification(config.getBoolean("allow-commands")); + entry.setSave(config.getBoolean("save", true)); + + return entry; + } + + public NpcLocation deserializeLocation(ConfigurationSection section) { + return new NpcLocation( + section.getDouble("x"), + section.getDouble("y"), + section.getDouble("z"), + (float) section.getDouble("yaw"), + (float) section.getDouble("pitch") + ); + } + + public YamlConfiguration serializeLocation(NpcLocation location) { + YamlConfiguration config = new YamlConfiguration(); + config.set("x", location.getX()); + config.set("y", location.getY()); + config.set("z", location.getZ()); + config.set("yaw", location.getYaw()); + config.set("pitch", location.getPitch()); + return config; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java new file mode 100644 index 0000000..1fe1623 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/BaseSkinDescriptor.java @@ -0,0 +1,44 @@ +package lol.pyr.znpcsplus.skin; + +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.UUIDFetchingDescriptor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface BaseSkinDescriptor extends SkinDescriptor { + CompletableFuture fetch(Player player); + SkinImpl fetchInstant(Player player); + boolean supportsInstant(Player player); + String serialize(); + + static BaseSkinDescriptor deserialize(MojangSkinCache skinCache, String str) { + String[] arr = str.split(";"); + if (arr[0].equalsIgnoreCase("mirror")) return new MirrorDescriptor(skinCache); + else if (arr[0].equalsIgnoreCase("fetching-uuid")) { + String value = String.join(";", Arrays.copyOfRange(arr, 1, arr.length)); + return new UUIDFetchingDescriptor(skinCache, UUID.fromString(value)); + } + else if(arr[0].equalsIgnoreCase("fetching")) { + String value = String.join(";", Arrays.copyOfRange(arr, 1, arr.length)); + return new NameFetchingDescriptor(skinCache, value); + } + else if (arr[0].equalsIgnoreCase("prefetched")) { + List properties = new ArrayList<>(); + for (int i = 0; i < (arr.length - 1) / 3; i++) { + properties.add(new TextureProperty(arr[i + 1], arr[i + 2], arr[i + 3])); + } + return new PrefetchedDescriptor(new SkinImpl(properties)); + } + throw new IllegalArgumentException("Unknown SkinDescriptor type!"); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java new file mode 100644 index 0000000..10a8cea --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinDescriptorFactoryImpl.java @@ -0,0 +1,67 @@ +package lol.pyr.znpcsplus.skin; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.api.skin.SkinDescriptorFactory; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.skin.descriptor.NameFetchingDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.MirrorDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.PrefetchedDescriptor; +import lol.pyr.znpcsplus.skin.descriptor.UUIDFetchingDescriptor; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +public class SkinDescriptorFactoryImpl implements SkinDescriptorFactory { + private final MojangSkinCache skinCache; + private final MirrorDescriptor mirrorDescriptor; + + public SkinDescriptorFactoryImpl(MojangSkinCache skinCache) { + this.skinCache = skinCache; + mirrorDescriptor = new MirrorDescriptor(skinCache); + } + + @Override + public SkinDescriptor createMirrorDescriptor() { + return mirrorDescriptor; + } + + @Override + public SkinDescriptor createRefreshingDescriptor(String playerName) { + return new NameFetchingDescriptor(skinCache, playerName); + } + + @Override + public SkinDescriptor createRefreshingDescriptor(UUID playerUUID) { + return new UUIDFetchingDescriptor(skinCache, playerUUID); + } + + @Override + public SkinDescriptor createStaticDescriptor(String playerName) { + return PrefetchedDescriptor.forPlayer(skinCache, playerName).join(); + } + + @Override + public SkinDescriptor createStaticDescriptor(String texture, String signature) { + return new PrefetchedDescriptor(new SkinImpl(texture, signature)); + } + + @Override + public SkinDescriptor createUrlDescriptor(String url, String variant) { + try { + return createUrlDescriptor(new URL(url), variant); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public SkinDescriptor createUrlDescriptor(URL url, String variant) { + return PrefetchedDescriptor.fromUrl(skinCache, url, variant).join(); + } + + @Override + public SkinDescriptor createFileDescriptor(String path) { + return PrefetchedDescriptor.fromFile(skinCache, path).join(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java new file mode 100644 index 0000000..66bc999 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/SkinImpl.java @@ -0,0 +1,91 @@ +package lol.pyr.znpcsplus.skin; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lol.pyr.znpcsplus.api.skin.Skin; +import lol.pyr.znpcsplus.reflection.Reflections; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class SkinImpl implements Skin { + private final long timestamp = System.currentTimeMillis(); + private final List properties; + private static final boolean V1_20_2 = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_2); + + public SkinImpl(String texture, String signature) { + properties = new ArrayList<>(1); + properties.add(new TextureProperty("textures", texture, signature)); + } + + public SkinImpl(Collection properties) { + this.properties = new ArrayList<>(properties); + } + + public SkinImpl(Object propertyMap) { + this.properties = new ArrayList<>(); + try { + Collection properties = (Collection) Reflections.PROPERTY_MAP_VALUES_METHOD.get().invoke(propertyMap); + for (Object property : properties) { + String name; + String value; + String signature; + if (V1_20_2) { + name = (String) Reflections.PROPERTY_NAME_FIELD.get().get(property); + value = (String) Reflections.PROPERTY_VALUE_FIELD.get().get(property); + signature = (String) Reflections.PROPERTY_SIGNATURE_FIELD.get().get(property); + } else { + name = (String) Reflections.PROPERTY_GET_NAME_METHOD.get().invoke(property); + value = (String) Reflections.PROPERTY_GET_VALUE_METHOD.get().invoke(property); + signature = (String) Reflections.PROPERTY_GET_SIGNATURE_METHOD.get().invoke(property); + } + this.properties.add(new TextureProperty(name, value, signature)); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public SkinImpl(JsonObject obj) { + properties = new ArrayList<>(); + for (JsonElement e : obj.get("properties").getAsJsonArray()) { + JsonObject o = e.getAsJsonObject(); + properties.add(new TextureProperty(o.get("name").getAsString(), o.get("value").getAsString(), o.has("signature") ? o.get("signature").getAsString() : null)); + } + } + + public UserProfile apply(UserProfile profile) { + profile.setTextureProperties(properties); + return profile; + } + + public List getProperties() { + return properties; + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > 60000L; + } + + @Override + public String getTexture() { + for (TextureProperty property : properties) + if (property.getName().equalsIgnoreCase("textures")) + return property.getValue(); + return null; + } + + @Override + public String getSignature() { + for (TextureProperty property : properties) + if (property.getName().equalsIgnoreCase("textures")) + return property.getSignature(); + return null; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java new file mode 100644 index 0000000..cf60441 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/CachedId.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.skin.cache; + +public class CachedId { + private final long timestamp = System.currentTimeMillis(); + private final String id; + + public CachedId(String id) { + this.id = id; + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > 60000L; + } + + public String getId() { + return id; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java new file mode 100644 index 0000000..3fe0fd3 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/MojangSkinCache.java @@ -0,0 +1,277 @@ +package lol.pyr.znpcsplus.skin.cache; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.reflection.Reflections; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.util.FutureUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +public class MojangSkinCache { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Skin Cache"); + + private final ConfigManager configManager; + + private final Map cache = new ConcurrentHashMap<>(); + private final Map idCache = new ConcurrentHashMap<>(); + private final File skinsFolder; + + public MojangSkinCache(ConfigManager configManager, File skinsFolder) { + this.configManager = configManager; + this.skinsFolder = skinsFolder; + if (!skinsFolder.exists()) skinsFolder.mkdirs(); + } + + public void cleanCache() { + for (Map.Entry entry : cache.entrySet()) if (entry.getValue().isExpired()) cache.remove(entry.getKey()); + for (Map.Entry entry : idCache.entrySet()) if (entry.getValue().isExpired()) cache.remove(entry.getKey()); + } + + public CompletableFuture fetchByName(String name) { + Player player = Bukkit.getPlayerExact(name); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (idCache.containsKey(name.toLowerCase())) return fetchByUUID(idCache.get(name.toLowerCase()).getId()); + + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL url = parseUrl("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + name); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("errorMessage")) return fetchByNameFallback(name).join(); + String id = obj.get("id").getAsString(); + idCache.put(name.toLowerCase(), new CachedId(id)); + SkinImpl skin = fetchByUUID(id).join(); + if (skin == null) return fetchByNameFallback(name).join(); + return skin; + } + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get uuid from player name, trying to use fallback server:"); + exception.printStackTrace(); + } + return fetchByNameFallback(name).join(); + } finally { + if (connection != null) connection.disconnect(); + } + }); + } + + public CompletableFuture fetchByNameFallback(String name) { + Player player = Bukkit.getPlayerExact(name); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (idCache.containsKey(name.toLowerCase())) return fetchByUUID(idCache.get(name.toLowerCase()).getId()); + + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL url = parseUrl("https://api.ashcon.app/mojang/v2/user/" + name); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("error")) return null; + String uuid = obj.get("uuid").getAsString(); + idCache.put(name.toLowerCase(), new CachedId(uuid)); + JsonObject textures = obj.get("textures").getAsJsonObject(); + String value = textures.get("raw").getAsJsonObject().get("value").getAsString(); + String signature = textures.get("raw").getAsJsonObject().get("signature").getAsString(); + SkinImpl skin = new SkinImpl(value, signature); + cache.put(uuid, skin); + return skin; + } + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to fetch skin from fallback server:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public CompletableFuture fetchByUrl(URL url, String variant) { + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL apiUrl = parseUrl("https://api.mineskin.org/generate/url"); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("accept", "application/json"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + OutputStream outStream = connection.getOutputStream(); + DataOutputStream out = new DataOutputStream(outStream); + out.writeBytes("{\"variant\":\"" + variant + "\",\"url\":\"" + url.toString() + "\"}"); + out.flush(); + out.close(); + outStream.close(); + + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("error")) return null; + if (!obj.has("data")) return null; + JsonObject texture = obj.get("data").getAsJsonObject().get("texture").getAsJsonObject(); + return new SkinImpl(texture.get("value").getAsString(), texture.get("signature").getAsString()); + } + + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get skin from url:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public CompletableFuture fetchFromFile(String path) throws FileNotFoundException { + File file = new File(skinsFolder, path); + if (!file.exists()) throw new FileNotFoundException("File not found: " + path); + return CompletableFuture.supplyAsync(() -> { + URL apiUrl = parseUrl("https://api.mineskin.org/generate/upload"); + HttpURLConnection connection = null; + try { + String boundary = "*****"; + String CRLF = "\r\n"; + + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + connection.setUseCaches(false); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); + connection.setDoInput(true); + connection.setDoOutput(true); + OutputStream outputStream = connection.getOutputStream(); + DataOutputStream out = new DataOutputStream(outputStream); + out.writeBytes("--" + boundary + CRLF); + out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"" + CRLF); + out.writeBytes("Content-Type: image/png" + CRLF); + out.writeBytes(CRLF); + out.write(Files.readAllBytes(file.toPath())); + out.writeBytes(CRLF); + out.writeBytes("--" + boundary + "--" + CRLF); + out.flush(); + out.close(); + outputStream.close(); + + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("error")) return null; + if (!obj.has("data")) return null; + JsonObject texture = obj.get("data").getAsJsonObject().get("texture").getAsJsonObject(); + return new SkinImpl(texture.get("value").getAsString(), texture.get("signature").getAsString()); + } + + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to get skin from file:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public boolean isNameFullyCached(String s) { + String name = s.toLowerCase(); + if (!idCache.containsKey(name)) return false; + CachedId id = idCache.get(name); + if (id.isExpired() || !cache.containsKey(id.getId())) return false; + SkinImpl skin = cache.get(id.getId()); + return !skin.isExpired(); + } + + public SkinImpl getFullyCachedByName(String s) { + String name = s.toLowerCase(); + if (!idCache.containsKey(name)) return null; + CachedId id = idCache.get(name); + if (id.isExpired() || !cache.containsKey(id.getId())) return null; + SkinImpl skin = cache.get(id.getId()); + if (skin.isExpired()) return null; + return skin; + } + + public CompletableFuture fetchByUUID(String uuid) { + Player player = Bukkit.getPlayer(uuid); + if (player != null && player.isOnline()) return CompletableFuture.completedFuture(getFromPlayer(player)); + + if (cache.containsKey(uuid)) { + SkinImpl skin = cache.get(uuid); + if (!skin.isExpired()) return CompletableFuture.completedFuture(skin); + } + + return FutureUtil.exceptionPrintingSupplyAsync(() -> { + URL url = parseUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + try (Reader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + if (obj.has("errorMessage")) return null; + SkinImpl skin = new SkinImpl(obj); + cache.put(uuid, skin); + return skin; + } + } catch (IOException exception) { + if (!configManager.getConfig().disableSkinFetcherWarnings()) { + logger.warning("Failed to fetch skin, trying to use fallback server:"); + exception.printStackTrace(); + } + } finally { + if (connection != null) connection.disconnect(); + } + return null; + }); + } + + public SkinImpl getFromPlayer(Player player) { + try { + Object playerHandle = Reflections.GET_PLAYER_HANDLE_METHOD.get().invoke(player); + Object gameProfile = Reflections.GET_PROFILE_METHOD.get().invoke(playerHandle); + Object propertyMap = Reflections.GET_PROPERTY_MAP_METHOD.get().invoke(gameProfile); + return new SkinImpl(propertyMap); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static URL parseUrl(String url) { + try { + return new URL(url); + } catch (MalformedURLException exception) { + throw new RuntimeException(exception); + } + } + + public File getSkinsFolder() { + return skinsFolder; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java new file mode 100644 index 0000000..92491cd --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/cache/SkinCacheCleanTask.java @@ -0,0 +1,16 @@ +package lol.pyr.znpcsplus.skin.cache; + +import org.bukkit.scheduler.BukkitRunnable; + +public class SkinCacheCleanTask extends BukkitRunnable { + private final MojangSkinCache skinCache; + + public SkinCacheCleanTask(MojangSkinCache skinCache) { + this.skinCache = skinCache; + } + + @Override + public void run() { + skinCache.cleanCache(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java new file mode 100644 index 0000000..8971d0b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/MirrorDescriptor.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class MirrorDescriptor implements BaseSkinDescriptor, SkinDescriptor { + private final MojangSkinCache skinCache; + + public MirrorDescriptor(MojangSkinCache skinCache) { + this.skinCache = skinCache; + } + + @Override + public CompletableFuture fetch(Player player) { + return CompletableFuture.completedFuture(skinCache.getFromPlayer(player)); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return skinCache.getFromPlayer(player); + } + + @Override + public boolean supportsInstant(Player player) { + return true; + } + + @Override + public String serialize() { + return "mirror"; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java new file mode 100644 index 0000000..00d4521 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/NameFetchingDescriptor.java @@ -0,0 +1,44 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.PapiUtil; +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +public class NameFetchingDescriptor implements BaseSkinDescriptor, SkinDescriptor { + private final MojangSkinCache skinCache; + private final String name; + + public NameFetchingDescriptor(MojangSkinCache skinCache, String name) { + this.skinCache = skinCache; + this.name = name; + } + + @Override + public CompletableFuture fetch(Player player) { + return skinCache.fetchByName(PapiUtil.set(player, name)); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return skinCache.getFullyCachedByName(PapiUtil.set(player, name)); + } + + @Override + public boolean supportsInstant(Player player) { + return skinCache.isNameFullyCached(PapiUtil.set(player, name)); + } + + public String getName() { + return name; + } + + @Override + public String serialize() { + return "fetching;" + name; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java new file mode 100644 index 0000000..b263cc2 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/PrefetchedDescriptor.java @@ -0,0 +1,70 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import lol.pyr.znpcsplus.util.FutureUtil; +import org.bukkit.entity.Player; + +import java.io.FileNotFoundException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class PrefetchedDescriptor implements BaseSkinDescriptor, SkinDescriptor { + private final SkinImpl skin; + + public PrefetchedDescriptor(SkinImpl skin) { + this.skin = skin; + } + + public static CompletableFuture forPlayer(MojangSkinCache cache, String name) { + return FutureUtil.exceptionPrintingSupplyAsync(() -> new PrefetchedDescriptor(cache.fetchByName(name).join())); + } + + public static CompletableFuture fromUrl(MojangSkinCache cache, URL url, String variant) { + return FutureUtil.exceptionPrintingSupplyAsync(() -> new PrefetchedDescriptor(cache.fetchByUrl(url, variant).join())); + } + + public static CompletableFuture fromFile(MojangSkinCache cache, String path) { + return CompletableFuture.supplyAsync(() -> { + try { + return new PrefetchedDescriptor(cache.fetchFromFile(path).join()); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public CompletableFuture fetch(Player player) { + return CompletableFuture.completedFuture(skin); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return skin; + } + + @Override + public boolean supportsInstant(Player player) { + return true; + } + + public SkinImpl getSkin() { + return skin; + } + + @Override + public String serialize() { + StringBuilder sb = new StringBuilder(); + sb.append("prefetched;"); + for (TextureProperty property : skin.getProperties()) { + sb.append(property.getName()).append(";"); + sb.append(property.getValue()).append(";"); + sb.append(property.getSignature()).append(";"); + } + return sb.toString(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java new file mode 100644 index 0000000..685c786 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/skin/descriptor/UUIDFetchingDescriptor.java @@ -0,0 +1,42 @@ +package lol.pyr.znpcsplus.skin.descriptor; + +import lol.pyr.znpcsplus.api.skin.SkinDescriptor; +import lol.pyr.znpcsplus.skin.BaseSkinDescriptor; +import lol.pyr.znpcsplus.skin.SkinImpl; +import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; +import org.bukkit.entity.Player; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class UUIDFetchingDescriptor implements BaseSkinDescriptor, SkinDescriptor { + + private final MojangSkinCache skinCache; + private final UUID uuid; + + public UUIDFetchingDescriptor(MojangSkinCache skinCache, UUID uuid) { + this.skinCache = skinCache; + this.uuid = uuid; + } + + @Override + public CompletableFuture fetch(Player player) { + return skinCache.fetchByUUID(uuid.toString()); + } + + @Override + public SkinImpl fetchInstant(Player player) { + return fetch(player).join(); + } + + @Override + public boolean supportsInstant(Player player) { + return false; + } + + @Override + public String serialize() { + return "fetching-uuid;" + uuid.toString(); + } + +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java new file mode 100644 index 0000000..702d9ca --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorage.java @@ -0,0 +1,14 @@ +package lol.pyr.znpcsplus.storage; + +import lol.pyr.znpcsplus.npc.NpcEntryImpl; + +import java.util.Collection; + +public interface NpcStorage { + Collection loadNpcs(); + void saveNpcs(Collection npcs); + void deleteNpc(NpcEntryImpl npc); + default void close() { + + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java new file mode 100644 index 0000000..b4139cc --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/NpcStorageType.java @@ -0,0 +1,48 @@ +package lol.pyr.znpcsplus.storage; + +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.mysql.MySQLStorage; +import lol.pyr.znpcsplus.storage.sqlite.SQLiteStorage; +import lol.pyr.znpcsplus.storage.yaml.YamlStorage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; + +public enum NpcStorageType { + YAML { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry) { + return new YamlStorage(serializerRegistry, new File(plugin.getDataFolder(), "data")); + } + }, + SQLITE { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry) { + try { + return new SQLiteStorage(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer, new File(plugin.getDataFolder(), "znpcsplus.sqlite")); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + }, + MYSQL { + @Override + public NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry) { + try { + return new MySQLStorage(packetFactory, configManager, actionRegistry, typeRegistry, propertyRegistry, textSerializer); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + }; + + public abstract NpcStorage create(ConfigManager configManager, ZNpcsPlus plugin, PacketFactory packetFactory, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, NpcSerializerRegistryImpl serializerRegistry); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java new file mode 100644 index 0000000..4e41696 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/database/Database.java @@ -0,0 +1,18 @@ +package lol.pyr.znpcsplus.storage.database; + +import java.sql.Connection; +import java.util.logging.Logger; + +public abstract class Database { + protected final Logger logger; + protected Connection connection; + public Database(Logger logger){ + this.logger = logger; + } + + public abstract Connection getSQLConnection(); + + public abstract void load(); + + public abstract void close(); +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java new file mode 100644 index 0000000..ccdf79d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQL.java @@ -0,0 +1,131 @@ +package lol.pyr.znpcsplus.storage.mysql; + +import lol.pyr.znpcsplus.storage.database.Database; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Logger; + +public class MySQL extends Database { + private final String connectionURL; + private final String username; + private final String password; + + public MySQL(String connectionURL, String username, String password, Logger logger) { + super(logger); + this.connectionURL = connectionURL; + this.username = username; + this.password = password; + } + + @Override + public Connection getSQLConnection() { + validateConnectionUrl(); + + try { + if (connection != null && !connection.isClosed()) { + return connection; + } + Class.forName("com.mysql.jdbc.Driver"); + connection = java.sql.DriverManager.getConnection(connectionURL, username, password); + return connection; + } catch (ClassNotFoundException ex) { + logger.severe("MySQL JDBC library not found" + ex); + } catch (SQLException ex) { + if (ex.getSQLState().equals("08006")) { + logger.severe("Could not connect to MySQL server. Check your connection settings and make sure the server is online."); + } else if (ex.getSQLState().equals("08002")) { + logger.severe("A connection already exists." + ex); + } else { + logger.severe("MySQL exception on initialize" + ex); + } + } + return null; + } + + private void validateConnectionUrl() { + if (connectionURL == null || connectionURL.isEmpty()) { + throw new IllegalArgumentException("Connection URL cannot be null or empty"); + } + if (!connectionURL.startsWith("jdbc:mysql://")) { + throw new IllegalArgumentException("Connection URL must start with 'jdbc:mysql://'"); + } + // TODO: Validate the rest of the URL + } + + @Override + public void load() { + connection = getSQLConnection(); + } + + @Override + public void close() { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + logger.severe("An error occurred while closing the connection"); + e.printStackTrace(); + } + } + + public boolean tableExists(String tableName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT * FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean columnExists(String tableName, String columnName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT " + columnName + " FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean addColumn(String tableName, String columnName, String type) { + if (columnExists(tableName, columnName)) return false; + try { + Statement s = connection.createStatement(); + s.executeQuery("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + type + ";"); + s.close(); + } catch (SQLException e) { + return false; + } + return true; + } + + public ResultSet executeQuery(String query) { + try { + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery(query); + s.close(); + return rs; + } catch (SQLException e) { + return null; + } + } + + public int executeUpdate(String sql) { + try { + Statement s = connection.createStatement(); + int rowCount = s.executeUpdate(sql); + s.close(); + return rowCount; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java new file mode 100644 index 0000000..b7fe0cb --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/mysql/MySQLStorage.java @@ -0,0 +1,322 @@ +package lol.pyr.znpcsplus.storage.mysql; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class MySQLStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("MySQLStorage"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final MySQL database; + + private final String TABLE_NPCS; + private final String TABLE_NPCS_PROPERTIES; + private final String TABLE_NPCS_HOLOGRAMS; + private final String TABLE_NPCS_ACTIONS; + + public MySQLStorage(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.database = new MySQL(configManager.getConfig().databaseConfig().createConnectionURL("mysql"), + configManager.getConfig().databaseConfig().username(), configManager.getConfig().databaseConfig().password(), logger); + database.load(); + if (database.getSQLConnection() == null) { + throw new RuntimeException("Failed to initialize MySQL Storage"); + } + TABLE_NPCS = "npcs"; + TABLE_NPCS_PROPERTIES = "npcs_properties"; + TABLE_NPCS_HOLOGRAMS = "npcs_holograms"; + TABLE_NPCS_ACTIONS = "npcs_actions"; + validateTables(); + } + + private void validateTables() { + if (!database.tableExists(TABLE_NPCS)) { + logger.info("Creating table " + TABLE_NPCS + "..."); + createNpcsTable(); + } + if (!database.tableExists(TABLE_NPCS_PROPERTIES)) { + logger.info("Creating table " + TABLE_NPCS_PROPERTIES + "..."); + createNpcsPropertiesTable(); + } + if (!database.tableExists(TABLE_NPCS_HOLOGRAMS)) { + logger.info("Creating table " + TABLE_NPCS_HOLOGRAMS + "..."); + createNpcsHologramsTable(); + } + if (!database.tableExists(TABLE_NPCS_ACTIONS)) { + logger.info("Creating table " + TABLE_NPCS_ACTIONS + "..."); + createNpcsActionsTable(); + } + updateTables(); + } + + private void createNpcsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS + + " (id VARCHAR(256) PRIMARY KEY, isProcessed BOOLEAN, allowCommands BOOLEAN, enabled BOOLEAN, " + + "uuid VARCHAR(36), world VARCHAR(128), x DOUBLE, y DOUBLE, z DOUBLE, yaw DOUBLE, pitch DOUBLE, type VARCHAR(128), hologramOffset DOUBLE, hologramRefreshDelay BIGINT)") != -1) { + logger.info("Table " + TABLE_NPCS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS + "."); + } + } + + private void createNpcsPropertiesTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_PROPERTIES + + " (npc_id VARCHAR(256), property VARCHAR(128), value TEXT, PRIMARY KEY (npc_id, property))") != -1) { + logger.info("Table " + TABLE_NPCS_PROPERTIES + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_PROPERTIES + "."); + } + } + + private void createNpcsHologramsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_HOLOGRAMS + + " (npc_id VARCHAR(256), line INT, text TEXT, PRIMARY KEY (npc_id, line))") != -1) { + logger.info("Table " + TABLE_NPCS_HOLOGRAMS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_HOLOGRAMS + "."); + } + } + + private void createNpcsActionsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_ACTIONS + + " (npc_id VARCHAR(256), action_id INT, action_data TEXT, PRIMARY KEY (npc_id, action_id))") != -1) { + logger.info("Table " + TABLE_NPCS_ACTIONS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_ACTIONS + "."); + } + } + + private void updateTables() { + // Any table updates go here + } + + @Override + public Collection loadNpcs() { + Map npcMap = new HashMap<>(); + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcImpl npc = new NpcImpl(UUID.fromString(rs.getString("uuid")), propertyRegistry, configManager, packetFactory, textSerializer, + rs.getString("world"), typeRegistry.getByName(rs.getString("type")), + new NpcLocation(rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch"))); + + if (!rs.getBoolean("enabled")) npc.setEnabled(false); + + npc.getHologram().setOffset(rs.getDouble("hologramOffset")); + if (rs.getBigDecimal("hologramRefreshDelay") != null) npc.getHologram().setRefreshDelay(rs.getBigDecimal("hologramRefreshDelay").longValue()); + + NpcEntryImpl entry = new NpcEntryImpl(rs.getString("id"), npc); + entry.setProcessed(rs.getBoolean("isProcessed")); + entry.setAllowCommandModification(rs.getBoolean("allowCommands")); + entry.setSave(true); + npcMap.put(rs.getString("id"), entry); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_PROPERTIES); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + String key = rs.getString("property"); + if (entry != null) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + logger.warning("Unknown property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(rs.getString("value")); + if (value == null) { + logger.warning("Failed to deserialize property '" + key + "' for npc '" + rs.getString("npc_id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + entry.getNpc().UNSAFE_setProperty(property, value); + npcMap.put(rs.getString("npc_id"), entry); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_HOLOGRAMS + " ORDER BY line"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().getHologram().insertLine(rs.getInt("line"), rs.getString("text")); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_ACTIONS + " ORDER BY action_id"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().addAction(actionRegistry.deserialize(rs.getString("action_data"))); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return npcMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void saveNpcs(Collection npcs) { + long start = System.currentTimeMillis(); + for (NpcEntryImpl entry : npcs) try { + + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS + " (id, isProcessed, allowCommands, enabled, uuid, world, x, y, z, yaw, pitch, type, hologramOffset, hologramRefreshDelay) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, entry.getId()); + ps.setBoolean(2, entry.isProcessed()); + ps.setBoolean(3, entry.isAllowCommandModification()); + NpcImpl npc = entry.getNpc(); + ps.setBoolean(4, npc.isEnabled()); + ps.setString(5, npc.getUuid().toString()); + ps.setString(6, npc.getWorldName()); + ps.setDouble(7, npc.getLocation().getX()); + ps.setDouble(8, npc.getLocation().getY()); + ps.setDouble(9, npc.getLocation().getZ()); + ps.setFloat(10, npc.getLocation().getYaw()); + ps.setFloat(11, npc.getLocation().getPitch()); + ps.setString(12, npc.getType().getName()); + HologramImpl hologram = npc.getHologram(); + ps.setDouble(13, hologram.getOffset()); + ps.setBigDecimal(14, new BigDecimal(hologram.getRefreshDelay())); + + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_PROPERTIES + " (npc_id, property, value) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setString(2, property.getName()); + ps.setString(3, serializer.UNSAFE_serialize(npc.getProperty(property))); + ps.executeUpdate(); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ? AND line > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, hologram.getLines().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < hologram.getLines().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_HOLOGRAMS + " (npc_id, line, text) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + ps.setString(3, hologram.getLine(i)); + ps.executeUpdate(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ? AND action_id > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, npc.getActions().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < npc.getActions().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_ACTIONS + " (npc_id, action_id, action_data) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + String action = actionRegistry.serialize(npc.getActions().get(i)); + if (action == null) continue; + ps.setString(3, action); + ps.executeUpdate(); + } + } catch (SQLException exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + if (configManager.getConfig().debugEnabled()) { + logger.info("Saved " + npcs.size() + " npcs in " + (System.currentTimeMillis() - start) + "ms"); + } + } + + @Override + public void deleteNpc(NpcEntryImpl entry) { + try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS + " WHERE id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + } catch (SQLException exception) { + logger.severe("Failed to delete npc with id " + entry.getId()); + exception.printStackTrace(); + } + } + + @Override + public void close() { + database.close(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java new file mode 100644 index 0000000..ff1a273 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLite.java @@ -0,0 +1,112 @@ +package lol.pyr.znpcsplus.storage.sqlite; + +import lol.pyr.znpcsplus.storage.database.Database; + +import java.io.File; +import java.io.IOException; +import java.sql.*; +import java.util.logging.Logger; + +public class SQLite extends Database{ + private final File dbFile; + public SQLite(File file, Logger logger){ + super(logger); + dbFile = file; + } + + public Connection getSQLConnection() { + if (!dbFile.exists()){ + try { + dbFile.createNewFile(); + } catch (IOException e) { + logger.severe("File write error: "+dbFile.getName()); + } + } + try { + if(connection!=null&&!connection.isClosed()){ + return connection; + } + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + return connection; + } catch (SQLException ex) { + logger.severe("SQLite exception on initialize" + ex); + } catch (ClassNotFoundException ex) { + logger.severe("SQLite JDBC library not found" + ex); + } + return null; + } + + public void load() { + connection = getSQLConnection(); + } + + @Override + public void close() { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + logger.severe("An error occurred while closing the connection"); + e.printStackTrace(); + } + } + + public boolean tableExists(String tableName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT * FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean columnExists(String tableName, String columnName) { + try { + Statement s = connection.createStatement(); + s.executeQuery("SELECT " + columnName + " FROM " + tableName + ";"); + s.close(); + return true; + } catch (SQLException e) { + return false; + } + } + + public boolean addColumn(String tableName, String columnName, String type) { + if (columnExists(tableName, columnName)) return false; + try { + Statement s = connection.createStatement(); + s.executeQuery("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + type + ";"); + s.close(); + } catch (SQLException e) { + return false; + } + return true; + } + + public ResultSet executeQuery(String query) { + try { + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery(query); + s.close(); + return rs; + } catch (SQLException e) { + return null; + } + } + + public int executeUpdate(String query) { + try { + Statement s = connection.createStatement(); + int rowCount = s.executeUpdate(query); + s.close(); + return rowCount; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java new file mode 100644 index 0000000..0dc7592 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/sqlite/SQLiteStorage.java @@ -0,0 +1,320 @@ +package lol.pyr.znpcsplus.storage.sqlite; + +import lol.pyr.znpcsplus.api.entity.EntityProperty; +import lol.pyr.znpcsplus.config.ConfigManager; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.entity.PropertySerializer; +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.interaction.ActionRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcTypeRegistryImpl; +import lol.pyr.znpcsplus.packets.PacketFactory; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.File; +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class SQLiteStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("SQLiteStorage"); + + private final PacketFactory packetFactory; + private final ConfigManager configManager; + private final ActionRegistryImpl actionRegistry; + private final NpcTypeRegistryImpl typeRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final LegacyComponentSerializer textSerializer; + private final SQLite database; + + private final String TABLE_NPCS; + private final String TABLE_NPCS_PROPERTIES; + private final String TABLE_NPCS_HOLOGRAMS; + private final String TABLE_NPCS_ACTIONS; + + public SQLiteStorage(PacketFactory packetFactory, ConfigManager configManager, ActionRegistryImpl actionRegistry, NpcTypeRegistryImpl typeRegistry, EntityPropertyRegistryImpl propertyRegistry, LegacyComponentSerializer textSerializer, File file) { + this.packetFactory = packetFactory; + this.configManager = configManager; + this.actionRegistry = actionRegistry; + this.typeRegistry = typeRegistry; + this.propertyRegistry = propertyRegistry; + this.textSerializer = textSerializer; + this.database = new SQLite(file, logger); + database.load(); + if (database.getSQLConnection() == null) { + throw new RuntimeException("Failed to initialize SQLite Storage."); + } + TABLE_NPCS = "npcs"; + TABLE_NPCS_PROPERTIES = "npcs_properties"; + TABLE_NPCS_HOLOGRAMS = "npcs_holograms"; + TABLE_NPCS_ACTIONS = "npcs_actions"; + validateTables(); + } + + private void validateTables() { + if (!database.tableExists(TABLE_NPCS)) { + logger.info("Creating table " + TABLE_NPCS + "..."); + createNpcsTable(); + } + if (!database.tableExists(TABLE_NPCS_PROPERTIES)) { + logger.info("Creating table " + TABLE_NPCS_PROPERTIES + "..."); + createNpcsPropertiesTable(); + } + if (!database.tableExists(TABLE_NPCS_HOLOGRAMS)) { + logger.info("Creating table " + TABLE_NPCS_HOLOGRAMS + "..."); + createNpcsHologramsTable(); + } + if (!database.tableExists(TABLE_NPCS_ACTIONS)) { + logger.info("Creating table " + TABLE_NPCS_ACTIONS + "..."); + createNpcsActionsTable(); + } + updateTables(); + } + + private void createNpcsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS + " " + + "(id TEXT PRIMARY KEY, isProcessed BOOLEAN, allowCommands BOOLEAN, enabled BOOLEAN, " + + "uuid TEXT, world TEXT, x REAL, y REAL, z REAL, yaw REAL, pitch REAL, type TEXT, hologramOffset REAL, hologramRefreshDelay INTEGER)") != -1) { + logger.info("Table " + TABLE_NPCS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS + "."); + } + } + + private void createNpcsPropertiesTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_PROPERTIES + " " + + "(npc_id TEXT, property TEXT, value TEXT, PRIMARY KEY (npc_id, property))") != -1) { + logger.info("Table " + TABLE_NPCS_PROPERTIES + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_PROPERTIES + "."); + } + } + + private void createNpcsHologramsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_HOLOGRAMS + " " + + "(npc_id TEXT, line INTEGER, text TEXT, PRIMARY KEY (npc_id, line))") != -1) { + logger.info("Table " + TABLE_NPCS_HOLOGRAMS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_HOLOGRAMS + "."); + } + } + + private void createNpcsActionsTable() { + if (database.executeUpdate("CREATE TABLE " + TABLE_NPCS_ACTIONS + " " + + "(npc_id TEXT, action_id INTEGER, action_data TEXT, PRIMARY KEY (npc_id, action_id))") != -1) { + logger.info("Table " + TABLE_NPCS_ACTIONS + " created."); + } else { + logger.severe("Failed to create table " + TABLE_NPCS_ACTIONS + "."); + } + } + + private void updateTables() { + // Any table updates go here + } + + @Override + public Collection loadNpcs() { + Map npcMap = new HashMap<>(); + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcImpl npc = new NpcImpl(UUID.fromString(rs.getString("uuid")), propertyRegistry, configManager, packetFactory, textSerializer, + rs.getString("world"), typeRegistry.getByName(rs.getString("type")), + new NpcLocation(rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch"))); + + if (!rs.getBoolean("enabled")) npc.setEnabled(false); + + npc.getHologram().setOffset(rs.getDouble("hologramOffset")); + if (rs.getBigDecimal("hologramRefreshDelay") != null) npc.getHologram().setRefreshDelay(rs.getBigDecimal("hologramRefreshDelay").longValue()); + + NpcEntryImpl entry = new NpcEntryImpl(rs.getString("id"), npc); + entry.setProcessed(rs.getBoolean("isProcessed")); + entry.setAllowCommandModification(rs.getBoolean("allowCommands")); + entry.setSave(true); + npcMap.put(rs.getString("id"), entry); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_PROPERTIES); + ResultSet rs = st.executeQuery(); + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + String key = rs.getString("property"); + if (entry != null) { + EntityPropertyImpl property = propertyRegistry.getByName(key); + if (property == null) { + logger.warning("Unknown property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + PropertySerializer serializer = propertyRegistry.getSerializer(property.getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + key + "' for npc '" + rs.getString("npc_id") + "'. skipping ..."); + continue; + } + Object value = serializer.deserialize(rs.getString("value")); + if (value == null) { + logger.warning("Failed to deserialize property '" + key + "' for npc '" + rs.getString("npc_id") + "'. Resetting to default ..."); + value = property.getDefaultValue(); + } + entry.getNpc().UNSAFE_setProperty(property, value); + npcMap.put(rs.getString("npc_id"), entry); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_HOLOGRAMS + " ORDER BY line"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().getHologram().insertLine(rs.getInt("line"), rs.getString("text")); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try { + PreparedStatement st = database.getSQLConnection().prepareStatement("SELECT * FROM " + TABLE_NPCS_ACTIONS + " ORDER BY action_id"); + ResultSet rs = st.executeQuery(); + + while (rs.next()) { + NpcEntryImpl entry = npcMap.get(rs.getString("npc_id")); + if (entry != null) { + entry.getNpc().addAction(actionRegistry.deserialize(rs.getString("action_data"))); + } + npcMap.put(rs.getString("npc_id"), entry); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return npcMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void saveNpcs(Collection npcs) { + long start = System.currentTimeMillis(); + for (NpcEntryImpl entry : npcs) try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS + " (id, isProcessed, allowCommands, enabled, uuid, world, x, y, z, yaw, pitch, type, hologramOffset, hologramRefreshDelay) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, entry.getId()); + ps.setBoolean(2, entry.isProcessed()); + ps.setBoolean(3, entry.isAllowCommandModification()); + NpcImpl npc = entry.getNpc(); + ps.setBoolean(4, npc.isEnabled()); + ps.setString(5, npc.getUuid().toString()); + ps.setString(6, npc.getWorldName()); + ps.setDouble(7, npc.getLocation().getX()); + ps.setDouble(8, npc.getLocation().getY()); + ps.setDouble(9, npc.getLocation().getZ()); + ps.setFloat(10, npc.getLocation().getYaw()); + ps.setFloat(11, npc.getLocation().getPitch()); + ps.setString(12, npc.getType().getName()); + HologramImpl hologram = npc.getHologram(); + ps.setDouble(13, hologram.getOffset()); + ps.setBigDecimal(14, new BigDecimal(hologram.getRefreshDelay())); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + for (EntityProperty property : npc.getAllProperties()) try { + PropertySerializer serializer = propertyRegistry.getSerializer(((EntityPropertyImpl) property).getType()); + if (serializer == null) { + logger.warning("Unknown serializer for property '" + property.getName() + "' for npc '" + entry.getId() + "'. skipping ..."); + continue; + } + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_PROPERTIES + " (npc_id, property, value) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setString(2, property.getName()); + ps.setString(3, serializer.UNSAFE_serialize(npc.getProperty(property))); + ps.executeUpdate(); + } catch (Exception exception) { + logger.severe("Failed to serialize property " + property.getName() + " for npc with id " + entry.getId()); + exception.printStackTrace(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ? AND line > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, hologram.getLines().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < hologram.getLines().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_HOLOGRAMS + " (npc_id, line, text) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + ps.setString(3, hologram.getLine(i)); + ps.executeUpdate(); + } + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ? AND action_id > ?"); + ps.setString(1, entry.getId()); + ps.setInt(2, npc.getActions().size() - 1); + ps.executeUpdate(); + + for (int i = 0; i < npc.getActions().size(); i++) { + ps = database.getSQLConnection().prepareStatement("REPLACE INTO " + TABLE_NPCS_ACTIONS + " (npc_id, action_id, action_data) VALUES(?,?,?)"); + ps.setString(1, entry.getId()); + ps.setInt(2, i); + String action = actionRegistry.serialize(npc.getActions().get(i)); + if (action == null) continue; + ps.setString(3, action); + ps.executeUpdate(); + } + } catch (SQLException exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + if (configManager.getConfig().debugEnabled()) { + logger.info("Saved " + npcs.size() + " npcs in " + (System.currentTimeMillis() - start) + "ms"); + } + } + + @Override + public void deleteNpc(NpcEntryImpl entry) { + try { + PreparedStatement ps; + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS + " WHERE id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_PROPERTIES + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_HOLOGRAMS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + + ps = database.getSQLConnection().prepareStatement("DELETE FROM " + TABLE_NPCS_ACTIONS + " WHERE npc_id = ?"); + ps.setString(1, entry.getId()); + ps.executeUpdate(); + } catch (SQLException exception) { + logger.severe("Failed to delete npc with id " + entry.getId()); + exception.printStackTrace(); + } + } + + @Override + public void close() { + database.close(); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java b/plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java new file mode 100644 index 0000000..b985fa7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/storage/yaml/YamlStorage.java @@ -0,0 +1,84 @@ +package lol.pyr.znpcsplus.storage.yaml; + +import lol.pyr.znpcsplus.api.serialization.NpcSerializer; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.serialization.NpcSerializerRegistryImpl; +import lol.pyr.znpcsplus.storage.NpcStorage; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +public class YamlStorage implements NpcStorage { + private final static Logger logger = Logger.getLogger("YamlStorage"); + + private final File folder; + private final NpcSerializer yamlSerializer; + + public YamlStorage(NpcSerializerRegistryImpl serializerRegistry, File folder) { + this.yamlSerializer = serializerRegistry.getSerializer(YamlConfiguration.class); + this.folder = folder; + if (!this.folder.exists()) this.folder.mkdirs(); + } + + @Override + public Collection loadNpcs() { + File[] files = folder.listFiles(); + if (files == null || files.length == 0) return Collections.emptyList(); + List npcs = new ArrayList<>(files.length); + for (File file : files) if (file.isFile() && file.getName().toLowerCase().endsWith(".yml")) try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + npcs.add((NpcEntryImpl) yamlSerializer.deserialize(config)); + } catch (Throwable t) { + logger.severe("Failed to load npc file: " + file.getName()); + t.printStackTrace(); + } + return npcs; + } + + @Override + public void saveNpcs(Collection npcs) { + for (NpcEntryImpl entry : npcs) try { + YamlConfiguration config = yamlSerializer.serialize(entry); + config.save(fileFor(entry)); + } catch (Exception exception) { + logger.severe("Failed to save npc with id " + entry.getId()); + exception.printStackTrace(); + } + } + + @Override + public void deleteNpc(NpcEntryImpl npc) { + fileFor(npc).delete(); + } + + private File fileFor(NpcEntryImpl entry) { + return new File(folder, entry.getId() + ".yml"); + } + + public NpcLocation deserializeLocation(ConfigurationSection section) { + return new NpcLocation( + section.getDouble("x"), + section.getDouble("y"), + section.getDouble("z"), + (float) section.getDouble("yaw"), + (float) section.getDouble("pitch") + ); + } + + public YamlConfiguration serializeLocation(NpcLocation location) { + YamlConfiguration config = new YamlConfiguration(); + config.set("x", location.getX()); + config.set("y", location.getY()); + config.set("z", location.getZ()); + config.set("yaw", location.getYaw()); + config.set("pitch", location.getPitch()); + return config; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java new file mode 100644 index 0000000..9139f68 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/HologramRefreshTask.java @@ -0,0 +1,22 @@ +package lol.pyr.znpcsplus.tasks; + +import lol.pyr.znpcsplus.hologram.HologramImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import org.bukkit.scheduler.BukkitRunnable; + +public class HologramRefreshTask extends BukkitRunnable { + private final NpcRegistryImpl npcRegistry; + + public HologramRefreshTask(NpcRegistryImpl npcRegistry) { + this.npcRegistry = npcRegistry; + } + + @Override + public void run() { + for (NpcEntryImpl entry : npcRegistry.getProcessable()) { + HologramImpl hologram = entry.getNpc().getHologram(); + if (hologram.shouldRefresh()) hologram.refresh(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java new file mode 100644 index 0000000..a975546 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/NpcProcessorTask.java @@ -0,0 +1,150 @@ +package lol.pyr.znpcsplus.tasks; + +import lol.pyr.znpcsplus.api.event.NpcDespawnEvent; +import lol.pyr.znpcsplus.api.event.NpcSpawnEvent; +import lol.pyr.znpcsplus.entity.EntityPropertyImpl; +import lol.pyr.znpcsplus.entity.EntityPropertyRegistryImpl; +import lol.pyr.znpcsplus.npc.NpcEntryImpl; +import lol.pyr.znpcsplus.npc.NpcImpl; +import lol.pyr.znpcsplus.npc.NpcRegistryImpl; +import lol.pyr.znpcsplus.user.User; +import lol.pyr.znpcsplus.user.UserManager; +import lol.pyr.znpcsplus.util.LookType; +import lol.pyr.znpcsplus.util.NpcLocation; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.NumberConversions; +import org.bukkit.util.Vector; + +public class NpcProcessorTask extends BukkitRunnable { + private final NpcRegistryImpl npcRegistry; + private final EntityPropertyRegistryImpl propertyRegistry; + private final UserManager userManager; + + public NpcProcessorTask(NpcRegistryImpl npcRegistry, EntityPropertyRegistryImpl propertyRegistry,UserManager userManager) { + this.npcRegistry = npcRegistry; + this.propertyRegistry = propertyRegistry; + this.userManager = userManager; + } + + public void run() { + EntityPropertyImpl viewDistanceProperty = propertyRegistry.getByName("view_distance", Integer.class); // Not sure why this is an Integer, but it is + EntityPropertyImpl lookProperty = propertyRegistry.getByName("look", LookType.class); + EntityPropertyImpl lookDistanceProperty = propertyRegistry.getByName("look_distance", Double.class); + EntityPropertyImpl lookReturnProperty = propertyRegistry.getByName("look_return", Boolean.class); + EntityPropertyImpl permissionRequiredProperty = propertyRegistry.getByName("permission_required", Boolean.class); + EntityPropertyImpl playerKnockbackProperty = propertyRegistry.getByName("player_knockback", Boolean.class); + EntityPropertyImpl playerKnockbackExemptPermissionProperty = propertyRegistry.getByName("player_knockback_exempt_permission", String.class); + EntityPropertyImpl playerKnockbackDistanceProperty = propertyRegistry.getByName("player_knockback_distance", Double.class); + EntityPropertyImpl playerKnockbackVerticalProperty = propertyRegistry.getByName("player_knockback_vertical", Double.class); + EntityPropertyImpl playerKnockbackHorizontalProperty = propertyRegistry.getByName("player_knockback_horizontal", Double.class); + EntityPropertyImpl playerKnockbackCooldownProperty = propertyRegistry.getByName("player_knockback_cooldown", Integer.class); + EntityPropertyImpl playerKnockbackSoundProperty = propertyRegistry.getByName("player_knockback_sound", Boolean.class); + EntityPropertyImpl playerKnockbackSoundNameProperty = propertyRegistry.getByName("player_knockback_sound_name", Sound.class); + EntityPropertyImpl playerKnockbackSoundVolumeProperty = propertyRegistry.getByName("player_knockback_sound_volume", Float.class); + EntityPropertyImpl playerKnockbackSoundPitchProperty = propertyRegistry.getByName("player_knockback_sound_pitch", Float.class); + double lookDistance; + boolean lookReturn; + boolean permissionRequired; + boolean playerKnockback; + String playerKnockbackExemptPermission = null; + double playerKnockbackDistance = 0; + double playerKnockbackVertical = 0; + double playerKnockbackHorizontal = 0; + int playerKnockbackCooldown = 0; + boolean playerKnockbackSound = false; + Sound playerKnockbackSoundName = null; + float playerKnockbackSoundVolume = 0; + float playerKnockbackSoundPitch = 0; + for (NpcEntryImpl entry : npcRegistry.getProcessable()) { + NpcImpl npc = entry.getNpc(); + if (!npc.isEnabled()) continue; + + double closestDist = Double.MAX_VALUE; + Player closest = null; + LookType lookType = npc.getProperty(lookProperty); + lookDistance = NumberConversions.square(npc.getProperty(lookDistanceProperty)); + lookReturn = npc.getProperty(lookReturnProperty); + permissionRequired = npc.getProperty(permissionRequiredProperty); + playerKnockback = npc.getProperty(playerKnockbackProperty); + if (playerKnockback) { + playerKnockbackExemptPermission = npc.getProperty(playerKnockbackExemptPermissionProperty); + playerKnockbackDistance = NumberConversions.square(npc.getProperty(playerKnockbackDistanceProperty)); + playerKnockbackVertical = npc.getProperty(playerKnockbackVerticalProperty); + playerKnockbackHorizontal = npc.getProperty(playerKnockbackHorizontalProperty); + playerKnockbackCooldown = npc.getProperty(playerKnockbackCooldownProperty); + playerKnockbackSound = npc.getProperty(playerKnockbackSoundProperty); + playerKnockbackSoundName = npc.getProperty(playerKnockbackSoundNameProperty); + playerKnockbackSoundVolume = npc.getProperty(playerKnockbackSoundVolumeProperty); + playerKnockbackSoundPitch = npc.getProperty(playerKnockbackSoundPitchProperty); + } + for (Player player : Bukkit.getOnlinePlayers()) { + if (!player.getWorld().equals(npc.getWorld())) { + if (npc.isVisibleTo(player)) npc.hide(player); + continue; + } + if (permissionRequired && !player.hasPermission("znpcsplus.npc." + entry.getId())) { + if (npc.isVisibleTo(player)) npc.hide(player); + continue; + } + double distance = player.getLocation().distanceSquared(npc.getBukkitLocation()); + + // visibility + boolean inRange = distance <= NumberConversions.square(npc.getProperty(viewDistanceProperty)); + if (!inRange && npc.isVisibleTo(player)) { + NpcDespawnEvent event = new NpcDespawnEvent(player, entry); + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled()) npc.hide(player); + } + if (inRange) { + if (!npc.isVisibleTo(player)) { + NpcSpawnEvent event = new NpcSpawnEvent(player, entry); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) continue; + npc.show(player); + } + if (distance < closestDist) { + closestDist = distance; + closest = player; + } + if (lookType.equals(LookType.PER_PLAYER)) { + if (lookDistance >= distance) { + NpcLocation expected = npc.getLocation().lookingAt(player.getLocation().add(0, -npc.getType().getHologramOffset(), 0)); + npc.setHeadRotation(player, expected.getYaw(), expected.getPitch()); + } else if (lookReturn) { + npc.setHeadRotation(player, npc.getLocation().getYaw(), npc.getLocation().getPitch()); + } + } + + // player knockback + User user = userManager.get(player.getUniqueId()); + if (playerKnockbackExemptPermission == null || !player.hasPermission(playerKnockbackExemptPermission)) { + if (playerKnockback && distance <= playerKnockbackDistance && user.canKnockback(playerKnockbackCooldown)) { + double x = npc.getLocation().getX() - player.getLocation().getX(); + double z = npc.getLocation().getZ() - player.getLocation().getZ(); + double angle = Math.atan2(z, x); + double knockbackX = -Math.cos(angle) * playerKnockbackHorizontal; + double knockbackZ = -Math.sin(angle) * playerKnockbackHorizontal; + player.setVelocity(player.getVelocity().add(new Vector(knockbackX, playerKnockbackVertical, knockbackZ))); + if (playerKnockbackSound) + player.playSound(player.getLocation(), playerKnockbackSoundName, playerKnockbackSoundVolume, playerKnockbackSoundPitch); + } + } + } + } + // look property + if (lookType.equals(LookType.CLOSEST_PLAYER)) { + if (closest != null && lookDistance >= closestDist) { + NpcLocation expected = npc.getLocation().lookingAt(closest.getLocation().add(0, -npc.getType().getHologramOffset(), 0)); + if (!expected.equals(npc.getLocation())) npc.setHeadRotation(expected.getYaw(), expected.getPitch()); + } else if (lookReturn) { + npc.setHeadRotation(npc.getLocation().getYaw(), npc.getLocation().getPitch()); + } + } else if (lookType.equals(LookType.FIXED)) { + npc.setHeadRotation(npc.getLocation().getYaw(), npc.getLocation().getPitch()); + } + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java new file mode 100644 index 0000000..a411711 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/tasks/ViewableHideOnLeaveListener.java @@ -0,0 +1,15 @@ +package lol.pyr.znpcsplus.tasks; + +import lol.pyr.znpcsplus.util.Viewable; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class ViewableHideOnLeaveListener implements Listener { + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Viewable.all().forEach(viewable -> { + if (viewable.isVisibleTo(event.getPlayer())) viewable.UNSAFE_removeViewer(event.getPlayer()); + }); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java new file mode 100644 index 0000000..ef6ecc1 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateChecker.java @@ -0,0 +1,116 @@ +package lol.pyr.znpcsplus.updater; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Logger; + +public class UpdateChecker extends BukkitRunnable { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Update Checker"); + private final static String GET_RESOURCE = "https://api.spigotmc.org/simple/0.2/index.php?action=getResource&id=109380"; + public final static String DOWNLOAD_LINK = "https://www.spigotmc.org/resources/znpcsplus.109380/"; + + private final PluginDescriptionFile info; + private Status status = Status.UNKNOWN; + private String newestVersion = "N/A"; + + public UpdateChecker(PluginDescriptionFile info) { + this.info = info; + } + + public void run() { + String foundVersion = null; + try { + URL getResource = new URL(GET_RESOURCE); + HttpURLConnection httpRequest = ((HttpURLConnection) getResource.openConnection()); + httpRequest.setRequestMethod("GET"); + httpRequest.setConnectTimeout(5_000); + httpRequest.setReadTimeout(5_000); + + if (httpRequest.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStreamReader reader = new InputStreamReader(httpRequest.getInputStream())) { + JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject(); + foundVersion = jsonObject.get("current_version").getAsString(); + } + } else { + logger.warning("Failed to check for updates: HTTP response code " + httpRequest.getResponseCode()); + } + } catch (IOException e) { + logger.warning("Failed to check for updates: " + e.getMessage()); + return; + } + + if (foundVersion == null) return; + newestVersion = foundVersion; + + status = compareVersions(info.getVersion(), newestVersion); + if (status == Status.UPDATE_NEEDED) notifyConsole(); + } + + private void notifyConsole() { + logger.warning("Version " + getLatestVersion() + " of " + info.getName() + " is available now!"); + logger.warning("Download it at " + UpdateChecker.DOWNLOAD_LINK); + } + + private Status compareVersions(String currentVersion, String newVersion) { + if (currentVersion.equalsIgnoreCase(newVersion)) return Status.LATEST_VERSION; + ReleaseType currentType = parseReleaseType(currentVersion); + ReleaseType newType = parseReleaseType(newVersion); + if (currentType == ReleaseType.UNKNOWN || newType == ReleaseType.UNKNOWN) return Status.UNKNOWN; + String currentVersionWithoutType = getVersionWithoutReleaseType(currentVersion); + String newVersionWithoutType = getVersionWithoutReleaseType(newVersion); + String[] currentParts = currentVersionWithoutType.split("\\."); + String[] newParts = newVersionWithoutType.split("\\."); + for (int i = 0; i < Math.min(currentParts.length, newParts.length); i++) { + int currentPart = Integer.parseInt(currentParts[i]); + int newPart = Integer.parseInt(newParts[i]); + if (newPart > currentPart) return Status.UPDATE_NEEDED; + if (newPart < currentPart) return Status.LATEST_VERSION; + } + if (newType.ordinal() > currentType.ordinal()) return Status.UPDATE_NEEDED; + if (newType == currentType) { + int currentReleaseTypeNumber = getReleaseTypeNumber(currentVersion); + int newReleaseTypeNumber = getReleaseTypeNumber(newVersion); + if (newReleaseTypeNumber > currentReleaseTypeNumber) return Status.UPDATE_NEEDED; + } + return Status.LATEST_VERSION; + } + + private ReleaseType parseReleaseType(String version) { + if (version.toLowerCase().contains("snapshot")) return ReleaseType.SNAPSHOT; + if (version.toLowerCase().contains("alpha")) return ReleaseType.ALPHA; + if (version.toLowerCase().contains("beta")) return ReleaseType.BETA; + return version.matches("\\d+\\.\\d+\\.\\d+") ? ReleaseType.RELEASE : ReleaseType.UNKNOWN; + } + + private String getVersionWithoutReleaseType(String version) { + return version.contains("-") ? version.split("-")[0] : version; + } + + private int getReleaseTypeNumber(String version) { + if (!version.contains("-")) return 0; + return Integer.parseInt(version.split("-")[1].split("\\.")[1]); + } + + public Status getStatus() { + return status; + } + + public String getLatestVersion() { + return newestVersion; + } + + public enum Status { + UNKNOWN, LATEST_VERSION, UPDATE_NEEDED + } + + public enum ReleaseType { + UNKNOWN, SNAPSHOT, ALPHA, BETA, RELEASE + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java new file mode 100644 index 0000000..f3b2493 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java @@ -0,0 +1,37 @@ +package lol.pyr.znpcsplus.updater; + +import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class UpdateNotificationListener implements Listener { + private final ZNpcsPlus plugin; + private final BukkitAudiences adventure; + private final UpdateChecker updateChecker; + private final TaskScheduler scheduler; + + public UpdateNotificationListener(ZNpcsPlus plugin, BukkitAudiences adventure, UpdateChecker updateChecker, TaskScheduler scheduler) { + this.plugin = plugin; + this.adventure = adventure; + this.updateChecker = updateChecker; + this.scheduler = scheduler; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + if (!event.getPlayer().hasPermission("znpcsplus.updates")) return; + if (updateChecker.getStatus() != UpdateChecker.Status.UPDATE_NEEDED) return; + scheduler.runLaterAsync(() -> { + if (!event.getPlayer().isOnline()) return; + adventure.player(event.getPlayer()) + .sendMessage(Component.text(plugin.getDescription().getName() + " v" + updateChecker.getLatestVersion() + " is available now!", NamedTextColor.GOLD).appendNewline() + .append(Component.text("Click this message to open the Spigot page (CLICK)", NamedTextColor.YELLOW)).clickEvent(ClickEvent.openUrl(UpdateChecker.DOWNLOAD_LINK))); + }, 100L); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java new file mode 100644 index 0000000..9580597 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/ClientPacketListener.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.user; + +import com.github.retrooper.packetevents.event.PacketListener; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerJoinGame; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerServerData; +import lol.pyr.znpcsplus.config.ConfigManager; + +public class ClientPacketListener implements PacketListener { + private final ConfigManager configManager; + + public ClientPacketListener(ConfigManager configManager) { + this.configManager = configManager; + } + + @Override + public void onPacketSend(PacketSendEvent event) { + if (!configManager.getConfig().fakeEnforceSecureChat()) return; + if (event.getPacketType() == PacketType.Play.Server.SERVER_DATA) { + WrapperPlayServerServerData packet = new WrapperPlayServerServerData(event); + packet.setEnforceSecureChat(true); + event.setByteBuf(packet.getBuffer()); + } else if (event.getPacketType() == PacketType.Play.Server.JOIN_GAME) { + WrapperPlayServerJoinGame packet = new WrapperPlayServerJoinGame(event); + packet.setEnforcesSecureChat(true); + event.setByteBuf(packet.getBuffer()); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/User.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/User.java new file mode 100644 index 0000000..216001e --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/User.java @@ -0,0 +1,53 @@ +package lol.pyr.znpcsplus.user; + +import lol.pyr.znpcsplus.api.interaction.InteractionAction; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class User { + private final UUID uuid; + private long lastNpcInteraction; + private long lastNpcKnockback; + private final Map actionCooldownMap = new HashMap<>(); + + public User(UUID uuid) { + this.uuid = uuid; + } + + public Player getPlayer() { + return Bukkit.getPlayer(uuid); + } + + public boolean canInteract() { + if (System.currentTimeMillis() - lastNpcInteraction > 100L) { + lastNpcInteraction = System.currentTimeMillis(); + return true; + } + return false; + } + + public boolean canKnockback(int cooldown) { + if (System.currentTimeMillis() - lastNpcKnockback > cooldown) { + lastNpcKnockback = System.currentTimeMillis(); + return true; + } + return false; + } + + public UUID getUuid() { + return uuid; + } + + public boolean actionCooldownCheck(InteractionAction action) { + UUID id = action.getUuid(); + if (System.currentTimeMillis() - actionCooldownMap.getOrDefault(id, 0L) >= action.getCooldown()) { + actionCooldownMap.put(id, System.currentTimeMillis()); + return true; + } + return false; + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java new file mode 100644 index 0000000..266a81b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserListener.java @@ -0,0 +1,24 @@ +package lol.pyr.znpcsplus.user; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class UserListener implements Listener { + private final UserManager manager; + + public UserListener(UserManager manager) { + this.manager = manager; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + manager.get(event.getPlayer()); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + manager.remove(event.getPlayer().getUniqueId()); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java new file mode 100644 index 0000000..ee0c12b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/user/UserManager.java @@ -0,0 +1,36 @@ +package lol.pyr.znpcsplus.user; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class UserManager { + private final Map userMap = new ConcurrentHashMap<>(); + + public UserManager() { + Bukkit.getOnlinePlayers().forEach(this::get); + } + + public User get(Player player) { + return get(player.getUniqueId()); + } + + public User get(UUID uuid) { + return userMap.computeIfAbsent(uuid, User::new); + } + + public void remove(Player player) { + remove(player.getUniqueId()); + } + + public void remove(UUID uuid) { + userMap.remove(uuid); + } + + public void shutdown() { + Bukkit.getOnlinePlayers().forEach(this::remove); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java new file mode 100644 index 0000000..18193c7 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/BungeeConnector.java @@ -0,0 +1,35 @@ +package lol.pyr.znpcsplus.util; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class BungeeConnector { + private final static String CHANNEL_NAME = "BungeeCord"; + private final Plugin plugin; + + public BungeeConnector(Plugin plugin) { + this.plugin = plugin; + } + + public void connectPlayer(Player player, String server) { + player.sendPluginMessage(plugin, CHANNEL_NAME, createMessage("Connect", server)); + } + + @SuppressWarnings("UnstableApiUsage") + private byte[] createMessage(String... parts) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + for (String part : parts) out.writeUTF(part); + return out.toByteArray(); + } + + public void registerChannel() { + Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, CHANNEL_NAME); + } + + public void unregisterChannel() { + Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, CHANNEL_NAME); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java new file mode 100644 index 0000000..1bf3197 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/FileUtil.java @@ -0,0 +1,23 @@ +package lol.pyr.znpcsplus.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +public class FileUtil { + public static String dumpReaderAsString(Reader reader) { + BufferedReader bReader = new BufferedReader(reader); + try { + List lines = new ArrayList<>(); + String line; + while ((line = bReader.readLine()) != null) { + lines.add(line); + } + return String.join("\n", lines); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java new file mode 100644 index 0000000..54c010b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/FoliaUtil.java @@ -0,0 +1,30 @@ +package lol.pyr.znpcsplus.util; + +import lol.pyr.znpcsplus.reflection.Reflections; +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +import java.lang.reflect.InvocationTargetException; + +public class FoliaUtil { + private static final Boolean FOLIA = isFolia(); + public static boolean isFolia() { + if (FOLIA != null) return FOLIA; + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static void teleport(Entity entity, Location location) { + if (!isFolia()) entity.teleport(location); + else try { + Reflections.FOLIA_TELEPORT_ASYNC.get().invoke(entity, location); + } catch (IllegalAccessException | InvocationTargetException e) { + System.err.println("Error while teleporting entity:"); + e.printStackTrace(); + } + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java new file mode 100644 index 0000000..18ca6e4 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/FutureUtil.java @@ -0,0 +1,34 @@ +package lol.pyr.znpcsplus.util; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class FutureUtil { + public static CompletableFuture allOf(Collection> futures) { + return exceptionPrintingRunAsync(() -> { + for (CompletableFuture future : futures) future.join(); + }); + } + + public static CompletableFuture newExceptionPrintingFuture() { + return new CompletableFuture().exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } + + public static CompletableFuture exceptionPrintingRunAsync(Runnable runnable) { + return CompletableFuture.runAsync(runnable).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } + + public static CompletableFuture exceptionPrintingSupplyAsync(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java new file mode 100644 index 0000000..ed5f866 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/ItemSerializationUtil.java @@ -0,0 +1,47 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; + +public class ItemSerializationUtil { + public static byte[] objectToBytes(Object obj) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try { + new BukkitObjectOutputStream(bout).writeObject(obj); + } catch (IOException e) { + throw new RuntimeException(e); + } + return bout.toByteArray(); + } + + @SuppressWarnings({"unchecked", "unused"}) + public static T objectFromBytes(byte[] bytes, Class clazz) { + ByteArrayInputStream bin = new ByteArrayInputStream(bytes); + try { + return (T) new BukkitObjectInputStream(bin).readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static String itemToB64(ItemStack item) { + if (item == null) return null; + return Base64.getEncoder().encodeToString(objectToBytes(item)); + } + + public static ItemStack itemFromB64(String str) { + if (str == null) return null; + return objectFromBytes(Base64.getDecoder().decode(str), ItemStack.class); + } + + public static ItemMeta metaFromB64(String str) { + return objectFromBytes(Base64.getDecoder().decode(str), ItemMeta.class); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java new file mode 100644 index 0000000..509022f --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/LazyLoader.java @@ -0,0 +1,21 @@ +package lol.pyr.znpcsplus.util; + +import java.util.function.Supplier; + +public class LazyLoader { + private final Supplier supplier; + private T value; + + private LazyLoader(Supplier supplier) { + this.supplier = supplier; + } + + public T get() { + if (value == null) value = supplier.get(); + return value; + } + + public static LazyLoader of(Supplier supplier) { + return new LazyLoader<>(supplier); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java new file mode 100644 index 0000000..f64d63b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/PapiUtil.java @@ -0,0 +1,27 @@ +package lol.pyr.znpcsplus.util; + +import me.clip.placeholderapi.PlaceholderAPI; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class PapiUtil { + private static boolean isSupported() { + return Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"); + } + + public static String set(String str) { + return set(null, str); + } + + public static String set(Player player, String str) { + return isSupported() ? PlaceholderAPI.setPlaceholders(player, str) : str; + } + + // Ugly workaround would be cool if a better solution existed + public static Component set(LegacyComponentSerializer serializer, Player player, Component component) { + if (!isSupported()) return component; + return serializer.deserialize(set(player, serializer.serialize(component))); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java b/plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java new file mode 100644 index 0000000..cc222a8 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/util/Viewable.java @@ -0,0 +1,123 @@ +package lol.pyr.znpcsplus.util; + +import org.bukkit.entity.Player; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; + +public abstract class Viewable { + private final static List> all = Collections.synchronizedList(new ArrayList<>()); + + public static List all() { + synchronized (all) { + all.removeIf(reference -> reference.get() == null); + return all.stream() + .map(Reference::get) + .collect(Collectors.toList()); + } + } + + private boolean queueRunning = false; + private final Queue visibilityTaskQueue = new ConcurrentLinkedQueue<>(); + private final Set viewers = ConcurrentHashMap.newKeySet(); + + public Viewable() { + all.add(new WeakReference<>(this)); + } + + private void tryRunQueue() { + if (visibilityTaskQueue.isEmpty() || queueRunning) return; + queueRunning = true; + FutureUtil.exceptionPrintingRunAsync(() -> { + while (!visibilityTaskQueue.isEmpty()) try { + visibilityTaskQueue.remove().run(); + } catch (Exception e) { + e.printStackTrace(); + } + queueRunning = false; + }); + } + + private void queueVisibilityTask(Runnable runnable) { + visibilityTaskQueue.add(runnable); + tryRunQueue(); + } + + public void delete() { + queueVisibilityTask(() -> { + UNSAFE_hideAll(); + viewers.clear(); + synchronized (all) { + all.removeIf(reference -> reference.get() == null || reference.get() == this); + } + }); + } + + public CompletableFuture respawn() { + CompletableFuture future = new CompletableFuture<>(); + queueVisibilityTask(() -> { + UNSAFE_hideAll(); + UNSAFE_showAll().join(); + future.complete(null); + }); + return future; + } + + public CompletableFuture respawn(Player player) { + hide(player); + return show(player); + } + + public CompletableFuture show(Player player) { + CompletableFuture future = new CompletableFuture<>(); + queueVisibilityTask(() -> { + if (viewers.contains(player)) { + future.complete(null); + return; + } + viewers.add(player); + UNSAFE_show(player).join(); + future.complete(null); + }); + return future; + } + + public void hide(Player player) { + queueVisibilityTask(() -> { + if (!viewers.contains(player)) return; + viewers.remove(player); + UNSAFE_hide(player); + }); + } + + public void UNSAFE_removeViewer(Player player) { + viewers.remove(player); + } + + protected void UNSAFE_hideAll() { + for (Player viewer : viewers) UNSAFE_hide(viewer); + } + + protected CompletableFuture UNSAFE_showAll() { + return FutureUtil.allOf(viewers.stream() + .map(this::UNSAFE_show) + .collect(Collectors.toList())); + } + + public Set getViewers() { + return Collections.unmodifiableSet(viewers); + } + + public boolean isVisibleTo(Player player) { + return viewers.contains(player); + } + + protected abstract CompletableFuture UNSAFE_show(Player player); + + protected abstract void UNSAFE_hide(Player player); +} diff --git a/plugin/src/main/resources/messages/action-hover/add.txt b/plugin/src/main/resources/messages/action-hover/add.txt new file mode 100644 index 0000000..23b0083 --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/add.txt @@ -0,0 +1,13 @@ +Examples: + * /npc action add consolecommand cool_npc1 ANY_CLICK 0 0 say {player} just clicked a cool npc! + * /npc action add playerchat dog LEFT_CLICK 0 100 It has been 5 seconds since i clicked the npc + * /npc action add message npc123 RIGHT_CLICK 1 0 You can only click this npc once per second + +Action Types: + * Console Command - Send a console command when a player interacts with the npc + * Message - Send a message to any player that interacts with the npc + * Player Chat - Make any player that interacts send something in the chat + * Player Command - Make any player that interacts send a command + * Switch Server - Send the player to a different server on the proxy using bungee messaging channel + +Command used to add actions to an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/clear.txt b/plugin/src/main/resources/messages/action-hover/clear.txt new file mode 100644 index 0000000..ef981f7 --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/clear.txt @@ -0,0 +1,3 @@ +Usage » /npc action clear + +Command used to clear all npc actions \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/delete.txt b/plugin/src/main/resources/messages/action-hover/delete.txt new file mode 100644 index 0000000..5a6507e --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/delete.txt @@ -0,0 +1,3 @@ +Usage » /npc action delete + +Command used to delete a specific action from an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/edit.txt b/plugin/src/main/resources/messages/action-hover/edit.txt new file mode 100644 index 0000000..1827560 --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/edit.txt @@ -0,0 +1,3 @@ +Usage » /npc action edit + +Command used to change a specific action on an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action-hover/list.txt b/plugin/src/main/resources/messages/action-hover/list.txt new file mode 100644 index 0000000..7397e5f --- /dev/null +++ b/plugin/src/main/resources/messages/action-hover/list.txt @@ -0,0 +1,3 @@ +Usage » /npc action list + +Command used to list all actions of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/action.txt b/plugin/src/main/resources/messages/action.txt new file mode 100644 index 0000000..f6e1e09 --- /dev/null +++ b/plugin/src/main/resources/messages/action.txt @@ -0,0 +1,10 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command for more info + + * /npc action add + * /npc action clear + * /npc action delete + * /npc action edit + * /npc action list + diff --git a/plugin/src/main/resources/messages/holo-hover/add.txt b/plugin/src/main/resources/messages/holo-hover/add.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/additem.txt b/plugin/src/main/resources/messages/holo-hover/additem.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/delete.txt b/plugin/src/main/resources/messages/holo-hover/delete.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/info.txt b/plugin/src/main/resources/messages/holo-hover/info.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/insert.txt b/plugin/src/main/resources/messages/holo-hover/insert.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/insertitem.txt b/plugin/src/main/resources/messages/holo-hover/insertitem.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/offset.txt b/plugin/src/main/resources/messages/holo-hover/offset.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/refreshdelay.txt b/plugin/src/main/resources/messages/holo-hover/refreshdelay.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/set.txt b/plugin/src/main/resources/messages/holo-hover/set.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo-hover/setitem.txt b/plugin/src/main/resources/messages/holo-hover/setitem.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/main/resources/messages/holo.txt b/plugin/src/main/resources/messages/holo.txt new file mode 100644 index 0000000..c4e0394 --- /dev/null +++ b/plugin/src/main/resources/messages/holo.txt @@ -0,0 +1,18 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command more info + + * /npc holo add + * /npc holo set + * /npc holo insert + + * /npc holo additem + * /npc holo setitem + * /npc holo insertitem + + * /npc holo delete + + * /npc holo offset + * /npc holo refreshdelay + * /npc holo info + diff --git a/plugin/src/main/resources/messages/property-hover/remove.txt b/plugin/src/main/resources/messages/property-hover/remove.txt new file mode 100644 index 0000000..20f6830 --- /dev/null +++ b/plugin/src/main/resources/messages/property-hover/remove.txt @@ -0,0 +1,3 @@ +Usage » /npc property remove + +Command used to unset properties on npcs \ No newline at end of file diff --git a/plugin/src/main/resources/messages/property-hover/set.txt b/plugin/src/main/resources/messages/property-hover/set.txt new file mode 100644 index 0000000..9f2888e --- /dev/null +++ b/plugin/src/main/resources/messages/property-hover/set.txt @@ -0,0 +1,3 @@ +Usage » /npc property set + +Command used to customize npcs with custom properties \ No newline at end of file diff --git a/plugin/src/main/resources/messages/property.txt b/plugin/src/main/resources/messages/property.txt new file mode 100644 index 0000000..33524c6 --- /dev/null +++ b/plugin/src/main/resources/messages/property.txt @@ -0,0 +1,7 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command more info + + * /npc property set + * /npc property remove + diff --git a/plugin/src/main/resources/messages/root-hover/center.txt b/plugin/src/main/resources/messages/root-hover/center.txt new file mode 100644 index 0000000..eb10c05 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/center.txt @@ -0,0 +1,3 @@ +Usage » /npc center + +Command used to move an npc to the center of the block it''s currently occupying \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/changeid.txt b/plugin/src/main/resources/messages/root-hover/changeid.txt new file mode 100644 index 0000000..a819ff2 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/changeid.txt @@ -0,0 +1,3 @@ +Usage » /npc changeid + +Command used to change the id of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/create.txt b/plugin/src/main/resources/messages/root-hover/create.txt new file mode 100644 index 0000000..4eb58b7 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/create.txt @@ -0,0 +1,3 @@ +Usage » /npc create + +Command used to create an npc of a given type \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/delete.txt b/plugin/src/main/resources/messages/root-hover/delete.txt new file mode 100644 index 0000000..69d0cdb --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/delete.txt @@ -0,0 +1,3 @@ +Usage » /npc delete + +Command used to delete an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/list.txt b/plugin/src/main/resources/messages/root-hover/list.txt new file mode 100644 index 0000000..4a2f00e --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/list.txt @@ -0,0 +1,3 @@ +Usage » /npc list + +Command used to list all npcs \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/lookatme.txt b/plugin/src/main/resources/messages/root-hover/lookatme.txt new file mode 100644 index 0000000..23c60a6 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/lookatme.txt @@ -0,0 +1,3 @@ +Usage » /npc lookatme + +Command used to set the rotation of an npc to be looking at your current location \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/move.txt b/plugin/src/main/resources/messages/root-hover/move.txt new file mode 100644 index 0000000..4edbf67 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/move.txt @@ -0,0 +1,3 @@ +Usage » /npc move + +Command used to set the location of an npc to your current location \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/near.txt b/plugin/src/main/resources/messages/root-hover/near.txt new file mode 100644 index 0000000..6129a95 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/near.txt @@ -0,0 +1,3 @@ +Usage » /npc near + +Command used to check which npcs are within a given radius around you \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/setlocation.txt b/plugin/src/main/resources/messages/root-hover/setlocation.txt new file mode 100644 index 0000000..94b16cc --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/setlocation.txt @@ -0,0 +1,3 @@ +Usage » /npc setlocation + +Command used to manually adjust an npc''s location \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/setrotation.txt b/plugin/src/main/resources/messages/root-hover/setrotation.txt new file mode 100644 index 0000000..ff97edf --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/setrotation.txt @@ -0,0 +1,3 @@ +Usage » /npc setrotation + +Command used to manually adjust an npc''s rotation \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/skin.txt b/plugin/src/main/resources/messages/root-hover/skin.txt new file mode 100644 index 0000000..8e6f756 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/skin.txt @@ -0,0 +1,13 @@ +Examples: + * /npc skin cool_npc1 static Notch + * /npc skin my_npc mirror + * /npc skin 12 dynamic %leaderboard_mining_top_1% + * /npc skin npc1234 url classic https://s.namemc.com/i/5d5eb6d84b57ea29.png + +Skin Types: + * Static - Only fetch the skin once and save the skin data + * Mirror - Copy the skin of the player who is viewing the npc + * Dynamic - Fetch the skin whenever the npc comes into viewing distance (supports placeholders) + * Url - Fetch the skin from an url to a raw skin file, this works like static + +Command used to change the skin of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/teleport.txt b/plugin/src/main/resources/messages/root-hover/teleport.txt new file mode 100644 index 0000000..6c4a944 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/teleport.txt @@ -0,0 +1,3 @@ +Usage » /npc teleport + +Command used to teleport yourself to an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/toggle.txt b/plugin/src/main/resources/messages/root-hover/toggle.txt new file mode 100644 index 0000000..85cd1de --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/toggle.txt @@ -0,0 +1,3 @@ +Usage » /npc toggle + +Command used to enable or disable an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root-hover/type.txt b/plugin/src/main/resources/messages/root-hover/type.txt new file mode 100644 index 0000000..93fc3f1 --- /dev/null +++ b/plugin/src/main/resources/messages/root-hover/type.txt @@ -0,0 +1,3 @@ +Usage » /npc type + +Command used to change the type of an npc \ No newline at end of file diff --git a/plugin/src/main/resources/messages/root.txt b/plugin/src/main/resources/messages/root.txt new file mode 100644 index 0000000..e7680ff --- /dev/null +++ b/plugin/src/main/resources/messages/root.txt @@ -0,0 +1,26 @@ + +ZNPCsPlus v${version} +Hover over any command for more info + + * /npc create + * /npc delete + * /npc changeid + * /npc toggle + * /npc list + * /npc type + + * /npc near + * /npc center + * /npc lookatme + * /npc setlocation + * /npc setrotation + * /npc move + * /npc teleport + + * /npc skin + + * Npc property commands
Click to view full list'>/npc property help + * Npc hologram commands
Click to view full list'>/npc holo help + * Player interaction commands
Click to view full list'>/npc action help + * Npc data storage commands
Click to view full list'>/npc storage help + diff --git a/plugin/src/main/resources/messages/storage-hover/import.txt b/plugin/src/main/resources/messages/storage-hover/import.txt new file mode 100644 index 0000000..0f758c2 --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/import.txt @@ -0,0 +1,8 @@ +Usage » /npc storage import + +Importers: + * znpcs - Imports npcs from the ZNPCs plugin + * znpcsplus_legacy - Imports npcs from legacy versions of ZNPCsPlus + * citizens - Imports npcs from the Citizens plugin + +Command used to import npcs from a different source \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage-hover/migrate.txt b/plugin/src/main/resources/messages/storage-hover/migrate.txt new file mode 100644 index 0000000..371148c --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/migrate.txt @@ -0,0 +1,16 @@ +Usage » /npc storage migrate [force] + +Storage Types: + * YAML - Npcs are stored in yaml files + * SQLite - Npcs are stored in a SQLite database + * MySQL - Npcs are stored in a MySQL database + +Command used to migrate npcs from one storage type to another. + +This command will NOT delete the original storage files or database, +but will copy the npcs to the new storage type. + +This will also not overwrite any existing npcs in the new storage +type, unless the force argument is set to true. +Warning: force will overwrite any existing npcs with the same id +in the new storage type and CANNOT be undone. \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage-hover/reload.txt b/plugin/src/main/resources/messages/storage-hover/reload.txt new file mode 100644 index 0000000..45d05a7 --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/reload.txt @@ -0,0 +1,4 @@ +Usage » /npc storage reload + +Command used to re-load all npcs from storage +Warning: This command will delete all unsaved changes to npcs \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage-hover/save.txt b/plugin/src/main/resources/messages/storage-hover/save.txt new file mode 100644 index 0000000..a255ede --- /dev/null +++ b/plugin/src/main/resources/messages/storage-hover/save.txt @@ -0,0 +1,3 @@ +Usage » /npc storage save + +Command used to save the currently loaded npcs to storage \ No newline at end of file diff --git a/plugin/src/main/resources/messages/storage.txt b/plugin/src/main/resources/messages/storage.txt new file mode 100644 index 0000000..6cfcc3f --- /dev/null +++ b/plugin/src/main/resources/messages/storage.txt @@ -0,0 +1,9 @@ + +ZNPCsPlus v${version} Click to view the main help message'>[BACK] +Hover over any command for more info + + * /npc storage save + * /npc storage reload + * /npc storage import + * /npc storage migrate [force] + diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml new file mode 100644 index 0000000..44dc2d4 --- /dev/null +++ b/plugin/src/main/resources/plugin.yml @@ -0,0 +1,33 @@ +name: ZNPCsPlus +authors: + - Pyr + - D3v1s0m + +main: lol.pyr.znpcsplus.ZNpcsPlusBootstrap +load: POSTWORLD + +version: ${version} +api-version: 1.13 + +folia-supported: true + +softdepend: + - PlaceholderAPI + - ServersNPC + - ProtocolLib + - ProtocolSupport + - ViaVersion + - ViaBackwards + - ViaRewind + - Geyser-Spigot + +loadbefore: + - Quests + +commands: + npc: + aliases: + - znpc + - znpcs + - npcs + permission: znpcsplus.command.npc diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..13c6ed6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = "ZNPCsPlus" + +include "api", "plugin" \ No newline at end of file