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
···
}
shadowJar {
+
dependsOn(check)
archiveClassifier.set(null as String?)
}
}
+2
gradle/libs.versions.toml
···
shadow = "8.3.0"
paper = "1.21.4-R0.1-SNAPSHOT"
+
configurate = "4.2.0-SNAPSHOT"
[libraries]
# build logic
···
# libraries
paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" }
+
configurate-hocon = { group = "org.spongepowered", name = "configurate-hocon", version.ref = "configurate" }
+2 -2
license_header.txt
···
-
<one line to give the program's name and a brief idea of what it does.>
+
Soulbinding
-
Copyright (C) <year> <name of author>
+
Copyright (C) 2024 kokiriglade
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
+4 -7
plugin/build.gradle.kts
···
-
//import io.papermc.paperweight.userdev.ReobfArtifactConfiguration.Companion.MOJANG_PRODUCTION // paperweight
-
plugins {
id("shadow-platform")
id("xyz.jpenilla.resource-factory-paper-convention") version "1.2.0" // paper plugin
id("xyz.jpenilla.run-paper") version "2.3.1"
}
-
//paperweight.reobfArtifactConfiguration = MOJANG_PRODUCTION // paperweight
-
dependencies {
-
// paperweight.paperDevBundle(libs.versions.paper.get()) // paperweight
compileOnly(libs.paper)
implementation(project(":${rootProject.name}-api"))
+
implementation(libs.configurate.hocon)
}
tasks {
···
}
paperPluginYaml {
-
main = "${rootProject.group}.${rootProject.name}.TemplatePlugin"
+
main = "${rootProject.group}.${rootProject.name}.Soulbinding"
+
bootstrapper = "${rootProject.group}.${rootProject.name}.SoulbindingBootstrap"
name = rootProject.name
authors.add("kokiriglade")
-
apiVersion = libs.versions.paper.get()
+
apiVersion = libs.versions.paper.get().split("-R0.1-SNAPSHOT")[0]
}
+104
plugin/src/main/java/de/kokirigla/soulbinding/SoulbindingBootstrap.java
···
+
/*
+
* Soulbinding
+
*
+
* Copyright (C) 2024 kokiriglade
+
*
+
* 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 <https://www.gnu.org/licenses/>.
+
*/
+
package de.kokirigla.soulbinding;
+
+
import de.kokirigla.soulbinding.configuration.ConfigHelper;
+
import de.kokirigla.soulbinding.configuration.MainConfig;
+
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
+
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
+
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
+
import io.papermc.paper.registry.RegistryKey;
+
import io.papermc.paper.registry.data.EnchantmentRegistryEntry;
+
import io.papermc.paper.registry.event.RegistryEvents;
+
import io.papermc.paper.registry.keys.EnchantmentKeys;
+
import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys;
+
import io.papermc.paper.registry.set.RegistrySet;
+
import io.papermc.paper.registry.tag.TagKey;
+
import io.papermc.paper.tag.TagEntry;
+
import net.kyori.adventure.key.Key;
+
import org.bukkit.inventory.EquipmentSlotGroup;
+
import org.bukkit.inventory.ItemType;
+
import org.jspecify.annotations.NullMarked;
+
import org.jspecify.annotations.Nullable;
+
+
import java.util.Objects;
+
import java.util.stream.Collectors;
+
import java.util.stream.Stream;
+
+
@NullMarked
+
public final class SoulbindingBootstrap implements PluginBootstrap {
+
+
public static final Key SOULBOUND_ENCHANTMENT = Key.key("soulbinding:soulbound");
+
public static final TagKey<ItemType> SOULBOUNDABLE_TAG = ItemTypeTagKeys.create(Key.key("soulbinding:soulboundable"));
+
+
private @Nullable MainConfig config;
+
+
@Override
+
public void bootstrap(final BootstrapContext context) {
+
this.config = ConfigHelper.loadConfig(MainConfig.class, context.getDataDirectory().resolve("config.conf"));
+
ConfigHelper.saveConfig(context.getDataDirectory().resolve("config.conf"), this.config);
+
+
context.getLifecycleManager().registerEventHandler(RegistryEvents.ENCHANTMENT.freeze().newHandler(event -> {
+
event.registry().register(
+
EnchantmentKeys.create(SOULBOUND_ENCHANTMENT),
+
b -> b.description(config.soulboundDescription())
+
.supportedItems(event.getOrCreateTag(SOULBOUNDABLE_TAG))
+
.anvilCost(3)
+
.maxLevel(1)
+
.weight(1)
+
.exclusiveWith(
+
RegistrySet.keySet(
+
RegistryKey.ENCHANTMENT,
+
EnchantmentKeys.BINDING_CURSE,
+
EnchantmentKeys.VANISHING_CURSE
+
)
+
)
+
.minimumCost(EnchantmentRegistryEntry.EnchantmentCost.of(1, 1))
+
.maximumCost(EnchantmentRegistryEntry.EnchantmentCost.of(3, 1))
+
.activeSlots(EquipmentSlotGroup.ANY)
+
);
+
}));
+
+
context.getLifecycleManager().registerEventHandler(LifecycleEvents.TAGS.preFlatten(
+
RegistryKey.ITEM).newHandler(event -> {
+
event.registrar().addToTag(
+
SOULBOUNDABLE_TAG,
+
Stream.of(
+
ItemTypeTagKeys.ENCHANTABLE_WEAPON,
+
ItemTypeTagKeys.ENCHANTABLE_MINING,
+
ItemTypeTagKeys.ENCHANTABLE_TRIDENT,
+
ItemTypeTagKeys.ENCHANTABLE_BOW,
+
ItemTypeTagKeys.ENCHANTABLE_CROSSBOW,
+
ItemTypeTagKeys.ENCHANTABLE_EQUIPPABLE,
+
ItemTypeTagKeys.LECTERN_BOOKS
+
)
+
.map(TagEntry::tagEntry)
+
.collect(Collectors.toSet())
+
);
+
}));
+
}
+
+
@Override
+
public Soulbinding createPlugin(final PluginProviderContext context) {
+
Objects.requireNonNull(config);
+
return new Soulbinding(config);
+
}
+
+
}
+17 -4
plugin/src/main/java/de/kokirigla/soulbinding/SoulbindingPlugin.java plugin/src/main/java/de/kokirigla/soulbinding/Soulbinding.java
···
/*
-
* <one line to give the program's name and a brief idea of what it does.>
+
* Soulbinding
*
-
* Copyright (C) <year> <name of author>
+
* Copyright (C) 2024 kokiriglade
*
* 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
···
*/
package de.kokirigla.soulbinding;
+
import de.kokirigla.soulbinding.configuration.MainConfig;
+
import de.kokirigla.soulbinding.listener.DeathListener;
import org.bukkit.plugin.java.JavaPlugin;
import org.jspecify.annotations.NullMarked;
@NullMarked
-
public final class SoulbindingPlugin extends JavaPlugin {
+
public final class Soulbinding extends JavaPlugin {
+
+
private final MainConfig config;
+
+
public Soulbinding(final MainConfig config) {
+
super();
+
this.config = config;
+
}
@Override
public void onEnable() {
-
getSLF4JLogger().info("Hello World!");
+
getServer().getPluginManager().registerEvents(new DeathListener(this), this);
+
}
+
+
public MainConfig config() {
+
return config;
}
}
+109
plugin/src/main/java/de/kokirigla/soulbinding/configuration/ConfigHelper.java
···
+
/*
+
* Soulbinding
+
*
+
* Copyright (C) 2024 kokiriglade
+
*
+
* 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 <https://www.gnu.org/licenses/>.
+
*/
+
package de.kokirigla.soulbinding.configuration;
+
+
import io.leangen.geantyref.TypeToken;
+
import org.jspecify.annotations.NullMarked;
+
import org.jspecify.annotations.Nullable;
+
import org.spongepowered.configurate.CommentedConfigurationNode;
+
import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
+
import org.spongepowered.configurate.objectmapping.ObjectMapper;
+
import org.spongepowered.configurate.util.NamingSchemes;
+
+
import java.nio.file.Files;
+
import java.nio.file.Path;
+
import java.util.Objects;
+
import java.util.function.Supplier;
+
+
@SuppressWarnings("unused")
+
@NullMarked
+
public final class ConfigHelper {
+
+
public static HoconConfigurationLoader createLoader(final Path file) {
+
final ObjectMapper.Factory factory = ObjectMapper.factoryBuilder()
+
.defaultNamingScheme(NamingSchemes.SNAKE_CASE)
+
.build();
+
+
return HoconConfigurationLoader.builder()
+
.defaultOptions(options -> options.serializers(build -> build.registerAnnotatedObjects(factory)))
+
.path(file)
+
.build();
+
}
+
+
+
public static <T> T loadConfig(final TypeToken<T> configType,
+
final Path path,
+
final Supplier<T> defaultConfigFactory) {
+
try {
+
if (Files.isRegularFile(path)) {
+
final HoconConfigurationLoader loader = createLoader(path);
+
final CommentedConfigurationNode node = loader.load();
+
return Objects.requireNonNull(node.get(configType));
+
} else {
+
return defaultConfigFactory.get();
+
}
+
} catch (final Exception ex) {
+
throw new RuntimeException("Failed to load config of type '" + configType.getType()
+
.getTypeName() + "' from file at '" + path + "'.", ex);
+
}
+
}
+
+
// For @ConfigSerializable types with no args constructor
+
public static <T> T loadConfig(final Class<T> configType, final Path path) {
+
return loadConfig(TypeToken.get(configType), path, () -> {
+
try {
+
return configType.getConstructor().newInstance();
+
} catch (final ReflectiveOperationException ex) {
+
throw new RuntimeException("Failed to create instance of type " + configType.getName() + ", does it have a public no args constructor?");
+
}
+
});
+
}
+
+
public static <T> void saveConfig(final Path path,
+
final TypeToken<T> configType,
+
final T config) {
+
saveConfig(path, config, configType);
+
}
+
+
// For @ConfigSerializable types
+
public static void saveConfig(final Path path, final Object config) {
+
saveConfig(path, config, null);
+
}
+
+
@SuppressWarnings({"unchecked", "rawtypes"})
+
private static void saveConfig(final Path path,
+
final Object config,
+
final @Nullable TypeToken<?> configType) {
+
try {
+
Files.createDirectories(path.getParent());
+
final HoconConfigurationLoader loader = createLoader(path);
+
final CommentedConfigurationNode node = loader.createNode();
+
if (configType != null) {
+
node.set((TypeToken) configType, config);
+
} else {
+
node.set(config);
+
}
+
loader.save(node);
+
} catch (final Exception ex) {
+
throw new RuntimeException("Failed to save config of type '" + (configType != null ? configType.getType()
+
.getTypeName() : config.getClass().getName()) + "' to file at '" + path + "'.", ex);
+
}
+
}
+
+
}
+48
plugin/src/main/java/de/kokirigla/soulbinding/configuration/MainConfig.java
···
+
/*
+
* Soulbinding
+
*
+
* Copyright (C) 2024 kokiriglade
+
*
+
* 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 <https://www.gnu.org/licenses/>.
+
*/
+
package de.kokirigla.soulbinding.configuration;
+
+
import net.kyori.adventure.text.Component;
+
import net.kyori.adventure.text.minimessage.MiniMessage;
+
import org.jspecify.annotations.NullMarked;
+
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
import org.spongepowered.configurate.objectmapping.meta.Comment;
+
+
@ConfigSerializable
+
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
+
@NullMarked
+
public final class MainConfig {
+
+
private String soulboundDescription = "<lang:enchantment.soulbinding.soulbound>";
+
+
@Comment("Chance of a Soulbound enchantment book being dropped upon killing the ender dragon. 10% by default")
+
private double chance = 0.10d;
+
+
public MainConfig() {
+
}
+
+
public Component soulboundDescription() {
+
return MiniMessage.miniMessage().deserialize(this.soulboundDescription);
+
}
+
+
public double chance() {
+
return this.chance;
+
}
+
+
}
+92
plugin/src/main/java/de/kokirigla/soulbinding/listener/DeathListener.java
···
+
/*
+
* Soulbinding
+
*
+
* Copyright (C) 2024 kokiriglade
+
*
+
* 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 <https://www.gnu.org/licenses/>.
+
*/
+
package de.kokirigla.soulbinding.listener;
+
+
import de.kokirigla.soulbinding.Soulbinding;
+
import de.kokirigla.soulbinding.SoulbindingBootstrap;
+
import io.papermc.paper.datacomponent.DataComponentTypes;
+
import io.papermc.paper.datacomponent.item.ItemEnchantments;
+
import io.papermc.paper.registry.RegistryAccess;
+
import io.papermc.paper.registry.RegistryKey;
+
import org.bukkit.Material;
+
import org.bukkit.enchantments.Enchantment;
+
import org.bukkit.entity.EnderDragon;
+
import org.bukkit.event.EventHandler;
+
import org.bukkit.event.EventPriority;
+
import org.bukkit.event.Listener;
+
import org.bukkit.event.entity.EntityDeathEvent;
+
import org.bukkit.event.entity.PlayerDeathEvent;
+
import org.bukkit.inventory.ItemStack;
+
+
import java.util.HashSet;
+
import java.util.Set;
+
+
public final class DeathListener implements Listener {
+
+
private final Soulbinding plugin;
+
+
public DeathListener(final Soulbinding plugin) {
+
this.plugin = plugin;
+
}
+
+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+
public void onPlayerDeath(final PlayerDeathEvent event) {
+
final Set<ItemStack> soulboundItems = new HashSet<>();
+
+
for (final ItemStack drop : event.getDrops()) {
+
if(drop.getEnchantments().containsKey(this.soulboundEnchantment())) {
+
soulboundItems.add(drop);
+
}
+
}
+
+
soulboundItems.forEach(item -> {
+
event.getDrops().remove(item);
+
event.getItemsToKeep().add(item);
+
});
+
}
+
+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+
public void onEnderDragonDeath(final EntityDeathEvent event) {
+
if(event.getEntity() instanceof EnderDragon dragon) {
+
if (Math.random() <= plugin.config().chance()) {
+
dragon.getLocation().getWorld().dropItemNaturally(
+
dragon.getLocation(),
+
createSoulboundBook()
+
);
+
}
+
}
+
}
+
+
private ItemStack createSoulboundBook() {
+
final ItemStack book = ItemStack.of(Material.ENCHANTED_BOOK);
+
book.setData(
+
DataComponentTypes.STORED_ENCHANTMENTS,
+
ItemEnchantments.itemEnchantments()
+
.add(soulboundEnchantment(), 1)
+
.build()
+
);
+
return book;
+
}
+
+
private Enchantment soulboundEnchantment() {
+
return RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).getOrThrow(
+
SoulbindingBootstrap.SOULBOUND_ENCHANTMENT);
+
}
+
+
}
+8 -1
settings.gradle.kts
···
dependencyResolutionManagement {
repositories {
mavenCentral()
-
maven {
name = "papermc"
url = uri("https://repo.papermc.io/repository/maven-public/")
+
}
+
maven {
+
name = "spongepowered"
+
url = uri("https://repo.spongepowered.org/maven/")
+
+
mavenContent {
+
includeModule("org.spongepowered", "configurate-hocon")
+
}
}
}
}