paper plugin that introduces the "Soulbound" enchantment

MVP

Changed files
+387 -14
build-logic
src
gradle
plugin
+1
build-logic/src/main/kotlin/shadow-platform.gradle.kts
··· 10 10 } 11 11 12 12 shadowJar { 13 + dependsOn(check) 13 14 archiveClassifier.set(null as String?) 14 15 } 15 16 }
+2
gradle/libs.versions.toml
··· 4 4 shadow = "8.3.0" 5 5 6 6 paper = "1.21.4-R0.1-SNAPSHOT" 7 + configurate = "4.2.0-SNAPSHOT" 7 8 8 9 [libraries] 9 10 # build logic ··· 17 18 18 19 # libraries 19 20 paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } 21 + configurate-hocon = { group = "org.spongepowered", name = "configurate-hocon", version.ref = "configurate" }
+2 -2
license_header.txt
··· 1 - <one line to give the program's name and a brief idea of what it does.> 1 + Soulbinding 2 2 3 - Copyright (C) <year> <name of author> 3 + Copyright (C) 2024 kokiriglade 4 4 5 5 This program is free software: you can redistribute it and/or modify 6 6 it under the terms of the GNU General Public License as published by
+4 -7
plugin/build.gradle.kts
··· 1 - //import io.papermc.paperweight.userdev.ReobfArtifactConfiguration.Companion.MOJANG_PRODUCTION // paperweight 2 - 3 1 plugins { 4 2 id("shadow-platform") 5 3 id("xyz.jpenilla.resource-factory-paper-convention") version "1.2.0" // paper plugin 6 4 id("xyz.jpenilla.run-paper") version "2.3.1" 7 5 } 8 6 9 - //paperweight.reobfArtifactConfiguration = MOJANG_PRODUCTION // paperweight 10 - 11 7 dependencies { 12 - // paperweight.paperDevBundle(libs.versions.paper.get()) // paperweight 13 8 compileOnly(libs.paper) 14 9 implementation(project(":${rootProject.name}-api")) 10 + implementation(libs.configurate.hocon) 15 11 } 16 12 17 13 tasks { ··· 24 20 } 25 21 26 22 paperPluginYaml { 27 - main = "${rootProject.group}.${rootProject.name}.TemplatePlugin" 23 + main = "${rootProject.group}.${rootProject.name}.Soulbinding" 24 + bootstrapper = "${rootProject.group}.${rootProject.name}.SoulbindingBootstrap" 28 25 name = rootProject.name 29 26 authors.add("kokiriglade") 30 - apiVersion = libs.versions.paper.get() 27 + apiVersion = libs.versions.paper.get().split("-R0.1-SNAPSHOT")[0] 31 28 }
+104
plugin/src/main/java/de/kokirigla/soulbinding/SoulbindingBootstrap.java
··· 1 + /* 2 + * Soulbinding 3 + * 4 + * Copyright (C) 2024 kokiriglade 5 + * 6 + * This program is free software: you can redistribute it and/or modify 7 + * it under the terms of the GNU General Public License as published by 8 + * the Free Software Foundation, either version 3 of the License, or 9 + * (at your option) any later version. 10 + * 11 + * This program is distributed in the hope that it will be useful, 12 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 + * GNU General Public License for more details. 15 + * 16 + * You should have received a copy of the GNU General Public License 17 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 18 + */ 19 + package de.kokirigla.soulbinding; 20 + 21 + import de.kokirigla.soulbinding.configuration.ConfigHelper; 22 + import de.kokirigla.soulbinding.configuration.MainConfig; 23 + import io.papermc.paper.plugin.bootstrap.BootstrapContext; 24 + import io.papermc.paper.plugin.bootstrap.PluginBootstrap; 25 + import io.papermc.paper.plugin.bootstrap.PluginProviderContext; 26 + import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; 27 + import io.papermc.paper.registry.RegistryKey; 28 + import io.papermc.paper.registry.data.EnchantmentRegistryEntry; 29 + import io.papermc.paper.registry.event.RegistryEvents; 30 + import io.papermc.paper.registry.keys.EnchantmentKeys; 31 + import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; 32 + import io.papermc.paper.registry.set.RegistrySet; 33 + import io.papermc.paper.registry.tag.TagKey; 34 + import io.papermc.paper.tag.TagEntry; 35 + import net.kyori.adventure.key.Key; 36 + import org.bukkit.inventory.EquipmentSlotGroup; 37 + import org.bukkit.inventory.ItemType; 38 + import org.jspecify.annotations.NullMarked; 39 + import org.jspecify.annotations.Nullable; 40 + 41 + import java.util.Objects; 42 + import java.util.stream.Collectors; 43 + import java.util.stream.Stream; 44 + 45 + @NullMarked 46 + public final class SoulbindingBootstrap implements PluginBootstrap { 47 + 48 + public static final Key SOULBOUND_ENCHANTMENT = Key.key("soulbinding:soulbound"); 49 + public static final TagKey<ItemType> SOULBOUNDABLE_TAG = ItemTypeTagKeys.create(Key.key("soulbinding:soulboundable")); 50 + 51 + private @Nullable MainConfig config; 52 + 53 + @Override 54 + public void bootstrap(final BootstrapContext context) { 55 + this.config = ConfigHelper.loadConfig(MainConfig.class, context.getDataDirectory().resolve("config.conf")); 56 + ConfigHelper.saveConfig(context.getDataDirectory().resolve("config.conf"), this.config); 57 + 58 + context.getLifecycleManager().registerEventHandler(RegistryEvents.ENCHANTMENT.freeze().newHandler(event -> { 59 + event.registry().register( 60 + EnchantmentKeys.create(SOULBOUND_ENCHANTMENT), 61 + b -> b.description(config.soulboundDescription()) 62 + .supportedItems(event.getOrCreateTag(SOULBOUNDABLE_TAG)) 63 + .anvilCost(3) 64 + .maxLevel(1) 65 + .weight(1) 66 + .exclusiveWith( 67 + RegistrySet.keySet( 68 + RegistryKey.ENCHANTMENT, 69 + EnchantmentKeys.BINDING_CURSE, 70 + EnchantmentKeys.VANISHING_CURSE 71 + ) 72 + ) 73 + .minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(1, 1)) 74 + .maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(3, 1)) 75 + .activeSlots(EquipmentSlotGroup.ANY) 76 + ); 77 + })); 78 + 79 + context.getLifecycleManager().registerEventHandler(LifecycleEvents.TAGS.preFlatten( 80 + RegistryKey.ITEM).newHandler(event -> { 81 + event.registrar().addToTag( 82 + SOULBOUNDABLE_TAG, 83 + Stream.of( 84 + ItemTypeTagKeys.ENCHANTABLE_WEAPON, 85 + ItemTypeTagKeys.ENCHANTABLE_MINING, 86 + ItemTypeTagKeys.ENCHANTABLE_TRIDENT, 87 + ItemTypeTagKeys.ENCHANTABLE_BOW, 88 + ItemTypeTagKeys.ENCHANTABLE_CROSSBOW, 89 + ItemTypeTagKeys.ENCHANTABLE_EQUIPPABLE, 90 + ItemTypeTagKeys.LECTERN_BOOKS 91 + ) 92 + .map(TagEntry::tagEntry) 93 + .collect(Collectors.toSet()) 94 + ); 95 + })); 96 + } 97 + 98 + @Override 99 + public Soulbinding createPlugin(final PluginProviderContext context) { 100 + Objects.requireNonNull(config); 101 + return new Soulbinding(config); 102 + } 103 + 104 + }
+17 -4
plugin/src/main/java/de/kokirigla/soulbinding/SoulbindingPlugin.java plugin/src/main/java/de/kokirigla/soulbinding/Soulbinding.java
··· 1 1 /* 2 - * <one line to give the program's name and a brief idea of what it does.> 2 + * Soulbinding 3 3 * 4 - * Copyright (C) <year> <name of author> 4 + * Copyright (C) 2024 kokiriglade 5 5 * 6 6 * This program is free software: you can redistribute it and/or modify 7 7 * it under the terms of the GNU General Public License as published by ··· 18 18 */ 19 19 package de.kokirigla.soulbinding; 20 20 21 + import de.kokirigla.soulbinding.configuration.MainConfig; 22 + import de.kokirigla.soulbinding.listener.DeathListener; 21 23 import org.bukkit.plugin.java.JavaPlugin; 22 24 import org.jspecify.annotations.NullMarked; 23 25 24 26 @NullMarked 25 - public final class SoulbindingPlugin extends JavaPlugin { 27 + public final class Soulbinding extends JavaPlugin { 28 + 29 + private final MainConfig config; 30 + 31 + public Soulbinding(final MainConfig config) { 32 + super(); 33 + this.config = config; 34 + } 26 35 27 36 @Override 28 37 public void onEnable() { 29 - getSLF4JLogger().info("Hello World!"); 38 + getServer().getPluginManager().registerEvents(new DeathListener(this), this); 39 + } 40 + 41 + public MainConfig config() { 42 + return config; 30 43 } 31 44 32 45 }
+109
plugin/src/main/java/de/kokirigla/soulbinding/configuration/ConfigHelper.java
··· 1 + /* 2 + * Soulbinding 3 + * 4 + * Copyright (C) 2024 kokiriglade 5 + * 6 + * This program is free software: you can redistribute it and/or modify 7 + * it under the terms of the GNU General Public License as published by 8 + * the Free Software Foundation, either version 3 of the License, or 9 + * (at your option) any later version. 10 + * 11 + * This program is distributed in the hope that it will be useful, 12 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 + * GNU General Public License for more details. 15 + * 16 + * You should have received a copy of the GNU General Public License 17 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 18 + */ 19 + package de.kokirigla.soulbinding.configuration; 20 + 21 + import io.leangen.geantyref.TypeToken; 22 + import org.jspecify.annotations.NullMarked; 23 + import org.jspecify.annotations.Nullable; 24 + import org.spongepowered.configurate.CommentedConfigurationNode; 25 + import org.spongepowered.configurate.hocon.HoconConfigurationLoader; 26 + import org.spongepowered.configurate.objectmapping.ObjectMapper; 27 + import org.spongepowered.configurate.util.NamingSchemes; 28 + 29 + import java.nio.file.Files; 30 + import java.nio.file.Path; 31 + import java.util.Objects; 32 + import java.util.function.Supplier; 33 + 34 + @SuppressWarnings("unused") 35 + @NullMarked 36 + public final class ConfigHelper { 37 + 38 + public static HoconConfigurationLoader createLoader(final Path file) { 39 + final ObjectMapper.Factory factory = ObjectMapper.factoryBuilder() 40 + .defaultNamingScheme(NamingSchemes.SNAKE_CASE) 41 + .build(); 42 + 43 + return HoconConfigurationLoader.builder() 44 + .defaultOptions(options -> options.serializers(build -> build.registerAnnotatedObjects(factory))) 45 + .path(file) 46 + .build(); 47 + } 48 + 49 + 50 + public static <T> T loadConfig(final TypeToken<T> configType, 51 + final Path path, 52 + final Supplier<T> defaultConfigFactory) { 53 + try { 54 + if (Files.isRegularFile(path)) { 55 + final HoconConfigurationLoader loader = createLoader(path); 56 + final CommentedConfigurationNode node = loader.load(); 57 + return Objects.requireNonNull(node.get(configType)); 58 + } else { 59 + return defaultConfigFactory.get(); 60 + } 61 + } catch (final Exception ex) { 62 + throw new RuntimeException("Failed to load config of type '" + configType.getType() 63 + .getTypeName() + "' from file at '" + path + "'.", ex); 64 + } 65 + } 66 + 67 + // For @ConfigSerializable types with no args constructor 68 + public static <T> T loadConfig(final Class<T> configType, final Path path) { 69 + return loadConfig(TypeToken.get(configType), path, () -> { 70 + try { 71 + return configType.getConstructor().newInstance(); 72 + } catch (final ReflectiveOperationException ex) { 73 + throw new RuntimeException("Failed to create instance of type " + configType.getName() + ", does it have a public no args constructor?"); 74 + } 75 + }); 76 + } 77 + 78 + public static <T> void saveConfig(final Path path, 79 + final TypeToken<T> configType, 80 + final T config) { 81 + saveConfig(path, config, configType); 82 + } 83 + 84 + // For @ConfigSerializable types 85 + public static void saveConfig(final Path path, final Object config) { 86 + saveConfig(path, config, null); 87 + } 88 + 89 + @SuppressWarnings({"unchecked", "rawtypes"}) 90 + private static void saveConfig(final Path path, 91 + final Object config, 92 + final @Nullable TypeToken<?> configType) { 93 + try { 94 + Files.createDirectories(path.getParent()); 95 + final HoconConfigurationLoader loader = createLoader(path); 96 + final CommentedConfigurationNode node = loader.createNode(); 97 + if (configType != null) { 98 + node.set((TypeToken) configType, config); 99 + } else { 100 + node.set(config); 101 + } 102 + loader.save(node); 103 + } catch (final Exception ex) { 104 + throw new RuntimeException("Failed to save config of type '" + (configType != null ? configType.getType() 105 + .getTypeName() : config.getClass().getName()) + "' to file at '" + path + "'.", ex); 106 + } 107 + } 108 + 109 + }
+48
plugin/src/main/java/de/kokirigla/soulbinding/configuration/MainConfig.java
··· 1 + /* 2 + * Soulbinding 3 + * 4 + * Copyright (C) 2024 kokiriglade 5 + * 6 + * This program is free software: you can redistribute it and/or modify 7 + * it under the terms of the GNU General Public License as published by 8 + * the Free Software Foundation, either version 3 of the License, or 9 + * (at your option) any later version. 10 + * 11 + * This program is distributed in the hope that it will be useful, 12 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 + * GNU General Public License for more details. 15 + * 16 + * You should have received a copy of the GNU General Public License 17 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 18 + */ 19 + package de.kokirigla.soulbinding.configuration; 20 + 21 + import net.kyori.adventure.text.Component; 22 + import net.kyori.adventure.text.minimessage.MiniMessage; 23 + import org.jspecify.annotations.NullMarked; 24 + import org.spongepowered.configurate.objectmapping.ConfigSerializable; 25 + import org.spongepowered.configurate.objectmapping.meta.Comment; 26 + 27 + @ConfigSerializable 28 + @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) 29 + @NullMarked 30 + public final class MainConfig { 31 + 32 + private String soulboundDescription = "<lang:enchantment.soulbinding.soulbound>"; 33 + 34 + @Comment("Chance of a Soulbound enchantment book being dropped upon killing the ender dragon. 10% by default") 35 + private double chance = 0.10d; 36 + 37 + public MainConfig() { 38 + } 39 + 40 + public Component soulboundDescription() { 41 + return MiniMessage.miniMessage().deserialize(this.soulboundDescription); 42 + } 43 + 44 + public double chance() { 45 + return this.chance; 46 + } 47 + 48 + }
+92
plugin/src/main/java/de/kokirigla/soulbinding/listener/DeathListener.java
··· 1 + /* 2 + * Soulbinding 3 + * 4 + * Copyright (C) 2024 kokiriglade 5 + * 6 + * This program is free software: you can redistribute it and/or modify 7 + * it under the terms of the GNU General Public License as published by 8 + * the Free Software Foundation, either version 3 of the License, or 9 + * (at your option) any later version. 10 + * 11 + * This program is distributed in the hope that it will be useful, 12 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 + * GNU General Public License for more details. 15 + * 16 + * You should have received a copy of the GNU General Public License 17 + * along with this program. If not, see <https://www.gnu.org/licenses/>. 18 + */ 19 + package de.kokirigla.soulbinding.listener; 20 + 21 + import de.kokirigla.soulbinding.Soulbinding; 22 + import de.kokirigla.soulbinding.SoulbindingBootstrap; 23 + import io.papermc.paper.datacomponent.DataComponentTypes; 24 + import io.papermc.paper.datacomponent.item.ItemEnchantments; 25 + import io.papermc.paper.registry.RegistryAccess; 26 + import io.papermc.paper.registry.RegistryKey; 27 + import org.bukkit.Material; 28 + import org.bukkit.enchantments.Enchantment; 29 + import org.bukkit.entity.EnderDragon; 30 + import org.bukkit.event.EventHandler; 31 + import org.bukkit.event.EventPriority; 32 + import org.bukkit.event.Listener; 33 + import org.bukkit.event.entity.EntityDeathEvent; 34 + import org.bukkit.event.entity.PlayerDeathEvent; 35 + import org.bukkit.inventory.ItemStack; 36 + 37 + import java.util.HashSet; 38 + import java.util.Set; 39 + 40 + public final class DeathListener implements Listener { 41 + 42 + private final Soulbinding plugin; 43 + 44 + public DeathListener(final Soulbinding plugin) { 45 + this.plugin = plugin; 46 + } 47 + 48 + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 49 + public void onPlayerDeath(final PlayerDeathEvent event) { 50 + final Set<ItemStack> soulboundItems = new HashSet<>(); 51 + 52 + for (final ItemStack drop : event.getDrops()) { 53 + if(drop.getEnchantments().containsKey(this.soulboundEnchantment())) { 54 + soulboundItems.add(drop); 55 + } 56 + } 57 + 58 + soulboundItems.forEach(item -> { 59 + event.getDrops().remove(item); 60 + event.getItemsToKeep().add(item); 61 + }); 62 + } 63 + 64 + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 65 + public void onEnderDragonDeath(final EntityDeathEvent event) { 66 + if(event.getEntity() instanceof EnderDragon dragon) { 67 + if (Math.random() <= plugin.config().chance()) { 68 + dragon.getLocation().getWorld().dropItemNaturally( 69 + dragon.getLocation(), 70 + createSoulboundBook() 71 + ); 72 + } 73 + } 74 + } 75 + 76 + private ItemStack createSoulboundBook() { 77 + final ItemStack book = ItemStack.of(Material.ENCHANTED_BOOK); 78 + book.setData( 79 + DataComponentTypes.STORED_ENCHANTMENTS, 80 + ItemEnchantments.itemEnchantments() 81 + .add(soulboundEnchantment(), 1) 82 + .build() 83 + ); 84 + return book; 85 + } 86 + 87 + private Enchantment soulboundEnchantment() { 88 + return RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).getOrThrow( 89 + SoulbindingBootstrap.SOULBOUND_ENCHANTMENT); 90 + } 91 + 92 + }
+8 -1
settings.gradle.kts
··· 1 1 dependencyResolutionManagement { 2 2 repositories { 3 3 mavenCentral() 4 - 5 4 maven { 6 5 name = "papermc" 7 6 url = uri("https://repo.papermc.io/repository/maven-public/") 7 + } 8 + maven { 9 + name = "spongepowered" 10 + url = uri("https://repo.spongepowered.org/maven/") 11 + 12 + mavenContent { 13 + includeModule("org.spongepowered", "configurate-hocon") 14 + } 8 15 } 9 16 } 10 17 }