diff --git a/.github/workflows/build_app.yml b/.github/workflows/build_app.yml index 72dca2f2e..fafa67f83 100644 --- a/.github/workflows/build_app.yml +++ b/.github/workflows/build_app.yml @@ -19,7 +19,7 @@ jobs: steps: - id: checkout name: Checkout - uses: actions/checkout@v6.0.1 + uses: actions/checkout@v6.0.2 - id: setup-java name: Setup Java @@ -30,7 +30,7 @@ jobs: - id: setup-gradle name: Setup Gradle - uses: gradle/actions/setup-gradle@63b23c47ec369cc3851ad527daec269a2f949651 + uses: gradle/actions/setup-gradle@576fcbecfed70890e466eeffd7c78d93b30b0472 - id: build-gradle name: Build with Gradle @@ -39,6 +39,6 @@ jobs: - id: upload-artifact name: Upload Build Artifact if: ${{ success() }} - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: path: build/libs/*.jar diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 8f217eb9b..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: "CodeQL Advanced" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '33 16 * * 3' - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - - language: java-kotlin - build-mode: manual - - steps: - - id: checkout - name: Checkout - uses: actions/checkout@v6.0.1 - - - id: setup-java - name: Setup Java - uses: actions/setup-java@v5 - with: - java-version: 21 - distribution: temurin - - - id: setup-gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@576fcbecfed70890e466eeffd7c78d93b30b0472 - - - id: init-codeql - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - id: build-gradle - name: Build with Gradle - if: matrix.build-mode == 'manual' - shell: bash - run: ./gradlew shadowJar - - - id: perform-codeql-analysis - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1bec35e57..6c39b1b83 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,7 @@ + diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 2fa839b98..ffe3b8db7 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -4,8 +4,10 @@ ageable appleboy armorposer + armorstand birdflop cmds + codeql coord decentholograms decentsoftware @@ -19,6 +21,7 @@ glorp gradleup griefing + illyrius intellectualsites invs invsearch @@ -37,8 +40,12 @@ searchinv shulker signedit + sinv + slenderman spellbite + tablist tellraw + temurin triumphteam unloadinv userdev diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 8ad8c8610..739bc3658 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 4f712e5b7..457fbe6d8 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,12 +2,14 @@ + + + - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6ac43961b..a8eba3472 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,11 @@ -import xyz.jpenilla.resourcefactory.paper.PaperPluginYaml import xyz.jpenilla.runtask.task.AbstractRun plugins { id("java") id("idea") - kotlin("jvm") version "2.2.21" - kotlin("plugin.serialization") version "2.2.21" - id("com.gradleup.shadow") version "9.3.0" + kotlin("jvm") version "2.3.0" + kotlin("plugin.serialization") version "2.3.0" + id("com.gradleup.shadow") version "9.3.1" id("xyz.jpenilla.run-paper") version "3.0.2" id("xyz.jpenilla.resource-factory-paper-convention") version "1.3.1" } @@ -20,16 +19,14 @@ description = "Minecraft plugin that enhances the base gameplay" repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") - maven("https://maven.enginehub.org/repo/") } dependencies { compileOnly("io.papermc.paper:paper-api:$version-R0.1-SNAPSHOT") - compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.17") // TODO("Move away from WorldEdit") implementation(kotlin("stdlib")) implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") } java { @@ -57,10 +54,8 @@ tasks { paperPluginYaml { main.set(group.toString()) + website.set("https://github.com/XodiumSoftware/VanillaPlus") authors.add("Xodium") apiVersion.set(version) bootstrapper.set("org.xodium.vanillaplus.VanillaPlusBootstrap") - dependencies { - server(name = "WorldEdit", load = PaperPluginYaml.Load.BEFORE, required = false, joinClasspath = true) - } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee312..61285a659 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2b5..19a6bdeb8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt b/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt index 90f6ab9ce..f554db910 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/VanillaPlus.kt @@ -5,11 +5,12 @@ package org.xodium.vanillaplus import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents import org.bukkit.plugin.java.JavaPlugin import org.xodium.vanillaplus.data.ConfigData -import org.xodium.vanillaplus.hooks.WorldEditHook -import org.xodium.vanillaplus.managers.ConfigManager +import org.xodium.vanillaplus.data.ConfigData.Companion.load import org.xodium.vanillaplus.modules.* +import org.xodium.vanillaplus.modules.ArmorStandModule.info +import org.xodium.vanillaplus.recipes.PaintingRecipe +import org.xodium.vanillaplus.recipes.PaintingRecipe.info import org.xodium.vanillaplus.recipes.RottenFleshRecipe -import org.xodium.vanillaplus.recipes.TorchArrowRecipe import org.xodium.vanillaplus.recipes.WoodLogRecipe /** Main class of the plugin. */ @@ -34,35 +35,43 @@ internal class VanillaPlus : JavaPlugin() { instance.lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> event.registrar().register( - ConfigManager.reloadCommand.builder.build(), - ConfigManager.reloadCommand.description, - ConfigManager.reloadCommand.aliases, + ConfigData.reloadCommand.builder.build(), + ConfigData.reloadCommand.description, + ConfigData.reloadCommand.aliases, ) } - instance.server.pluginManager.addPermission(ConfigManager.reloadPermission) - configData = ConfigManager.load() + instance.server.pluginManager.addPermission(ConfigData.reloadPermission) - RottenFleshRecipe.register() - TorchArrowRecipe.register() - WoodLogRecipe.register() + configData = ConfigData().load("config.json") - BooksModule.register() - ChatModule.register() - DimensionsModule.register() - EntityModule.register() - InvModule.register() - LocatorModule.register() - MotdModule.register() - OpenableModule.register() - PetModule.register() - PlayerModule.register() - ScoreBoardModule.register() - SignModule.register() - SitModule.register() - TabListModule.register() - ArrowModule.register() - if (WorldEditHook.get()) TreesModule.register() + logger.info( + listOf( + PaintingRecipe, + RottenFleshRecipe, + WoodLogRecipe, + ), + ) + + logger.info( + listOf( + ArmorStandModule, + BooksModule, + ChatModule, + DimensionsModule, + EntityModule, + InventoryModule, + LocatorModule, + MotdModule, + OpenableModule, + PlayerModule, + ScoreBoardModule, + SignModule, + SitModule, + TabListModule, + TameableModule, + ), + ) } /** diff --git a/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt b/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt index e9a8b8a16..b6daf0277 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable import net.kyori.adventure.inventory.Book import org.bukkit.permissions.PermissionDefault import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.utils.ExtUtils.mm +import org.xodium.vanillaplus.utils.Utils.MM /** * Represents the data structure for a book in the game. @@ -26,5 +26,6 @@ internal data class BookData( * Converts this [BookData] instance to a [Book] instance. * @return A [Book] instance with the properties of this [BookData]. */ - fun toBook(): Book = Book.book(title.mm(), author.mm(), pages.map { it.joinToString("\n").mm() }) + fun toBook(): Book = + Book.book(MM.deserialize(title), MM.deserialize(author), pages.map { MM.deserialize(it.joinToString("\n")) }) } diff --git a/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt b/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt index 2a08ccd86..24f46ba38 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/data/ConfigData.kt @@ -1,27 +1,103 @@ @file:Suppress("ktlint:standard:no-wildcard-imports") +@file:OptIn(ExperimentalSerializationApi::class) package org.xodium.vanillaplus.data +import io.papermc.paper.command.brigadier.Commands +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.bukkit.entity.Player +import org.bukkit.permissions.Permission +import org.bukkit.permissions.PermissionDefault +import org.xodium.vanillaplus.VanillaPlus.Companion.configData +import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.modules.* +import org.xodium.vanillaplus.strategies.CapitalizedStrategy +import org.xodium.vanillaplus.utils.CommandUtils.executesCatching +import org.xodium.vanillaplus.utils.Utils.MM +import org.xodium.vanillaplus.utils.Utils.prefix +import kotlin.io.path.* +import kotlin.time.measureTime /** Configuration data for the plugin. */ @Serializable internal data class ConfigData( - var arrowModule: ArrowModule.Config = ArrowModule.Config(), + var armorStandModule: ArmorStandModule.Config = ArmorStandModule.Config(), var booksModule: BooksModule.Config = BooksModule.Config(), var chatModule: ChatModule.Config = ChatModule.Config(), var dimensionsModule: DimensionsModule.Config = DimensionsModule.Config(), var entityModule: EntityModule.Config = EntityModule.Config(), - var invModule: InvModule.Config = InvModule.Config(), + var inventoryModule: InventoryModule.Config = InventoryModule.Config(), var locatorModule: LocatorModule.Config = LocatorModule.Config(), var motdModule: MotdModule.Config = MotdModule.Config(), var openableModule: OpenableModule.Config = OpenableModule.Config(), - var petModule: PetModule.Config = PetModule.Config(), var playerModule: PlayerModule.Config = PlayerModule.Config(), var scoreboardModule: ScoreBoardModule.Config = ScoreBoardModule.Config(), var signModule: SignModule.Config = SignModule.Config(), var sitModule: SitModule.Config = SitModule.Config(), var tabListModule: TabListModule.Config = TabListModule.Config(), - var treesModule: TreesModule.Config = TreesModule.Config(), -) + var tameableModule: TameableModule.Config = TameableModule.Config(), +) { + companion object { + private val json = + Json { + prettyPrint = true + encodeDefaults = true + ignoreUnknownKeys = true + namingStrategy = CapitalizedStrategy + } + + val reloadCommand: CommandData = + CommandData( + Commands + .literal("vanillaplus") + .requires { it.sender.hasPermission(reloadPermission) } + .then( + Commands + .literal("reload") + .executesCatching { + configData = ConfigData().load("config.json") + + if (it.source.sender is Player) { + it.source.sender.sendMessage( + MM.deserialize("${instance.prefix} configuration reloaded!"), + ) + } else { + instance.logger.info("Configuration reloaded!") + } + }, + ), + "Allows to plugin specific admin commands", + listOf("vp"), + ) + + val reloadPermission: Permission = + Permission( + "${instance.javaClass.simpleName}.reload".lowercase(), + "Allows use of the reload command", + PermissionDefault.OP, + ) + + /** + * Loads or creates the configuration file. + * @param fileName The name of the configuration file. + * @return The loaded configuration data. + */ + inline fun T.load(fileName: String): T { + val file = instance.dataFolder.toPath() / fileName + + if (!instance.dataFolder.toPath().exists()) instance.dataFolder.toPath().createDirectories() + + val loadedConfig = if (file.exists()) json.decodeFromString(file.readText()) else this + + instance.logger.info( + "${if (file.exists()) "Loaded configuration from $fileName" else "Created default $fileName"} | Took ${ + measureTime { file.writeText(json.encodeToString(loadedConfig)) }.inWholeMilliseconds + }ms", + ) + + return loadedConfig + } + } +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/enchantments/FeatherFallingEnchantment.kt b/src/main/kotlin/org/xodium/vanillaplus/enchantments/FeatherFallingEnchantment.kt index d190e349b..1026e1307 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/enchantments/FeatherFallingEnchantment.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/enchantments/FeatherFallingEnchantment.kt @@ -15,11 +15,12 @@ internal object FeatherFallingEnchantment : EnchantmentInterface { * @param event The PlayerInteractEvent to handle. */ fun featherFalling(event: PlayerInteractEvent) { - if (event.action != Action.PHYSICAL) return - if (event.clickedBlock?.type != Material.FARMLAND) return - if (!isValidTool(event.player.inventory.boots)) return - - event.isCancelled = true + when { + event.action != Action.PHYSICAL -> return + event.clickedBlock?.type != Material.FARMLAND -> return + !isValidTool(event.player.inventory.boots) -> return + else -> event.isCancelled = true + } } /** @@ -28,5 +29,8 @@ internal object FeatherFallingEnchantment : EnchantmentInterface { * @return `true` if the item is a pickaxe with Silk Touch, otherwise `false`. */ private fun isValidTool(item: ItemStack?): Boolean = - item?.let { Tag.ITEMS_FOOT_ARMOR.isTagged(it.type) && it.containsEnchantment(Enchantment.FEATHER_FALLING) } == true + item?.let { + Tag.ITEMS_FOOT_ARMOR.isTagged(it.type) && + it.containsEnchantment(Enchantment.FEATHER_FALLING) + } == true } diff --git a/src/main/kotlin/org/xodium/vanillaplus/enchantments/NightVisionEnchantment.kt b/src/main/kotlin/org/xodium/vanillaplus/enchantments/NightVisionEnchantment.kt index 7ab7329f8..00a155ea2 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/enchantments/NightVisionEnchantment.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/enchantments/NightVisionEnchantment.kt @@ -7,7 +7,7 @@ import org.bukkit.inventory.EquipmentSlotGroup import org.bukkit.potion.PotionEffect import org.bukkit.potion.PotionEffectType import org.xodium.vanillaplus.interfaces.EnchantmentInterface -import org.xodium.vanillaplus.utils.ExtUtils.displayName +import org.xodium.vanillaplus.utils.Utils.displayName /** Represents an object handling night vision enchantment implementation within the system. */ @Suppress("UnstableApiUsage") diff --git a/src/main/kotlin/org/xodium/vanillaplus/enchantments/NimbusEnchantment.kt b/src/main/kotlin/org/xodium/vanillaplus/enchantments/NimbusEnchantment.kt index 81519b856..56ae2da59 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/enchantments/NimbusEnchantment.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/enchantments/NimbusEnchantment.kt @@ -7,7 +7,7 @@ import org.bukkit.entity.HappyGhast import org.bukkit.inventory.EquipmentSlot import org.bukkit.inventory.EquipmentSlotGroup import org.xodium.vanillaplus.interfaces.EnchantmentInterface -import org.xodium.vanillaplus.utils.ExtUtils.displayName +import org.xodium.vanillaplus.utils.Utils.displayName /** Represents an object handling nimbus enchantment implementation within the system. */ @Suppress("UnstableApiUsage") diff --git a/src/main/kotlin/org/xodium/vanillaplus/enchantments/PickupEnchantment.kt b/src/main/kotlin/org/xodium/vanillaplus/enchantments/PickupEnchantment.kt index 46ad2b6d1..e2270b929 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/enchantments/PickupEnchantment.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/enchantments/PickupEnchantment.kt @@ -6,7 +6,7 @@ import org.bukkit.entity.Player import org.bukkit.event.block.BlockDropItemEvent import org.bukkit.inventory.EquipmentSlotGroup import org.xodium.vanillaplus.interfaces.EnchantmentInterface -import org.xodium.vanillaplus.utils.ExtUtils.displayName +import org.xodium.vanillaplus.utils.Utils.displayName /** Represents an object handling pickup enchantment implementation within the system. */ @Suppress("UnstableApiUsage") diff --git a/src/main/kotlin/org/xodium/vanillaplus/enchantments/ReplantEnchantment.kt b/src/main/kotlin/org/xodium/vanillaplus/enchantments/ReplantEnchantment.kt index 26b1a337a..2f59be023 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/enchantments/ReplantEnchantment.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/enchantments/ReplantEnchantment.kt @@ -6,7 +6,7 @@ import org.bukkit.event.block.BlockBreakEvent import org.bukkit.inventory.EquipmentSlotGroup import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.interfaces.EnchantmentInterface -import org.xodium.vanillaplus.utils.ExtUtils.displayName +import org.xodium.vanillaplus.utils.Utils.displayName /** Represents an object handling replant enchantment implementation within the system. */ @Suppress("UnstableApiUsage") @@ -33,16 +33,9 @@ internal object ReplantEnchantment : EnchantmentInterface { if (ageable.age < ageable.maximumAge) return if (!itemInHand.hasItemMeta() || !itemInHand.itemMeta.hasEnchant(get())) return - // TODO: take seed out of drop. since planting would require a seed being used. - instance.server.scheduler.runTaskLater( instance, - Runnable { - val blockType = block.type - - block.type = blockType - block.blockData = ageable.apply { age = 0 } - }, + Runnable { block.blockData = ageable.apply { age = 0 } }, 2, ) } diff --git a/src/main/kotlin/org/xodium/vanillaplus/enchantments/VeinMineEnchantment.kt b/src/main/kotlin/org/xodium/vanillaplus/enchantments/VeinMineEnchantment.kt index 3d5a78de1..f3265f203 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/enchantments/VeinMineEnchantment.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/enchantments/VeinMineEnchantment.kt @@ -10,7 +10,7 @@ import org.bukkit.event.block.BlockBreakEvent import org.bukkit.inventory.EquipmentSlotGroup import org.bukkit.inventory.meta.Damageable import org.xodium.vanillaplus.interfaces.EnchantmentInterface -import org.xodium.vanillaplus.utils.ExtUtils.displayName +import org.xodium.vanillaplus.utils.Utils.displayName /** Represents an object handling vein mine enchantment implementation within the system. */ @Suppress("UnstableApiUsage") diff --git a/src/main/kotlin/org/xodium/vanillaplus/hooks/WorldEditHook.kt b/src/main/kotlin/org/xodium/vanillaplus/hooks/WorldEditHook.kt deleted file mode 100644 index 1cf8ec50b..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/hooks/WorldEditHook.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.xodium.vanillaplus.hooks - -import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.modules.TreesModule - -/** A utility object for checking plugin availability and handling related dependencies. */ -internal object WorldEditHook { - /** - * Checks if a specified plugin is available and optionally logs a warning if not found. - * @return true if the plugin is installed and enabled, false otherwise. - */ - fun get(): Boolean { - val plugin = instance.server.pluginManager.getPlugin("WorldEdit") != null - - if (!plugin) { - instance.logger.warning("FAWE or WorldEdit not found, disabling ${TreesModule.javaClass.simpleName}") - } - - return plugin - } -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/interfaces/EnchantmentInterface.kt b/src/main/kotlin/org/xodium/vanillaplus/interfaces/EnchantmentInterface.kt index df4cd5a76..62d100f67 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/interfaces/EnchantmentInterface.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/interfaces/EnchantmentInterface.kt @@ -17,8 +17,7 @@ internal interface EnchantmentInterface { * Retrieves the configuration data associated with the module. * @return A [ConfigData] object representing the configuration for the module. */ - val config: ConfigData - get() = configData + val config: ConfigData get() = configData /** * The unique typed key identifies this enchantment in the registry. diff --git a/src/main/kotlin/org/xodium/vanillaplus/interfaces/ModuleInterface.kt b/src/main/kotlin/org/xodium/vanillaplus/interfaces/ModuleInterface.kt index 26116b617..94c494ea3 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/interfaces/ModuleInterface.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/interfaces/ModuleInterface.kt @@ -7,6 +7,8 @@ import org.xodium.vanillaplus.VanillaPlus.Companion.configData import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.data.ConfigData +import java.util.logging.Logger +import kotlin.reflect.full.memberProperties import kotlin.time.measureTime /** Represents a contract for a module within the system. */ @@ -15,42 +17,68 @@ internal interface ModuleInterface : Listener { * Retrieves the configuration data associated with the module. * @return A [ConfigData] object representing the configuration for the module. */ - val config: ConfigData - get() = configData + val config: ConfigData get() = configData + + /** + * Determines if this module is enabled. + * @return True if the module is enabled, false otherwise. + */ + val isEnabled: Boolean + get() = + configData::class + .memberProperties + .firstOrNull { property -> + property.name == (this::class.simpleName?.replaceFirstChar { it.lowercase() } ?: return true) + }?.call(configData) + ?.let { moduleConfig -> + moduleConfig::class + .memberProperties + .firstOrNull { it.name == "enabled" } + ?.call(moduleConfig) as? Boolean + } + ?: true /** * Retrieves a list of command data associated with the module. * @return A [Collection] of [CommandData] objects representing the commands for the module. */ - val cmds: Collection - get() = emptyList() + val cmds: Collection get() = emptyList() /** * Retrieves a list of permissions associated with this module. * @return A [List] of [Permission] objects representing the permissions for this module. */ - val perms: List - get() = emptyList() + val perms: List get() = emptyList() - /** Registers this feature as an event listener with the server. */ + /** + * Registers this feature as an event listener with the server. + * @return The time taken to register the feature in milliseconds, or null if the feature is disabled. + */ @Suppress("UnstableApiUsage") - fun register() { - instance.logger.info( - "Registering: ${this::class.simpleName} | Took ${ - measureTime { - instance.server.pluginManager.registerEvents(this, instance) - instance.lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> - cmds.forEach { cmd -> - event.registrar().register( - cmd.builder.build(), - cmd.description, - cmd.aliases, - ) - } - } - instance.server.pluginManager.addPermissions(perms) - }.inWholeMilliseconds - }ms", - ) + fun register(): Long? { + if (!isEnabled) return null + + return measureTime { + instance.server.pluginManager.registerEvents(this, instance) + instance.lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> + cmds.forEach { cmd -> + event.registrar().register( + cmd.builder.build(), + cmd.description, + cmd.aliases, + ) + } + } + instance.server.pluginManager.addPermissions(perms) + }.inWholeMilliseconds + } + + /** + * Logs the registration details of a list of modules. + * @receiver Logger The logger to use for logging. + * @param modules List of [ModuleInterface] instances to log. + */ + fun Logger.info(modules: List) { + info("Registered: ${modules.size} module(s) | Took ${modules.sumOf { it.register() ?: return }}ms") } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/interfaces/RecipeInterface.kt b/src/main/kotlin/org/xodium/vanillaplus/interfaces/RecipeInterface.kt index 6c56f67ac..230f284ba 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/interfaces/RecipeInterface.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/interfaces/RecipeInterface.kt @@ -2,6 +2,7 @@ package org.xodium.vanillaplus.interfaces import org.bukkit.inventory.Recipe import org.xodium.vanillaplus.VanillaPlus.Companion.instance +import java.util.logging.Logger import kotlin.time.measureTime /** Represents a contract for recipes within the system. */ @@ -12,12 +13,18 @@ internal interface RecipeInterface { */ val recipes: Collection - /** Registers all recipes returned by [recipes] with the server. */ - fun register() { - instance.logger.info( - "Registering: ${this::class.simpleName} | Took ${ - measureTime { recipes.forEach { recipe -> instance.server.addRecipe(recipe) } }.inWholeMilliseconds - }ms", - ) + /** + * Registers all recipes returned by [recipes] with the server. + * @return The time taken to register the recipes in milliseconds. + */ + fun register(): Long = measureTime { recipes.forEach { instance.server.addRecipe(it) } }.inWholeMilliseconds + + /** + * Logs the registration details of a list of recipes. + * @receiver Logger The logger to use for logging. + * @param recipes List of [RecipeInterface] instances to log. + */ + fun Logger.info(recipes: List) { + info("Registered: ${recipes.sumOf { it.recipes.size }} recipes(s) | Took ${recipes.sumOf { it.register() }}ms") } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt b/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt deleted file mode 100644 index dbb03a4c4..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt +++ /dev/null @@ -1,85 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package org.xodium.vanillaplus.managers - -import io.papermc.paper.command.brigadier.Commands -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import org.bukkit.entity.Player -import org.bukkit.permissions.Permission -import org.bukkit.permissions.PermissionDefault -import org.xodium.vanillaplus.VanillaPlus.Companion.configData -import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.data.CommandData -import org.xodium.vanillaplus.data.ConfigData -import org.xodium.vanillaplus.strategies.CapitalizedStrategy -import org.xodium.vanillaplus.utils.CommandUtils.executesCatching -import org.xodium.vanillaplus.utils.ExtUtils.mm -import org.xodium.vanillaplus.utils.ExtUtils.prefix -import java.io.File -import kotlin.time.measureTime - -/** Manages loading and saving the configuration file. */ -internal object ConfigManager { - private val json = - Json { - prettyPrint = true - encodeDefaults = true - ignoreUnknownKeys = true - namingStrategy = CapitalizedStrategy - } - - val reloadCommand: CommandData = - CommandData( - Commands - .literal("vanillaplus") - .requires { it.sender.hasPermission(reloadPermission) } - .then( - Commands - .literal("reload") - .executesCatching { - if (it.source.sender !is Player) instance.logger.warning("Command can only be executed by a Player!") - configData = load() - it.source.sender.sendMessage("${instance.prefix} configuration reloaded!".mm()) - }, - ), - "Allows to plugin specific admin commands", - listOf("vp"), - ) - - val reloadPermission: Permission = - Permission( - "${instance.javaClass.simpleName}.reload".lowercase(), - "Allows use of the reload command", - PermissionDefault.OP, - ) - - /** - * Loads or creates the configuration file. - * @param fileName The name of the configuration file. - * @return The loaded configuration data. - */ - fun load(fileName: String = "config.json"): ConfigData { - val file = File(instance.dataFolder, fileName) - - if (!instance.dataFolder.exists()) instance.dataFolder.mkdirs() - - val config = getOrCreateConfig(file) - - instance.logger.info( - "${if (file.exists()) "Loaded configuration from $fileName" else "Created default $fileName"} | Took ${ - measureTime { file.writeText(json.encodeToString(ConfigData.serializer(), config)) }.inWholeMilliseconds - }ms", - ) - - return config - } - - /** - * Gets the existing configuration or creates a default one. - * @param file The configuration file. - * @return The configuration data. - */ - private fun getOrCreateConfig(file: File): ConfigData = - if (file.exists()) json.decodeFromString(ConfigData.serializer(), file.readText()) else ConfigData() -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/managers/PlayerMessageManager.kt b/src/main/kotlin/org/xodium/vanillaplus/managers/PlayerMessageManager.kt new file mode 100644 index 000000000..2bbc5db29 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/managers/PlayerMessageManager.kt @@ -0,0 +1,94 @@ +package org.xodium.vanillaplus.managers + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.bukkit.advancement.Advancement +import org.bukkit.entity.Player +import org.xodium.vanillaplus.modules.PlayerModule +import org.xodium.vanillaplus.utils.Utils.MM + +/** Manages player messages and internationalization. */ +internal object PlayerMessageManager { + private val config = PlayerModule.config.playerModule.i18n + + /** + * Handles the player join message. + * @param player The player who joined. + * @return The formatted join message component, or null if no message is set. + */ + fun handleJoin(player: Player): Component? { + if (config.playerJoinMsg.isEmpty()) return null + + return MM.deserialize(config.playerJoinMsg, Placeholder.component("player", player.displayName())) + } + + /** + * Handles the player leave message. + * @param player The player who left. + * @return The formatted leave message component, or null if no message is set. + */ + fun handleQuit(player: Player): Component? { + if (config.playerQuitMsg.isEmpty()) return null + + return MM.deserialize(config.playerQuitMsg, Placeholder.component("player", player.displayName())) + } + + /** + * Handles the player death message. + * @param player The player who died. + * @param killer The player who killed them. + * @return The formatted death message component, or null if no message is set. + */ + fun handleDeath( + player: Player, + killer: Player?, + ): Component? { + if (config.playerDeathByPlayerMsg.isEmpty() || killer == null) return null + + return MM.deserialize( + config.playerDeathByPlayerMsg, + Placeholder.component("player", player.displayName()), + Placeholder.component("killer", killer.displayName()), + ) + } + + /** + * Handles the player death screen message. + * @return The formatted death screen message component, or null if no message is set. + */ + fun handleDeathScreen(): Component? { + if (config.playerDeathScreenMsg.isEmpty()) return null + + return MM.deserialize(config.playerDeathScreenMsg) + } + + /** + * Handles the player advancement completion message. + * @param player The player who completed the advancement. + * @param advancement The advancement that was completed. + * @return The formatted advancement completion message component, or null if no message is set. + */ + fun handleAdvancement( + player: Player, + advancement: Advancement, + ): Component? { + if (config.playerAdvancementDoneMsg.isEmpty()) return null + + return MM.deserialize( + config.playerAdvancementDoneMsg, + Placeholder.component("player", player.displayName()), + Placeholder.component("advancement", advancement.displayName()), + ) + } + + /** + * Handles the player kick message. + * @param reason The reason for the kick. + * @return The formatted kick message component, or null if no message is set. + */ + fun handleKick(reason: Component): Component? { + if (config.playerKickMsg.isEmpty()) return null + + return MM.deserialize(config.playerKickMsg, Placeholder.component("reason", reason)) + } +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ArmorStandModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ArmorStandModule.kt new file mode 100644 index 000000000..35ec722d2 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ArmorStandModule.kt @@ -0,0 +1,289 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + +package org.xodium.vanillaplus.modules + +import io.papermc.paper.datacomponent.DataComponentTypes +import io.papermc.paper.datacomponent.item.TooltipDisplay +import kotlinx.serialization.Serializable +import org.bukkit.Material +import org.bukkit.entity.ArmorStand +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.event.player.PlayerInteractAtEntityEvent +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.MenuType +import org.xodium.vanillaplus.interfaces.ModuleInterface +import org.xodium.vanillaplus.utils.Utils.MM +import java.util.* + +/** Represents a module handling armor stand mechanics within the system. */ +internal object ArmorStandModule : ModuleInterface { + private val armorStandViews = WeakHashMap() + + // Inventory slot constants for armorstand equipment + private const val ARMOR_STAND_MAIN_HAND_SLOT = 0 + private const val ARMOR_STAND_OFF_HAND_SLOT = 1 + + // Inventory slot constants for armorstand properties + private const val ARMOR_STAND_NAME_TAG_ITEM_SLOT = 3 + private const val ARMOR_STAND_ARMS_ITEM_SLOT = 4 + private const val ARMOR_STAND_SMALL_ITEM_SLOT = 5 + private const val ARMOR_STAND_BASEPLATE_ITEM_SLOT = 6 + private const val ARMOR_STAND_MORE_OPTIONS_SLOT = 8 + + @EventHandler + private fun on(event: PlayerInteractAtEntityEvent) = handleArmorStandMenu(event) + + @EventHandler + fun on(event: InventoryClickEvent) = handleArmorStandMenuClicking(event) + + @EventHandler + fun on(event: InventoryCloseEvent) = handleArmorStandCleanup(event) + + /** + * Handles the interaction with an ArmorStand's inventory. + * @param event EntityInteractEvent The event triggered by the interaction. + */ + private fun handleArmorStandMenu(event: PlayerInteractAtEntityEvent) { + val armorStand = event.rightClicked as? ArmorStand ?: return + + if (!event.player.isSneaking) return + + event.isCancelled = true + + val view = armorStand.menu(event.player) + + armorStandViews[view] = armorStand + view.open() + } + + /** + * Handles the interaction with ArmorStand slots in the inventory. + * @param event InventoryClickEvent The event triggered by the inventory click. + */ + private fun handleArmorStandMenuClicking(event: InventoryClickEvent) { + val armorStand = armorStandViews[event.view] ?: return + + if (event.click.isShiftClick) event.isCancelled = true // TODO: Handle shift-clicking better in v2. + if (event.clickedInventory != event.view.topInventory) return + + when (event.slot) { + // Equipment Slots + ARMOR_STAND_MAIN_HAND_SLOT -> { + armorStand.equipment.setItemInMainHand(event.cursor) + } + + ARMOR_STAND_OFF_HAND_SLOT -> { + armorStand.equipment.setItemInOffHand(event.cursor) + } + + // Properties Slots + ARMOR_STAND_NAME_TAG_ITEM_SLOT -> { + event.isCancelled = true + toggleArmorStandProperty( + armorStand, + event.inventory, + ARMOR_STAND_NAME_TAG_ITEM_SLOT, + { stand -> stand.isCustomNameVisible }, + { stand, value -> stand.isCustomNameVisible = value }, + ) + } + + ARMOR_STAND_ARMS_ITEM_SLOT -> { + event.isCancelled = true + toggleArmorStandProperty( + armorStand, + event.inventory, + ARMOR_STAND_ARMS_ITEM_SLOT, + { stand -> stand.hasArms() }, + { stand, value -> stand.setArms(value) }, + ) + } + + ARMOR_STAND_SMALL_ITEM_SLOT -> { + event.isCancelled = true + toggleArmorStandProperty( + armorStand, + event.inventory, + ARMOR_STAND_SMALL_ITEM_SLOT, + { stand -> stand.isSmall }, + { stand, value -> stand.isSmall = value }, + ) + } + + ARMOR_STAND_BASEPLATE_ITEM_SLOT -> { + event.isCancelled = true + toggleArmorStandProperty( + armorStand, + event.inventory, + ARMOR_STAND_BASEPLATE_ITEM_SLOT, + { stand -> stand.hasBasePlate() }, + { stand, value -> stand.setBasePlate(value) }, + ) + } + + else -> { + event.isCancelled = true + } + } + } + + /** + * Cleans up the armor stand view mapping when the inventory is closed. + * @param event InventoryCloseEvent The event triggered by the inventory close. + */ + private fun handleArmorStandCleanup(event: InventoryCloseEvent) { + armorStandViews.remove(event.view) + } + + /** + * Toggles a boolean property of the [ArmorStand] and updates the corresponding inventory item. + * @param armorStand [ArmorStand] The ArmorStand whose property is to be toggled. + * @param inventory [Inventory] The inventory where the toggle item is located. + * @param slot Int The slot index of the toggle item in the inventory. + * @param getCurrentState Function to get the current state of the property. + * @param setState Function to set the new state of the property. + */ + @Suppress("UnstableApiUsage") + private fun toggleArmorStandProperty( + armorStand: ArmorStand, + inventory: Inventory, + slot: Int, + getCurrentState: (ArmorStand) -> Boolean, + setState: (ArmorStand, Boolean) -> Unit, + ) { + val newState = !getCurrentState(armorStand) + val currentItem = inventory.getItem(slot) ?: return + val displayName = currentItem.getData(DataComponentTypes.ITEM_NAME) + + setState(armorStand, newState) + inventory.setItem( + slot, + ItemStack.of(if (newState) Material.GREEN_WOOL else Material.RED_WOOL).apply { + displayName?.let { setData(DataComponentTypes.ITEM_NAME, it) } + }, + ) + } + + /** + * Creates a toggle item with the specified material and display name. + * @param material [Material] The material of the item. + * @param displayName String The display name of the item. + * @return [ItemStack] The created toggle item. + */ + @Suppress("UnstableApiUsage") + private fun createToggleItem( + material: Material, + displayName: String, + ): ItemStack = ItemStack.of(material).apply { setData(DataComponentTypes.ITEM_NAME, MM.deserialize(displayName)) } + + /** + * Creates a menu for the given ArmorStand and Player. + * @receiver ArmorStand The ArmorStand for which the menu is created. + * @param player Player The player for whom the menu is created. + * @return InventoryView The created menu view. + */ + @Suppress("UnstableApiUsage") + private fun ArmorStand.menu(player: Player): InventoryView = + MenuType + .GENERIC_9X1 + .builder() + .title(customName() ?: MM.deserialize(name)) + .build(player) + .apply { + topInventory + .apply { + fill() + + // Equipment Slots + setItem(ARMOR_STAND_MAIN_HAND_SLOT, equipment.itemInMainHand) + setItem(ARMOR_STAND_OFF_HAND_SLOT, equipment.itemInOffHand) + + // Property Slots + setItem( + ARMOR_STAND_NAME_TAG_ITEM_SLOT, + createToggleItem( + if (isCustomNameVisible) Material.GREEN_WOOL else Material.RED_WOOL, + config.armorStandModule.i18n.toggleNameTagVisibility, + ), + ) + setItem( + ARMOR_STAND_ARMS_ITEM_SLOT, + createToggleItem( + if (hasArms()) Material.GREEN_WOOL else Material.RED_WOOL, + config.armorStandModule.i18n.toggleArmsVisibility, + ), + ) + setItem( + ARMOR_STAND_SMALL_ITEM_SLOT, + createToggleItem( + if (isSmall) Material.GREEN_WOOL else Material.RED_WOOL, + config.armorStandModule.i18n.toggleSmallArmorStand, + ), + ) + setItem( + ARMOR_STAND_BASEPLATE_ITEM_SLOT, + createToggleItem( + if (hasBasePlate()) Material.GREEN_WOOL else Material.RED_WOOL, + config.armorStandModule.i18n.toggleBasePlateVisibility, + ), + ) + setItem( + ARMOR_STAND_MORE_OPTIONS_SLOT, + ItemStack.of(Material.ARMOR_STAND).apply { + setData( + DataComponentTypes.CUSTOM_NAME, + MM.deserialize(config.armorStandModule.i18n.moreOptionsComingSoon), + ) + }, + ) + } + } + + /** + * Fills the inventory with the configured fill item. + * @receiver Inventory The inventory to be filled. + */ + private fun Inventory.fill() { + for (i in 0 until size) { + setItem( + i, + @Suppress("UnstableApiUsage") + ItemStack.of(config.armorStandModule.menuFillItemMaterial).apply { + setData( + DataComponentTypes.TOOLTIP_DISPLAY, + TooltipDisplay.tooltipDisplay().hideTooltip(config.armorStandModule.menuFIllItemTooltip), + ) + }, + ) + } + } + + /** Represents the config of the module. */ + @Serializable + data class Config( + var enabled: Boolean = true, + var menuFillItemMaterial: Material = Material.BLACK_STAINED_GLASS_PANE, + var menuFIllItemTooltip: Boolean = true, + var i18n: I18n = I18n(), + ) { + /** Represents the internationalization settings of the module. */ + @Serializable + data class I18n( + var toggleNameTagVisibility: String = + "Toggle Name Tag Visibility", + var toggleArmsVisibility: String = + "Toggle Arms Visibility", + var toggleSmallArmorStand: String = + "Toggle Small ArmorStand", + var toggleBasePlateVisibility: String = + "Toggle Base Plate Visibility", + var moreOptionsComingSoon: String = + "More Options Coming Soon!", + ) + } +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ArrowModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ArrowModule.kt deleted file mode 100644 index a45ef72cf..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ArrowModule.kt +++ /dev/null @@ -1,133 +0,0 @@ -package org.xodium.vanillaplus.modules - -import kotlinx.serialization.Serializable -import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.block.BlockFace -import org.bukkit.block.data.Directional -import org.bukkit.entity.Arrow -import org.bukkit.event.EventHandler -import org.bukkit.event.EventPriority -import org.bukkit.event.entity.EntityDamageByEntityEvent -import org.bukkit.event.entity.ProjectileHitEvent -import org.bukkit.event.entity.ProjectileLaunchEvent -import org.bukkit.persistence.PersistentDataType -import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.recipes.TorchArrowRecipe - -/** Represents a module handling custom arrow mechanics within the system. */ -internal object ArrowModule : ModuleInterface { - @EventHandler - fun on(event: ProjectileLaunchEvent) = handleProjectileLaunch(event) - - @EventHandler - fun on(event: ProjectileHitEvent) = handleProjectileHit(event) - - @EventHandler(priority = EventPriority.HIGHEST) - fun on(event: EntityDamageByEntityEvent) = handleEntityDamage(event) - - /** - * Applies visual tipped-arrow particles to torch arrows. - * @param event The projectile launch event. - */ - private fun handleProjectileLaunch(event: ProjectileLaunchEvent) { - val arrow = event.entity as? Arrow ?: return - val torchTypeId = arrow.torchArrowType ?: return - val torchType = TorchArrowRecipe.getTorchArrowTypeById(torchTypeId) ?: return - - arrow.color = torchType.arrowColor - } - - /** - * Handles the logic for torch arrows when they hit a target. - * @param event The projectile hit event to process. - */ - private fun handleProjectileHit(event: ProjectileHitEvent) { - val arrow = event.entity as? Arrow ?: return - val torchTypeId = arrow.torchArrowType ?: return - val torchType = TorchArrowRecipe.getTorchArrowTypeById(torchTypeId) ?: return - val hitBlock = event.hitBlock - val hitFace = event.hitBlockFace - - if (hitBlock == null || hitFace == null) { - dropArrow(arrow, arrow.location.block.location) - return - } - - val target = hitBlock.getRelative(hitFace) - - if (hitBlock.type == Material.VINE) { - hitBlock.breakNaturally() - target.type = torchType.torchMaterial - arrow.remove() - return - } - - when (hitFace) { - BlockFace.UP -> { - if (target.type == Material.AIR) { - target.type = torchType.torchMaterial - } else { - dropArrow(arrow, target.location) - } - } - - BlockFace.DOWN -> { - dropArrow(arrow, target.location) - } - - else -> { - if (target.type != Material.AIR) { - dropArrow(arrow, target.location) - return - } - - target.type = torchType.wallTorchMaterial - - val data = target.blockData as? Directional - - data?.facing = hitFace - data?.let { target.blockData = it } - } - } - - arrow.remove() - } - - /** - * Handles the logic for torch arrows when they attempt to deal damage. - * @param event The entity damage event to process. - */ - private fun handleEntityDamage(event: EntityDamageByEntityEvent) { - val arrow = event.damager as? Arrow ?: return - - if (arrow.torchArrowType == null) return - - arrow.remove() - event.isCancelled = true - } - - /** - * Drops a torch arrow item at the specified location. - * @param location The location where the torch arrow should be dropped. - * @param arrow The arrow to be dropped. - */ - private fun dropArrow( - arrow: Arrow, - location: Location, - ) { - location.world.dropItemNaturally(location, arrow.itemStack) - } - - /** - * Checks whether this arrow is a torch arrow based on its ItemStack metadata. - * @return True if the arrow is a torch arrow, false otherwise. - */ - private val Arrow.torchArrowType: String? - get() = itemStack.persistentDataContainer.get(TorchArrowRecipe.torchArrowKey, PersistentDataType.STRING) - - @Serializable - data class Config( - var enabled: Boolean = true, - ) -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt index 410065fd3..f76027392 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt @@ -33,6 +33,7 @@ internal object BooksModule : ModuleInterface { ) } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt index 2813a9e45..a5b295c10 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt @@ -4,6 +4,8 @@ import com.destroystokyo.paper.event.player.PlayerSetSpawnEvent import com.mojang.brigadier.arguments.StringArgumentType import io.papermc.paper.chat.ChatRenderer import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.command.brigadier.argument.ArgumentTypes +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver import io.papermc.paper.event.player.AsyncChatEvent import kotlinx.serialization.Serializable import net.kyori.adventure.chat.SignedMessage @@ -21,13 +23,9 @@ import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.utils.CommandUtils.executesCatching -import org.xodium.vanillaplus.utils.ExtUtils.clickOpenUrl -import org.xodium.vanillaplus.utils.ExtUtils.clickRunCmd -import org.xodium.vanillaplus.utils.ExtUtils.clickSuggestCmd -import org.xodium.vanillaplus.utils.ExtUtils.face -import org.xodium.vanillaplus.utils.ExtUtils.mm -import org.xodium.vanillaplus.utils.ExtUtils.prefix -import java.util.concurrent.CompletableFuture +import org.xodium.vanillaplus.utils.PlayerUtils.face +import org.xodium.vanillaplus.utils.Utils.MM +import org.xodium.vanillaplus.utils.Utils.prefix /** Represents a module handling chat mechanics within the system. */ internal object ChatModule : ModuleInterface { @@ -39,14 +37,8 @@ internal object ChatModule : ModuleInterface { .requires { it.sender.hasPermission(perms[0]) } .then( Commands - .argument("target", StringArgumentType.string()) - .suggests { _, builder -> - instance.server.onlinePlayers - .map { it.name } - .filter { it.lowercase().startsWith(builder.remaining.lowercase()) } - .forEach(builder::suggest) - CompletableFuture.completedFuture(builder.build()) - }.then( + .argument("target", ArgumentTypes.player()) + .then( Commands .argument("message", StringArgumentType.greedyString()) .executesCatching { @@ -57,13 +49,12 @@ internal object ChatModule : ModuleInterface { } val sender = it.source.sender as Player - val targetName = it.getArgument("target", String().javaClass) + val targetResolver = + it.getArgument("target", PlayerSelectorArgumentResolver::class.java) val target = - instance.server - .getPlayer(targetName) + targetResolver.resolve(it.source).singleOrNull() ?: return@executesCatching sender.sendMessage( - config.chatModule.i18n.playerIsNotOnline - .mm(), + MM.deserialize(config.chatModule.i18n.playerIsNotOnline), ) val message = it.getArgument("message", String().javaClass) @@ -86,58 +77,72 @@ internal object ChatModule : ModuleInterface { ) @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - fun on(event: AsyncChatEvent) { + fun on(event: AsyncChatEvent) = asyncChat(event) + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun on(event: PlayerJoinEvent) = playerJoin(event) + + @EventHandler + fun on(event: PlayerSetSpawnEvent) = playerSetSpawn(event) + + /** + * Handles asynchronous chat events. + * @param event The [AsyncChatEvent] to be processed. + */ + private fun asyncChat(event: AsyncChatEvent) { event.renderer(ChatRenderer.defaultRenderer()) event.renderer { player, displayName, message, audience -> var base = - config.chatModule.chatFormat.mm( - Placeholder.component("player_head", "".mm()), + MM.deserialize( + config.chatModule.chatFormat, + Placeholder.component("player_head", MM.deserialize("")), Placeholder.component( "player", displayName .clickEvent(ClickEvent.suggestCommand("/w ${player.name} ")) .hoverEvent( - HoverEvent.showText( - config.chatModule.i18n.clickToWhisper - .mm(), - ), + HoverEvent.showText(MM.deserialize(config.chatModule.i18n.clickToWhisper)), ), ), Placeholder.component("message", message), ) + if (audience == player) base = base.appendSpace().append(createDeleteCross(event.signedMessage())) base } } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - fun on(event: PlayerJoinEvent) { + /** + * Handles player join events. + * @param event The [PlayerJoinEvent] to be processed. + */ + private fun playerJoin(event: PlayerJoinEvent) { val player = event.player var imageIndex = 0 player.sendMessage( - Regex("") - .replace(config.chatModule.welcomeText.joinToString("\n")) { "" } - .mm( - Placeholder.component("player", player.displayName()), - *player - .face() - .lines() - .mapIndexed { i, line -> Placeholder.component("image${i + 1}", line.mm()) } - .toTypedArray(), - ), + MM.deserialize( + Regex("").replace(config.chatModule.welcomeText.joinToString("\n")) { "" }, + Placeholder.component("player", player.displayName()), + *player + .face() + .lines() + .mapIndexed { i, line -> Placeholder.component("image${i + 1}", MM.deserialize(line)) } + .toTypedArray(), + ), ) } - @EventHandler - fun on(event: PlayerSetSpawnEvent) { + /** + * Handles player set spawn events. + * @param event The [PlayerSetSpawnEvent] to be processed. + */ + private fun playerSetSpawn(event: PlayerSetSpawnEvent) { event.notification = - config.chatModule.i18n.playerSetSpawn.mm( - Placeholder.component( - "notification", - event.notification ?: return, - ), + MM.deserialize( + config.chatModule.i18n.playerSetSpawn, + Placeholder.component("notification", event.notification ?: return), ) } @@ -153,38 +158,30 @@ internal object ChatModule : ModuleInterface { message: String, ) { sender.sendMessage( - config.chatModule.whisperToFormat.mm( + MM.deserialize( + config.chatModule.whisperToFormat, Placeholder.component( "player", target .displayName() .clickEvent(ClickEvent.suggestCommand("/w ${target.name} ")) - .hoverEvent( - HoverEvent.showText( - config.chatModule.i18n.clickToWhisper - .mm(), - ), - ), + .hoverEvent(HoverEvent.showText(MM.deserialize(config.chatModule.i18n.clickToWhisper))), ), - Placeholder.component("message", message.mm()), + Placeholder.component("message", MM.deserialize(message)), ), ) target.sendMessage( - config.chatModule.whisperFromFormat.mm( + MM.deserialize( + config.chatModule.whisperFromFormat, Placeholder.component( "player", sender .displayName() .clickEvent(ClickEvent.suggestCommand("/w ${sender.name} ")) - .hoverEvent( - HoverEvent.showText( - config.chatModule.i18n.clickToWhisper - .mm(), - ), - ), + .hoverEvent(HoverEvent.showText(MM.deserialize(config.chatModule.i18n.clickToWhisper))), ), - Placeholder.component("message", message.mm()), + Placeholder.component("message", MM.deserialize(message)), ), ) } @@ -195,18 +192,12 @@ internal object ChatModule : ModuleInterface { * @return A [net.kyori.adventure.text.Component] representing the delete cross with hover text and click action. */ private fun createDeleteCross(signedMessage: SignedMessage): Component = - config.chatModule.deleteCross - .mm() - .hoverEvent( - config.chatModule.i18n.deleteMessage - .mm(), - ).clickEvent( - ClickEvent.callback { - instance.server - .deleteMessage(signedMessage) - }, - ) + MM + .deserialize(config.chatModule.deleteCross) + .hoverEvent(MM.deserialize(config.chatModule.i18n.deleteMessage)) + .clickEvent(ClickEvent.callback { instance.server.deleteMessage(signedMessage) }) + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, @@ -216,21 +207,11 @@ internal object ChatModule : ModuleInterface { "]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[", "⯈", "⯈", - "Welcome " - .clickSuggestCmd( - "/nickname", - "Set your nickname!", - ), + "Welcome Set your nickname!'> Change your locator color!'>", "⯈", - "Check out:", - "".clickRunCmd( - "/rules", - "View the server /rules", - ), - "".clickOpenUrl( - "https://illyria.fandom.com", - "Visit the wiki!", - ), + "Check out:", + " /rules", + " wiki", "⯈", "]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[", ), @@ -241,14 +222,17 @@ internal object ChatModule : ModuleInterface { var deleteCross: String = "[X]", var i18n: I18n = I18n(), ) { + /** Represents the internationalization strings for the module. */ @Serializable data class I18n( var clickMe: String = "Click me!", var clickToWhisper: String = "Click to Whisper", - var playerIsNotOnline: String = "${instance.prefix} Player is not Online!", + var playerIsNotOnline: String = + "${instance.prefix} Player is not Online!", var deleteMessage: String = "Click to delete your message", var clickToClipboard: String = "Click to copy position to clipboard", - var playerSetSpawn: String = "", + var playerSetSpawn: String = + "", ) } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/DimensionsModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/DimensionsModule.kt index 8c0e97d8c..da0b991da 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/DimensionsModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/DimensionsModule.kt @@ -14,28 +14,27 @@ import org.bukkit.event.player.PlayerTeleportEvent import org.bukkit.event.world.PortalCreateEvent import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.utils.ExtUtils.mm -import kotlin.math.pow -import kotlin.math.sqrt +import org.xodium.vanillaplus.utils.Utils.MM +import kotlin.math.hypot /** Represents a module handling dimension mechanics within the system. */ internal object DimensionsModule : ModuleInterface { private const val NETHER_TO_OVERWORLD_RATIO = 8 @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - fun on(event: PlayerPortalEvent) = handlePlayerPortal(event) + fun on(event: PlayerPortalEvent) = playerPortal(event) @EventHandler(priority = EventPriority.HIGH) - fun on(event: EntityPortalEvent) = handleEntityPortal(event) + fun on(event: EntityPortalEvent) = entityPortal(event) @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - fun on(event: PortalCreateEvent) = handlePortalCreate(event) + fun on(event: PortalCreateEvent) = portalCreate(event) /** * Handles the PlayerPortalEvent to prevent portal creation in the Nether. * @param event The PlayerPortalEvent to handle. */ - private fun handlePlayerPortal(event: PlayerPortalEvent) { + private fun playerPortal(event: PlayerPortalEvent) { if (event.cause == PlayerTeleportEvent.TeleportCause.NETHER_PORTAL) { if (event.player.world.environment == World.Environment.NETHER) event.canCreatePortal = false } @@ -45,7 +44,7 @@ internal object DimensionsModule : ModuleInterface { * Handles the EntityPortalEvent to prevent portal creation in the Nether. * @param event The EntityPortalEvent to handle. */ - private fun handleEntityPortal(event: EntityPortalEvent) { + private fun entityPortal(event: EntityPortalEvent) { if (event.entity.world.environment == World.Environment.NETHER) event.canCreatePortal = false } @@ -53,17 +52,18 @@ internal object DimensionsModule : ModuleInterface { * Handles the PortalCreateEvent to prevent portal creation in the Nether if no corresponding Overworld portal exists. * @param event The PortalCreateEvent to handle. */ - private fun handlePortalCreate(event: PortalCreateEvent) { + private fun portalCreate(event: PortalCreateEvent) { if (event.world.environment == World.Environment.NETHER && event.reason == PortalCreateEvent.CreateReason.FIRE ) { if (findCorrespondingPortal(calcPortalCentre(event.blocks), getOverworld()) == null) { event.isCancelled = true val player = event.entity as? Player ?: return - player.sendActionBar( - config.dimensionsModule.i18n.portalCreationDenied - .mm(), - ) + val overworld = getOverworld() + val destination = player.respawnLocation?.takeIf { it.world == overworld } ?: overworld.spawnLocation + + player.sendActionBar(MM.deserialize(config.dimensionsModule.i18n.portalCreationDenied)) + player.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN) } } } @@ -96,7 +96,7 @@ internal object DimensionsModule : ModuleInterface { for (y in 0..overworld.maxHeight) { val block = overworld.getBlockAt(x, y, z) if (block.type == Material.NETHER_PORTAL) { - val dist = distance2D(targetX, targetZ, x.toDouble(), z.toDouble()) + val dist = hypot(targetX - x.toDouble(), targetZ - z.toDouble()) if (dist < closestDistance) { closestDistance = dist closestPortal = block.location @@ -108,21 +108,6 @@ internal object DimensionsModule : ModuleInterface { return closestPortal } - /** - * Calculates the 2D Euclidean distance between two points in the X-Z plane. - * @param x1 The X-coordinate of the first point. - * @param z1 The Z-coordinate of the first point. - * @param x2 The X-coordinate of the second point. - * @param z2 The Z-coordinate of the second point. - * @return The distance between the two points. - */ - private fun distance2D( - x1: Double, - z1: Double, - x2: Double, - z2: Double, - ): Double = sqrt((x1 - x2).pow(2) + (z1 - z2).pow(2)) - /** * Calculates the centre point of a portal structure by averaging the positions of its constituent blocks. * @param blockStates The list of [org.bukkit.block.BlockState]s representing the portal frame and portal blocks. @@ -141,12 +126,14 @@ internal object DimensionsModule : ModuleInterface { */ private fun getOverworld(): World = instance.server.getWorld("world") ?: error("Overworld (world) is not loaded.") + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, var portalSearchRadius: Int = 128, var i18n: I18n = I18n(), ) { + /** Represents the internationalization strings for the module. */ @Serializable data class I18n( var portalCreationDenied: String = diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/EntityModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/EntityModule.kt index fd2c9063c..ba8d45bf6 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/EntityModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/EntityModule.kt @@ -54,6 +54,7 @@ internal object EntityModule : ModuleInterface { else -> false } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, @@ -63,6 +64,6 @@ internal object EntityModule : ModuleInterface { var disableEndermanGrief: Boolean = true, var disableGhastGrief: Boolean = true, var disableWitherGrief: Boolean = true, - var entityEggDropChance: Double = 0.1, + var entityEggDropChance: Double = 0.001, ) } diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt deleted file mode 100644 index cd68d02ca..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ /dev/null @@ -1,220 +0,0 @@ -@file:Suppress("ktlint:standard:no-wildcard-imports") - -package org.xodium.vanillaplus.modules - -import com.mojang.brigadier.arguments.StringArgumentType -import com.mojang.brigadier.context.CommandContext -import io.papermc.paper.command.brigadier.CommandSourceStack -import io.papermc.paper.command.brigadier.Commands -import kotlinx.serialization.Serializable -import net.kyori.adventure.sound.Sound -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import org.bukkit.Color -import org.bukkit.Material -import org.bukkit.Particle -import org.bukkit.block.Block -import org.bukkit.entity.Player -import org.bukkit.permissions.Permission -import org.bukkit.permissions.PermissionDefault -import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.data.CommandData -import org.xodium.vanillaplus.data.SoundData -import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.utils.BlockUtils.center -import org.xodium.vanillaplus.utils.CommandUtils.executesCatching -import org.xodium.vanillaplus.utils.CommandUtils.playerExecuted -import org.xodium.vanillaplus.utils.ExtUtils.mm -import org.xodium.vanillaplus.utils.InvUtils -import org.xodium.vanillaplus.utils.PlayerUtils -import org.xodium.vanillaplus.utils.ScheduleUtils -import java.util.concurrent.CompletableFuture - -/** Represents a module handling inv mechanics within the system. */ -internal object InvModule : ModuleInterface { - override val cmds = - listOf( - CommandData( - Commands - .literal("invsearch") - .requires { it.sender.hasPermission(perms[0]) } - .then( - Commands - .argument("material", StringArgumentType.word()) - .suggests { _, builder -> - Material.entries - .map { it.name.lowercase() } - .filter { it.startsWith(builder.remaining.lowercase()) } - .forEach(builder::suggest) - CompletableFuture.completedFuture(builder.build()) - }.playerExecuted { _, ctx -> handleSearch(ctx) }, - ).executesCatching { handleSearch(it) }, - "Search nearby chests for specific items", - listOf("search", "searchinv", "invs"), - ), - CommandData( - Commands - .literal("invunload") - .requires { it.sender.hasPermission(perms[1]) } - .playerExecuted { player, _ -> unload(player) }, - "Unload your inventory into nearby chests", - listOf("unload", "unloadinv", "invu"), - ), - ) - - override val perms = - listOf( - Permission( - "${instance.javaClass.simpleName}.invsearch".lowercase(), - "Allows use of the invsearch command", - PermissionDefault.TRUE, - ), - Permission( - "${instance.javaClass.simpleName}.invunload".lowercase(), - "Allows use of the invunload command", - PermissionDefault.TRUE, - ), - ) - - /** - * Handles the search command execution. - * @param ctx The command context containing the command source and arguments. - * @return An integer indicating the result of the command execution. - */ - private fun handleSearch(ctx: CommandContext): Int { - val player = ctx.source.sender as? Player ?: return 0 - val materialName = runCatching { StringArgumentType.getString(ctx, "material") }.getOrNull() - val material = - materialName?.let { Material.getMaterial(it.uppercase()) } ?: player.inventory.itemInMainHand.type - - if (material == Material.AIR) { - player.sendActionBar( - config.invModule.i18n.noMaterialSpecified - .mm(), - ) - return 0 - } - - search(player, material) - - return 1 - } - - /** - * Searches for chests within the specified radius of the player that contain the specified material. - * @param player The player who initiated the search. - * @param material The material to search for in the chests. - */ - private fun search( - player: Player, - material: Material, - ) { - val foundContainers = mutableListOf() - - for (container in PlayerUtils.getContainersAroundPlayer(player)) { - if (container.inventory.contains(material)) foundContainers.add(container.block) - } - - if (foundContainers.isEmpty()) { - player.sendActionBar( - config.invModule.i18n.noMatchingItems.mm( - Placeholder.component( - "material", - material.name.mm(), - ), - ), - ) - return - } - - player.sendActionBar( - config.invModule.i18n.foundItemsInChests - .mm(Placeholder.component("material", material.name.mm())), - ) - - ScheduleUtils.schedule(duration = 200L) { - foundContainers.forEach { container -> - Particle.TRAIL - .builder() - .location(player.location) - .data(Particle.Trail(container.center, Color.MAROON, 40)) - .receivers(player) - .spawn() - Particle.DUST - .builder() - .location(container.center) - .count(10) - .data(Particle.DustOptions(Color.MAROON, 5.0f)) - .receivers(player) - .spawn() - } - } - } - - /** - * Unloads items from the player's inventory into nearby chests. - * @param player The player whose inventory is to be unloaded. - */ - private fun unload(player: Player) { - val foundContainers = mutableListOf() - - for (container in PlayerUtils.getContainersAroundPlayer(player)) { - val transferred = - InvUtils.transferItems( - source = player.inventory, - destination = container.inventory, - startSlot = 9, - endSlot = 35, - onlyMatching = true, - enchantmentChecker = { item1, item2 -> item1.enchantments == item2.enchantments }, - ) - - if (transferred) foundContainers.add(container.block) - } - - if (foundContainers.isEmpty()) { - return player.sendActionBar( - config.invModule.i18n.noItemsUnloaded - .mm(), - ) - } - - player.sendActionBar( - config.invModule.i18n.inventoryUnloaded - .mm(), - ) - player.playSound(config.invModule.soundOnUnload.toSound(), Sound.Emitter.self()) - - ScheduleUtils.schedule(duration = 60L) { - foundContainers.forEach { container -> - Particle.DUST - .builder() - .location(container.center) - .count(10) - .data(Particle.DustOptions(Color.LIME, 5.0f)) - .receivers(player) - .spawn() - } - } - } - - @Serializable - data class Config( - var enabled: Boolean = true, - var soundOnUnload: SoundData = SoundData("entity.player.levelup", Sound.Source.PLAYER), - var i18n: I18n = I18n(), - ) { - @Serializable - data class I18n( - var noMaterialSpecified: String = - "You must specify a valid material " + - "or hold something in your hand", - var noMatchingItems: String = - "No containers contain " + - "", - var foundItemsInChests: String = - "Found in container(s), follow trail(s)", - var noItemsUnloaded: String = "No items were unloaded", - var inventoryUnloaded: String = "Inventory unloaded", - ) - } -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InventoryModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InventoryModule.kt new file mode 100644 index 000000000..82f7270b7 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InventoryModule.kt @@ -0,0 +1,130 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + +package org.xodium.vanillaplus.modules + +import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.command.brigadier.argument.ArgumentTypes +import kotlinx.serialization.Serializable +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.bukkit.Color +import org.bukkit.Material +import org.bukkit.Particle +import org.bukkit.block.Block +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.permissions.Permission +import org.bukkit.permissions.PermissionDefault +import org.xodium.vanillaplus.VanillaPlus.Companion.instance +import org.xodium.vanillaplus.data.CommandData +import org.xodium.vanillaplus.interfaces.ModuleInterface +import org.xodium.vanillaplus.utils.BlockUtils.center +import org.xodium.vanillaplus.utils.CommandUtils.playerExecuted +import org.xodium.vanillaplus.utils.PlayerUtils.getContainersAround +import org.xodium.vanillaplus.utils.ScheduleUtils +import org.xodium.vanillaplus.utils.Utils.MM + +/** Represents a module handling inventory mechanics within the system. */ +internal object InventoryModule : ModuleInterface { + override val cmds = + listOf( + CommandData( + Commands + .literal("invsearch") + .requires { it.sender.hasPermission(perms[0]) } + .then( + Commands + .argument("material", ArgumentTypes.itemStack()) + .playerExecuted { player, ctx -> + searchContainer(player, ctx.getArgument("material", ItemStack::class.java).type) + }, + ).playerExecuted { player, _ -> searchContainer(player, player.inventory.itemInMainHand.type) }, + "Search nearby chests for specific items", + listOf("search", "searchinv", "invs", "sinv"), + ), + ) + + override val perms = + listOf( + Permission( + "${instance.javaClass.simpleName}.invsearch".lowercase(), + "Allows use of the invsearch command", + PermissionDefault.TRUE, + ), + ) + + /** + * Searches for chests within the specified radius of the player that contain the specified material. + * @param player The player who initiated the search. + * @param material The material to search for in the chests. + */ + private fun searchContainer( + player: Player, + material: Material, + ) { + if (material == Material.AIR) { + player.sendActionBar(MM.deserialize(config.inventoryModule.i18n.noMaterialSpecified)) + return + } + + val foundContainers = mutableListOf() + + for (container in player.getContainersAround()) { + if (container.inventory.contains(material)) foundContainers.add(container.block) + } + + if (foundContainers.isEmpty()) { + player.sendActionBar( + MM.deserialize( + config.inventoryModule.i18n.noMatchingItems, + Placeholder.component("material", MM.deserialize(material.name)), + ), + ) + return + } + + player.sendActionBar( + MM.deserialize( + config.inventoryModule.i18n.foundItemsInChests, + Placeholder.component("material", MM.deserialize(material.name)), + ), + ) + + ScheduleUtils.schedule(duration = 200L) { + foundContainers.forEach { container -> + Particle.TRAIL + .builder() + .location(player.location) + .data(Particle.Trail(container.center(), Color.MAROON, 40)) + .receivers(player) + .spawn() + Particle.DUST + .builder() + .location(container.center()) + .count(10) + .data(Particle.DustOptions(Color.MAROON, 5.0f)) + .receivers(player) + .spawn() + } + } + } + + /** Represents the config of the module. */ + @Serializable + data class Config( + var enabled: Boolean = true, + var i18n: I18n = I18n(), + ) { + /** Represents the internationalization strings for the module. */ + @Serializable + data class I18n( + var noMaterialSpecified: String = + "You must specify a valid material " + + "or hold something in your hand", + var noMatchingItems: String = + "No containers contain " + + "", + var foundItemsInChests: String = + "Found in container(s), follow trail(s)", + ) + } +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/LocatorModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/LocatorModule.kt index a7f6b911c..b1d4e64b3 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/LocatorModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/LocatorModule.kt @@ -7,20 +7,16 @@ import io.papermc.paper.command.brigadier.argument.ArgumentTypes import kotlinx.serialization.Serializable import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextColor -import org.bukkit.entity.Player import org.bukkit.permissions.Permission import org.bukkit.permissions.PermissionDefault import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.utils.CommandUtils.playerExecuted -import java.util.* -import java.util.concurrent.CompletableFuture +import org.xodium.vanillaplus.utils.PlayerUtils.locator /** Represents a module handling locator mechanics within the system. */ internal object LocatorModule : ModuleInterface { - private val colors = NamedTextColor.NAMES.keys().map { it.toString() } + listOf("", "reset") - override val cmds = listOf( CommandData( @@ -30,24 +26,19 @@ internal object LocatorModule : ModuleInterface { .then( Commands .argument("color", ArgumentTypes.namedColor()) - .suggests { _, builder -> - colors - .filter { it.startsWith(builder.remaining.lowercase()) } - .forEach(builder::suggest) - CompletableFuture.completedFuture(builder.build()) - }.playerExecuted { player, ctx -> - locator(player, colour = ctx.getArgument("color", NamedTextColor::class.java)) + .playerExecuted { player, ctx -> + player.locator(ctx.getArgument("color", NamedTextColor::class.java)) }, ).then( Commands .argument("hex", ArgumentTypes.hexColor()) .playerExecuted { player, ctx -> - locator(player, hex = ctx.getArgument("hex", TextColor::class.java)) + player.locator(ctx.getArgument("hex", TextColor::class.java)) }, ).then( Commands .literal("reset") - .playerExecuted { player, _ -> locator(player) }, + .playerExecuted { player, _ -> player.locator() }, ), "Allows players to personalise their locator bar", listOf("lc"), @@ -63,37 +54,7 @@ internal object LocatorModule : ModuleInterface { ), ) - /** - * Modifies the colour of a player's waypoint based on the specified parameters. - * @param player The player whose waypoint is being modified. - * @param colour The optional named colour to apply to the waypoint. - * @param hex The optional hex colour to apply to the waypoint. - */ - private fun locator( - player: Player, - colour: NamedTextColor? = null, - hex: TextColor? = null, - ) { - val cmd = "waypoint modify ${player.name}" - - when { - colour != null -> { - instance.server.dispatchCommand(player, "$cmd color $colour") - } - - hex != null -> { - instance.server.dispatchCommand( - player, - "$cmd color hex ${String.format(Locale.ENGLISH, "%06X", hex.value())}", - ) - } - - else -> { - instance.server.dispatchCommand(player, "$cmd color reset") - } - } - } - + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/MotdModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/MotdModule.kt index b83922350..9a90bdb02 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/MotdModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/MotdModule.kt @@ -5,7 +5,7 @@ import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.server.ServerListPingEvent import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.utils.ExtUtils.mm +import org.xodium.vanillaplus.utils.Utils.MM /** Represents a module handling MOTD mechanics within the system. */ internal object MotdModule : ModuleInterface { @@ -16,13 +16,9 @@ internal object MotdModule : ModuleInterface { * Sets the MOTD for the server list ping event. * @param event The server list ping event. */ - private fun motd(event: ServerListPingEvent) = - event.motd( - config.motdModule.motd - .joinToString("\n") - .mm(), - ) + private fun motd(event: ServerListPingEvent) = event.motd(MM.deserialize(config.motdModule.motd.joinToString("\n"))) + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/OpenableModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/OpenableModule.kt index f5deed0b6..0e4e3d17c 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/OpenableModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/OpenableModule.kt @@ -23,11 +23,10 @@ import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.AdjacentBlockData import org.xodium.vanillaplus.data.SoundData import org.xodium.vanillaplus.interfaces.ModuleInterface -import java.util.* /** Represents a module handling openable blocks mechanics within the system. */ internal object OpenableModule : ModuleInterface { - private val disallowedKnockGameModes = EnumSet.of(GameMode.CREATIVE, GameMode.SPECTATOR) + private val disallowedKnockGameModes = setOf(GameMode.CREATIVE, GameMode.SPECTATOR) private val possibleNeighbours: Set = setOf( AdjacentBlockData(0, -1, Door.Hinge.RIGHT, BlockFace.EAST), @@ -41,13 +40,13 @@ internal object OpenableModule : ModuleInterface { ) @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - fun on(event: PlayerInteractEvent) = handleInteract(event) + fun on(event: PlayerInteractEvent) = playerInteract(event) /** * Handles block interactions and delegates to the correct click handler. * @param event The [PlayerInteractEvent] triggered by the player. */ - private fun handleInteract(event: PlayerInteractEvent) { + private fun playerInteract(event: PlayerInteractEvent) { val clickedBlock = event.clickedBlock ?: return if (!isValidInteraction(event)) return @@ -230,6 +229,7 @@ internal object OpenableModule : ModuleInterface { ?.getRelativeBlock(block) } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/PetModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/PetModule.kt deleted file mode 100644 index 07f8b27c2..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/PetModule.kt +++ /dev/null @@ -1,138 +0,0 @@ -package org.xodium.vanillaplus.modules - -import kotlinx.serialization.Serializable -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import org.bukkit.Material -import org.bukkit.entity.LivingEntity -import org.bukkit.entity.Player -import org.bukkit.entity.Tameable -import org.bukkit.event.EventHandler -import org.bukkit.event.player.PlayerInteractEntityEvent -import org.bukkit.inventory.ItemStack -import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.utils.ExtUtils.mm - -/** Represents a module handling pet mechanics within the system. */ -internal object PetModule : ModuleInterface { - @EventHandler - fun on(event: PlayerInteractEntityEvent) = handleInteractEntity(event) - - /** - * Handles transferring ownership of a leashed pet when a player - * right-clicks another player while holding a lead. - * @param event The [PlayerInteractEntityEvent] triggered on entity interaction. - */ - private fun handleInteractEntity(event: PlayerInteractEntityEvent) { - val source = event.player - val target = event.rightClicked as? Player ?: return - - if (source == target) return - if (source.inventory.itemInMainHand.type != Material.LEAD) return - - val leashedEntity = findLeashedPet(source) ?: return - - if (!isTransferablePet(leashedEntity, source)) return - - transferPetOwnership(source, target, leashedEntity) - event.isCancelled = true - } - - /** - * Checks if a pet can be transferred to another player. - * @param pet The tameable entity to check. - * @param owner The player attempting to transfer ownership. - * @return `true` if the pet is tamed and owned by the player, `false` otherwise. - */ - private fun isTransferablePet( - pet: Tameable, - owner: Player, - ): Boolean = pet.isTamed && pet.owner == owner - - /** - * Transfers ownership of a pet from one player to another. - * @param source The original owner of the pet. - * @param target The new owner of the pet. - * @param pet The tameable entity being transferred. - */ - private fun transferPetOwnership( - source: Player, - target: Player, - pet: Tameable, - ) { - pet.owner = target - pet.setLeashHolder(null) - - returnLeadToSource(source) - notifyTransfer(source, target, pet.customName() ?: pet.name.mm()) - } - - /** - * Attempts to return a lead to the player's inventory, dropping it if inventory is full. - * @param player The player to return the lead to. - */ - private fun returnLeadToSource(player: Player) { - player.inventory - .addItem(ItemStack(Material.LEAD)) - .takeIf { it.isNotEmpty() } - ?.let { player.world.dropItem(player.location, ItemStack(Material.LEAD)) } - } - - /** - * Finds the first leashed pet owned by the player within the config radius. - * @param player The player to search around. - * @return The found tameable entity or `null` if none exists. - */ - private fun findLeashedPet(player: Player): Tameable? = - player - .getNearbyEntities( - config.petModule.transferRadius.toDouble(), - config.petModule.transferRadius.toDouble(), - config.petModule.transferRadius.toDouble(), - ).filterIsInstance() - .firstOrNull { it.isLeashed && it.leashHolder == player } - as? Tameable - - /** - * Notifies both players about the pet transfer via action bar messages. - * @param source The original owner of the pet. - * @param target The new owner of the pet. - * @param petName The display name of the transferred pet. - */ - private fun notifyTransfer( - source: Player, - target: Player, - petName: Component, - ) { - source.sendActionBar( - config.petModule.i18n.sourceTransfer.mm( - Placeholder.component("", petName), - Placeholder.component("", target.displayName()), - ), - ) - - target.sendActionBar( - config.petModule.i18n.targetTransfer.mm( - Placeholder.component("", petName), - Placeholder.component("", source.displayName()), - ), - ) - } - - @Serializable - data class Config( - var enabled: Boolean = true, - var transferRadius: Int = 10, - var i18n: I18n = I18n(), - ) { - @Serializable - data class I18n( - var sourceTransfer: String = - "You have transferred " + - "to ", - var targetTransfer: String = - " has transferred " + - "to you", - ) - } -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt index 8fc026474..57a682d0f 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt @@ -5,7 +5,6 @@ package org.xodium.vanillaplus.modules import com.mojang.brigadier.arguments.StringArgumentType import io.papermc.paper.command.brigadier.Commands import io.papermc.paper.datacomponent.DataComponentTypes -import io.papermc.paper.datacomponent.item.ItemLore import io.papermc.paper.datacomponent.item.ResolvableProfile import io.papermc.paper.event.entity.EntityEquipmentChangedEvent import kotlinx.serialization.Serializable @@ -20,10 +19,7 @@ import org.bukkit.event.entity.PlayerDeathEvent import org.bukkit.event.inventory.ClickType import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryType -import org.bukkit.event.player.PlayerAdvancementDoneEvent -import org.bukkit.event.player.PlayerInteractEvent -import org.bukkit.event.player.PlayerJoinEvent -import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.player.* import org.bukkit.inventory.ItemStack import org.bukkit.permissions.Permission import org.bukkit.permissions.PermissionDefault @@ -31,14 +27,14 @@ import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.enchantments.* import org.xodium.vanillaplus.interfaces.ModuleInterface +import org.xodium.vanillaplus.managers.PlayerMessageManager import org.xodium.vanillaplus.pdcs.PlayerPDC.nickname import org.xodium.vanillaplus.utils.CommandUtils.playerExecuted -import org.xodium.vanillaplus.utils.ExtUtils.mm +import org.xodium.vanillaplus.utils.Utils.MM +import kotlin.random.Random /** Represents a module handling player mechanics within the system. */ internal object PlayerModule : ModuleInterface { - private val tabListModule by lazy { TabListModule } - override val cmds = listOf( CommandData( @@ -69,82 +65,34 @@ internal object PlayerModule : ModuleInterface { @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) fun on(event: PlayerJoinEvent) { - val player = event.player - - player.displayName(player.nickname?.mm()) - - if (config.playerModule.i18n.playerJoinMsg - .isEmpty() - ) { - return - } - - event.joinMessage(null) - - instance.server.onlinePlayers - .filter { it.uniqueId != player.uniqueId } - .forEach { - it.sendMessage( - config.playerModule.i18n.playerJoinMsg.mm( - Placeholder.component("player", player.displayName()), - ), - ) - } + event.player.setNickname() + event.joinMessage(PlayerMessageManager.handleJoin(event.player) ?: return) } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) fun on(event: PlayerQuitEvent) { - if (config.playerModule.i18n.playerQuitMsg - .isEmpty() - ) { - return - } + event.quitMessage(PlayerMessageManager.handleQuit(event.player) ?: return) + } - event.quitMessage( - config.playerModule.i18n.playerQuitMsg.mm( - Placeholder.component( - "player", - event.player.displayName(), - ), - ), - ) + @EventHandler + fun on(event: PlayerKickEvent) { + event.leaveMessage(PlayerMessageManager.handleKick(event.reason()) ?: return) } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) fun on(event: PlayerDeathEvent) { - val killer = event.entity.killer ?: return - - if (Math.random() < config.playerModule.skullDropChance) { - event.entity.world.dropItemNaturally( - event.entity.location, - playerSkull(event.entity, killer), - ) - } - // TODO -// if (config.playerFeature.i18n.playerDeathMsg.isNotEmpty()) event.deathMessage() -// if (config.playerFeature.i18n.playerDeathScreenMsg.isNotEmpty()) event.deathScreenMessageOverride() + dropPlayerHead(event.player) + event.deathMessage(PlayerMessageManager.handleDeath(event.player, event.entity.killer) ?: return) + event.deathScreenMessageOverride(PlayerMessageManager.handleDeathScreen()) } @EventHandler fun on(event: PlayerAdvancementDoneEvent) { - if (config.playerModule.i18n.playerAdvancementDoneMsg - .isEmpty() - ) { - return - } - - event.message( - config.playerModule.i18n.playerAdvancementDoneMsg.mm( - Placeholder.component("player", event.player.displayName()), - Placeholder.component("advancement", event.advancement.displayName()), - ), - ) + event.message(PlayerMessageManager.handleAdvancement(event.player, event.advancement) ?: return) } @EventHandler - fun on(event: InventoryClickEvent) { - enderchest(event) - } + fun on(event: InventoryClickEvent) = handleEnderchest(event) @EventHandler(ignoreCancelled = true) fun on(event: PlayerInteractEvent) { @@ -160,13 +108,19 @@ internal object PlayerModule : ModuleInterface { } @EventHandler - fun on(event: BlockDropItemEvent) { - PickupEnchantment.pickup(event) - } + fun on(event: BlockDropItemEvent) = PickupEnchantment.pickup(event) @EventHandler - fun on(event: EntityEquipmentChangedEvent) { - NightVisionEnchantment.nightVision(event) + fun on(event: EntityEquipmentChangedEvent) = NightVisionEnchantment.nightVision(event) + + /** + * Handles the event when a player dies. + * @param event The PlayerDeathEvent triggered when a player dies. + */ + private fun dropPlayerHead(player: Player) { + if (Random.nextDouble() > config.playerModule.skullDropChance) return + + player.world.dropItemNaturally(player.location, player.head()) } /** @@ -174,13 +128,10 @@ internal object PlayerModule : ModuleInterface { * in their inventory. * @param event The InventoryClickEvent triggered when a player clicks in an inventory. */ - private fun enderchest(event: InventoryClickEvent) { - if (event.click != config.playerModule.enderChestClickType || - event.currentItem?.type != Material.ENDER_CHEST || - event.clickedInventory?.type != InventoryType.PLAYER - ) { - return - } + private fun handleEnderchest(event: InventoryClickEvent) { + if (event.click != config.playerModule.enderChestClickType) return + if (event.currentItem?.type != Material.ENDER_CHEST) return + if (event.clickedInventory?.type != InventoryType.PLAYER) return event.isCancelled = true @@ -225,78 +176,57 @@ internal object PlayerModule : ModuleInterface { name: String, ) { player.nickname = name - player.displayName(player.nickname?.mm()) - // TODO: add enabled check. - tabListModule.updatePlayerDisplayName(player) + player.displayName(MM.deserialize(player.nickname)) player.sendActionBar( - config.playerModule.i18n.nicknameUpdated.mm( - Placeholder.component( - "nickname", - player.displayName(), - ), + MM.deserialize( + config.playerModule.i18n.nicknameUpdated, + Placeholder.component("nickname", player.displayName()), ), ) } - /** - * Creates a custom player skull item when a player is killed. - * @param entity The player whose head is being created. - * @param killer The player who killed the entity. - * @return An [ItemStack] representing the customized player head. - */ + /** Sets the display name of the player based on their nickname. */ + private fun Player.setNickname() = displayName(MM.deserialize(nickname)) + + /** Drops the player's head at their location based on the configured chance. */ @Suppress("UnstableApiUsage") - private fun playerSkull( - entity: Player, - killer: Player, - ): ItemStack = + private fun Player.head(): ItemStack = ItemStack.of(Material.PLAYER_HEAD).apply { - setData(DataComponentTypes.PROFILE, ResolvableProfile.resolvableProfile(entity.playerProfile)) - setData( - DataComponentTypes.CUSTOM_NAME, - config.playerModule.i18n.playerHeadName - .mm(Placeholder.component("player", entity.name.mm())), - ) - setData( - DataComponentTypes.LORE, - ItemLore - .lore( - config.playerModule.i18n.playerHeadLore - .mm( - Placeholder.component("player", entity.name.mm()), - Placeholder.component("killer", killer.name.mm()), - ), - ), - ) + setData(DataComponentTypes.PROFILE, ResolvableProfile.resolvableProfile(playerProfile)) } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, var enderChestClickType: ClickType = ClickType.SHIFT_RIGHT, - var skullDropChance: Double = 0.1, + var skullDropChance: Double = 0.01, var xpCostToBottle: Int = 11, var silkTouch: SilkTouchEnchantment = SilkTouchEnchantment(), var i18n: I18n = I18n(), ) { + /** Represents the settings for the Silk Touch enchantment. */ @Serializable data class SilkTouchEnchantment( var allowSpawnerSilk: Boolean = true, var allowBuddingAmethystSilk: Boolean = true, ) + /** Represents the internationalization strings for the module. */ @Serializable data class I18n( - var playerHeadName: String = "’s Skull", - var playerHeadLore: List = listOf(" killed by "), -// var playerDeathMsg: String = " ", var playerJoinMsg: String = " ", var playerQuitMsg: String = " ", - var playerDeathMsg: String = "☠ ›", + var playerDeathByPlayerMsg: String = " ", var playerDeathScreenMsg: String = "☠", var playerAdvancementDoneMsg: String = "\uD83C\uDF89 " + "has made the advancement: ", - var nicknameUpdated: String = "Nickname has been updated to: ", + var playerKickMsg: String = + " " + + "reason: ", + var nicknameUpdated: String = + "Nickname has been updated to: ", ) } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt index a023f244c..df54d21ec 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt @@ -37,15 +37,15 @@ internal object ScoreBoardModule : ModuleInterface { ) @EventHandler - fun on(event: PlayerJoinEvent) = handleJoin(event) + fun on(event: PlayerJoinEvent) = playerJoin(event) /** * Applies the correct scoreboard to players when they join. * @param event The [PlayerJoinEvent] triggered when the player joins. */ - private fun handleJoin(event: PlayerJoinEvent) { + private fun playerJoin(event: PlayerJoinEvent) { event.player.scoreboard = - if (event.player.scoreboardVisibility == true) { + if (event.player.scoreboardVisibility) { instance.server.scoreboardManager.newScoreboard } else { instance.server.scoreboardManager.mainScoreboard @@ -57,7 +57,7 @@ internal object ScoreBoardModule : ModuleInterface { * @param player The player whose scoreboard sidebar should be toggled. */ private fun toggle(player: Player) { - if (player.scoreboardVisibility == true) { + if (player.scoreboardVisibility) { player.scoreboard = instance.server.scoreboardManager.mainScoreboard player.scoreboardVisibility = false } else { @@ -66,6 +66,7 @@ internal object ScoreBoardModule : ModuleInterface { } } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/SignModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/SignModule.kt index 59eb5bd12..1dbac6783 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/SignModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/SignModule.kt @@ -12,7 +12,7 @@ import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.utils.CommandUtils.playerExecuted -import org.xodium.vanillaplus.utils.ExtUtils.mm +import org.xodium.vanillaplus.utils.Utils.MM /** Represents a module handling sign mechanics within the system. */ internal object SignModule : ModuleInterface { @@ -65,10 +65,11 @@ internal object SignModule : ModuleInterface { val sign = target.state as Sign val signSide = sign.getSide(sign.getInteractableSideFor(player)) - signSide.line(line, text.mm()) + signSide.line(line, MM.deserialize(text)) sign.update() } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/SitModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/SitModule.kt index 275bb1efb..21733b83e 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/SitModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/SitModule.kt @@ -1,8 +1,7 @@ -@file:Suppress("ktlint:standard:no-wildcard-imports") - package org.xodium.vanillaplus.modules import kotlinx.serialization.Serializable +import org.bukkit.GameMode import org.bukkit.Location import org.bukkit.Material import org.bukkit.block.BlockFace @@ -14,39 +13,48 @@ import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.block.Action +import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.entity.EntityDamageEvent import org.bukkit.event.entity.EntityDismountEvent import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.util.Vector import org.xodium.vanillaplus.interfaces.ModuleInterface -import java.util.* +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +import kotlin.uuid.toKotlinUuid /** Represents a module handling sit mechanics within the system. */ +@OptIn(ExperimentalUuidApi::class) internal object SitModule : ModuleInterface { - private val sittingPlayers = mutableMapOf() + private val sittingPlayers = mutableMapOf() + private val occupiedBlocks = mutableMapOf() private val blockCenterOffset = Vector(0.5, 0.5, 0.5) private val playerStandUpOffset = Vector(0.0, 0.5, 0.0) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - fun on(event: PlayerInteractEvent) = handleInteract(event) + fun on(event: PlayerInteractEvent) = playerInteract(event) @EventHandler - fun on(event: EntityDismountEvent) = handleDismount(event) + fun on(event: EntityDismountEvent) = entityDismount(event) @EventHandler - fun on(event: PlayerQuitEvent) = handleQuit(event) + fun on(event: PlayerQuitEvent) = playerQuit(event) @EventHandler(ignoreCancelled = true) - fun on(event: EntityDamageEvent) = handleDamage(event) + fun on(event: EntityDamageEvent) = entityDamage(event) + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun on(event: BlockBreakEvent) = blockBreak(event) /** * Handles player interaction to initiate sitting. * @param event The [PlayerInteractEvent] triggered by the player. */ - private fun handleInteract(event: PlayerInteractEvent) { + private fun playerInteract(event: PlayerInteractEvent) { val player = event.player + if (player.gameMode != GameMode.SURVIVAL) return if (event.action != Action.RIGHT_CLICK_BLOCK || player.isSneaking || player.isInsideVehicle) return if (player.inventory.itemInMainHand.type != Material.AIR) return @@ -61,8 +69,8 @@ internal object SitModule : ModuleInterface { } if (!isSitTarget) return - if (block.getRelative(BlockFace.UP).type.isCollidable) return + if (occupiedBlocks.containsKey(block.location)) return event.isCancelled = true sit(player, block.location.add(blockCenterOffset)) @@ -72,11 +80,20 @@ internal object SitModule : ModuleInterface { * Handles dismounting from the sitting ArmorStand. * @param event The [EntityDismountEvent] triggered when the player dismounts. */ - private fun handleDismount(event: EntityDismountEvent) { + private fun entityDismount(event: EntityDismountEvent) { val player = event.entity as? Player ?: return - sittingPlayers.remove(player.uniqueId)?.let { armorStand -> + sittingPlayers.remove(player.uniqueId.toKotlinUuid())?.let { armorStand -> + val blockLocation = + armorStand.location + .clone() + .subtract(blockCenterOffset) + .block.location + + occupiedBlocks.remove(blockLocation) + val safe = armorStand.location.clone().add(playerStandUpOffset) + safe.yaw = player.location.yaw safe.pitch = player.location.pitch player.teleport(safe) @@ -88,20 +105,60 @@ internal object SitModule : ModuleInterface { * Handles clean-up when a player quits. * @param event The [PlayerQuitEvent] triggered when the player leaves the server. */ - private fun handleQuit(event: PlayerQuitEvent) { - sittingPlayers.remove(event.player.uniqueId)?.remove() + private fun playerQuit(event: PlayerQuitEvent) { + sittingPlayers.remove(event.player.uniqueId.toKotlinUuid())?.let { armorStand -> + val blockLocation = + armorStand.location + .clone() + .subtract(blockCenterOffset) + .block.location + + occupiedBlocks.remove(blockLocation) + armorStand.remove() + } } /** * Handles player damage while sitting. * @param event The [EntityDamageEvent] triggered when the player takes damage. */ - private fun handleDamage(event: EntityDamageEvent) { + private fun entityDamage(event: EntityDamageEvent) { val player = event.entity as? Player ?: return + val playerId = player.uniqueId.toKotlinUuid() + + sittingPlayers[playerId]?.let { stand -> + val blockLocation = + stand.location + .clone() + .subtract(blockCenterOffset) + .block.location - sittingPlayers[player.uniqueId]?.let { stand -> + occupiedBlocks.remove(blockLocation) stand.removePassenger(player) - sittingPlayers.remove(player.uniqueId) + sittingPlayers.remove(playerId) + } + } + + /** + * Handles block break events to remove sitting ArmorStands on broken blocks. + * @param event The [BlockBreakEvent] triggered when a block is broken. + */ + private fun blockBreak(event: BlockBreakEvent) { + val brokenBlockLocation = event.block.location + + sittingPlayers.entries.removeIf { (_, armorStand) -> + val armorStandBlock = armorStand.location.subtract(blockCenterOffset).block + + if (armorStandBlock.location == brokenBlockLocation) { + occupiedBlocks.remove(brokenBlockLocation) + armorStand.passengers + .filterIsInstance() + .forEach { player -> armorStand.removePassenger(player) } + armorStand.remove() + true + } else { + false + } } } @@ -115,6 +172,11 @@ internal object SitModule : ModuleInterface { location: Location, ) { val world = location.world ?: return + val blockLocation = + location + .clone() + .subtract(blockCenterOffset) + .block.location val armorStand = world.spawn(location, ArmorStand::class.java) { it.isVisible = false @@ -124,9 +186,14 @@ internal object SitModule : ModuleInterface { } armorStand.addPassenger(player) - sittingPlayers[player.uniqueId] = armorStand + + val playerId = player.uniqueId.toKotlinUuid() + + sittingPlayers[playerId] = armorStand + occupiedBlocks[blockLocation] = playerId } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/TabListModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/TabListModule.kt index a917083a3..3d33b9cb2 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/TabListModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/TabListModule.kt @@ -4,19 +4,14 @@ package org.xodium.vanillaplus.modules import kotlinx.serialization.Serializable import net.kyori.adventure.audience.Audience -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.JoinConfiguration import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority -import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.weather.ThunderChangeEvent import org.bukkit.event.weather.WeatherChangeEvent import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.utils.ExtUtils.mm -import java.util.* +import org.xodium.vanillaplus.utils.Utils.MM import kotlin.math.roundToInt /** Represents a module handling tab-list mechanics within the system. */ @@ -28,52 +23,36 @@ internal object TabListModule : ModuleInterface { private const val COLOR_FORMAT = "#%02X%02X%02X" init { - instance.server.onlinePlayers.forEach { - updateTabList(it) - updatePlayerDisplayName(it) - } - // TPS Check. instance.server.scheduler.runTaskTimer( instance, - Runnable { instance.server.onlinePlayers.forEach { updateTabList(it) } }, + Runnable { + instance.server.onlinePlayers.forEach { player -> + tablist(player) + player.playerListName(player.displayName()) + } + }, config.tabListModule.initDelayInTicks, config.tabListModule.intervalInTicks, ) } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - fun on(event: PlayerJoinEvent) { - updateTabList(event.player) - updatePlayerDisplayName(event.player) - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - fun on(event: WeatherChangeEvent) = event.world.players.forEach { updateTabList(it) } + fun on(event: WeatherChangeEvent) = event.world.players.forEach { player -> tablist(player) } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - fun on(event: ThunderChangeEvent) = event.world.players.forEach { updateTabList(it) } - - /** - * Update the player's display name in the tab list. - * @param player the player to update. - */ - fun updatePlayerDisplayName(player: Player): Unit = player.playerListName(player.displayName()) + fun on(event: ThunderChangeEvent) = event.world.players.forEach { player -> tablist(player) } /** * Update the tab list for the given audience. * @param audience the audience to update the tab list for. */ - private fun updateTabList(audience: Audience) { - val joinConfig = JoinConfiguration.separator(Component.newline()) - + private fun tablist(audience: Audience) { audience.sendPlayerListHeaderAndFooter( - Component.join(joinConfig, config.tabListModule.header.mm()), - Component.join( - joinConfig, - config.tabListModule.footer.mm( - Placeholder.component("weather", getWeather().mm()), - Placeholder.component("tps", getTps().mm()), - ), + MM.deserialize(config.tabListModule.header.joinToString("\n")), + MM.deserialize( + config.tabListModule.footer.joinToString("\n"), + Placeholder.component("weather", MM.deserialize(getWeather())), + Placeholder.component("tps", MM.deserialize(getTps())), ), ) } @@ -87,7 +66,7 @@ internal object TabListModule : ModuleInterface { val clampedTps = tps.coerceIn(MIN_TPS, MAX_TPS) val ratio = clampedTps / MAX_TPS val color = getColorForTps(ratio) - val formattedTps = String.format(Locale.ENGLISH, TPS_DECIMAL_FORMAT, tps) + val formattedTps = TPS_DECIMAL_FORMAT.format(tps) return "$formattedTps" } @@ -102,7 +81,7 @@ internal object TabListModule : ModuleInterface { val r = (MAX_COLOR_VALUE * (1 - clamped)).roundToInt() val g = (MAX_COLOR_VALUE * clamped).roundToInt() - return String.format(Locale.ENGLISH, COLOR_FORMAT, r, g, 0) + return COLOR_FORMAT.format(r, g, 0) } /** @@ -119,6 +98,7 @@ internal object TabListModule : ModuleInterface { } } + /** Represents the config of the module. */ @Serializable data class Config( var enabled: Boolean = true, @@ -136,6 +116,7 @@ internal object TabListModule : ModuleInterface { ), var i18n: I18n = I18n(), ) { + /** Represents the internationalization strings for the module. */ @Serializable data class I18n( var weatherThundering: String = "\uD83C\uDF29", diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/TameableModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/TameableModule.kt new file mode 100644 index 000000000..397ee951f --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/TameableModule.kt @@ -0,0 +1,42 @@ +package org.xodium.vanillaplus.modules + +import kotlinx.serialization.Serializable +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.player.PlayerInteractEntityEvent +import org.xodium.vanillaplus.interfaces.ModuleInterface +import org.xodium.vanillaplus.utils.PlayerUtils.getLeashedEntity + +/** Represents a module handling tameable mechanics within the system. */ +internal object TameableModule : ModuleInterface { + @EventHandler + fun on(event: PlayerInteractEntityEvent) = playerInteractEntity(event) + + /** + * Handles the interaction event when a player interacts with another entity. + * @param event The [PlayerInteractEntityEvent] triggered on entity interaction. + */ + private fun playerInteractEntity(event: PlayerInteractEntityEvent) { + val source = event.player + val target = event.rightClicked as? Player ?: return + + if (source == target) return + if (source.inventory.itemInMainHand.type != Material.LEAD) return + + val pet = source.getLeashedEntity() ?: return + + if (!pet.isTamed || pet.owner != source) return + + pet.owner = target + pet.setLeashHolder(target) + + event.isCancelled = true + } + + /** Represents the config of the module. */ + @Serializable + data class Config( + var enabled: Boolean = true, + ) +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/TreesModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/TreesModule.kt deleted file mode 100644 index 54ca4dbb9..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/TreesModule.kt +++ /dev/null @@ -1,224 +0,0 @@ -package org.xodium.vanillaplus.modules - -import com.sk89q.worldedit.WorldEdit -import com.sk89q.worldedit.bukkit.BukkitAdapter -import com.sk89q.worldedit.extent.clipboard.Clipboard -import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats -import com.sk89q.worldedit.function.mask.BlockTypeMask -import com.sk89q.worldedit.function.operation.Operations -import com.sk89q.worldedit.math.BlockVector3 -import com.sk89q.worldedit.math.transform.AffineTransform -import com.sk89q.worldedit.session.ClipboardHolder -import kotlinx.serialization.Serializable -import org.bukkit.Material -import org.bukkit.Tag -import org.bukkit.block.Block -import org.bukkit.event.EventHandler -import org.bukkit.event.EventPriority -import org.bukkit.event.world.StructureGrowEvent -import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.interfaces.ModuleInterface -import org.xodium.vanillaplus.registries.MaterialRegistry -import java.io.IOException -import java.nio.channels.Channels -import java.nio.channels.ReadableByteChannel -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import java.util.stream.Collectors - -/** Represents a module handling tree mechanics within the system. */ -internal object TreesModule : ModuleInterface { - private val schematicCache: Map> by lazy { - MaterialRegistry.SAPLING_LINKS.mapValues { loadSchematics("/schematics/${it.value}") } - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - fun on(event: StructureGrowEvent) = handleStructureGrow(event) - - /** - * Handles StructureGrowEvent and attempts to paste a schematic - * when the grown block is a sapling or fungus. - * @param event The [StructureGrowEvent] triggered by natural growth. - */ - private fun handleStructureGrow(event: StructureGrowEvent) { - event.location.block - .takeIf { - Tag.SAPLINGS.isTagged(it.type) || - it.type == Material.WARPED_FUNGUS || - it.type == Material.CRIMSON_FUNGUS - }?.let { event.isCancelled = pasteSchematic(it) } - } - - /** - * Load schematics from the specified resource directory. - * @param resourceDir The directory containing the schematics. - * @return A list of loaded schematics. - */ - private fun loadSchematics(resourceDir: String): List { - val url = javaClass.getResource(resourceDir) ?: error("Resource directory not found: $resourceDir") - return try { - FileSystems.newFileSystem(url.toURI(), mapOf("create" to false)).use { fs -> - Files - .walk(fs.getPath(resourceDir.removePrefix("/")), 1) - .filter { Files.isRegularFile(it) } - .collect(Collectors.toList()) - .map { path -> - Files.newByteChannel(path, StandardOpenOption.READ).use { channel -> - readClipboard(path, channel) - } - } - } - } catch (e: IOException) { - error("Failed to load schematics from $resourceDir: ${e.message}") - } - } - - /** - * Read a schematic from the specified path. - * @param path The path to the schematic file. - * @param channel The channel to read the schematic from. - * @return The loaded schematic. - */ - private fun readClipboard( - path: Path, - channel: ReadableByteChannel, - ): Clipboard { - val format = ClipboardFormats.findByAlias("schem") ?: error("Unsupported schematic format for resource: $path") - return try { - format.getReader(Channels.newInputStream(channel)).read() - } catch (e: Exception) { - throw IOException("Failed to read schematic $path: ${e.message}", e) - } - } - - /** - * Paste a random schematic at the specified block. - * @param block The block to paste the schematic at. - * @return True if the schematic was pasted successfully. - */ - private fun pasteSchematic(block: Block): Boolean { - val clipboards = schematicCache[block.type] ?: return false - return pasteSchematic(block, clipboards.random()) - } - - /** - * Paste a schematic at the specified block. - * @param block The block to paste the schematic at. - * @param clipboard The specific clipboard to paste. - * @return True if the schematic was pasted successfully. - */ - private fun pasteSchematic( - block: Block, - clipboard: Clipboard, - ): Boolean { - instance.server.scheduler.runTask( - instance, - Runnable { - try { - WorldEdit - .getInstance() - .newEditSession(BukkitAdapter.adapt(block.world)) - .use { editSession -> - block.type = Material.AIR - editSession.mask = - BlockTypeMask( - editSession, - config.treesModule.treeMask.map { BukkitAdapter.asBlockType(it) }, - ) - ClipboardHolder(clipboard).apply { - transform = transform.combine(AffineTransform().rotateY(getRandomRotation().toDouble())) - Operations.complete( - createPaste(editSession) - .to(BlockVector3.at(block.x, block.y, block.z)) - .copyBiomes(config.treesModule.copyBiomes) - .copyEntities(config.treesModule.copyEntities) - .ignoreAirBlocks(config.treesModule.ignoreAirBlocks) - .ignoreStructureVoidBlocks(config.treesModule.ignoreStructureVoidBlocks) - .build(), - ) - } - } - } catch (e: Exception) { - instance.logger.severe("Error while pasting schematic: ${e.message}") - } - }, - ) - return true - } - - /** - * Returns a random rotation angle from a given list of angles. - * @param angle The list of angles to choose from. Defaults to [0, 90, 180, 270]. - * @return A random angle from the provided or default list. - * @throws IllegalArgumentException if any angle is not a multiple of 90 or outside [0, 270) - */ - private fun getRandomRotation(angle: List = listOf(0, 90, 180, 270)): Int { - require(angle.all { it in setOf(0, 90, 180, 270) }) { "Angles must be one of: 0, 90, 180, 270" } - return angle.random() - } - - @Serializable - data class Config( - var enabled: Boolean = true, - var copyBiomes: Boolean = false, - var copyEntities: Boolean = false, - var ignoreAirBlocks: Boolean = true, - var ignoreStructureVoidBlocks: Boolean = true, - var treeMask: Set = - setOf( - Material.AZALEA, - Material.WEEPING_VINES, - Material.CORNFLOWER, - Material.CLOSED_EYEBLOSSOM, - Material.PINK_TULIP, - Material.OPEN_EYEBLOSSOM, - Material.WHITE_TULIP, - Material.SNOW, - Material.FERN, - Material.AZALEA_LEAVES, - Material.SUNFLOWER, - Material.PEONY, - Material.PINK_PETALS, - Material.LILAC, - Material.LARGE_FERN, - Material.VINE, - Material.CAVE_VINES_PLANT, - Material.TORCHFLOWER, - Material.RED_TULIP, - Material.ORANGE_TULIP, - Material.KELP, - Material.AIR, - Material.FLOWERING_AZALEA, - Material.AZURE_BLUET, - Material.MOSS_BLOCK, - Material.PITCHER_PLANT, - Material.WEEPING_VINES_PLANT, - Material.TALL_SEAGRASS, - Material.TWISTING_VINES, - Material.BLUE_ORCHID, - Material.CAVE_VINES, - Material.ROSE_BUSH, - Material.SPORE_BLOSSOM, - Material.FLOWERING_AZALEA_LEAVES, - Material.POPPY, - Material.TWISTING_VINES_PLANT, - Material.DANDELION, - Material.DEAD_BUSH, - Material.LILY_OF_THE_VALLEY, - Material.KELP_PLANT, - Material.SHORT_GRASS, - Material.CHORUS_FLOWER, - Material.ALLIUM, - Material.MANGROVE_PROPAGULE, - Material.CHERRY_LEAVES, - Material.SUGAR_CANE, - Material.SEAGRASS, - Material.MOSS_CARPET, - Material.WITHER_ROSE, - Material.TALL_GRASS, - Material.OXEYE_DAISY, - ), - ) -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt b/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt index c4146c5bb..cd27d8474 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt @@ -19,12 +19,12 @@ internal object PlayerPDC { /** * Gets or sets the player's nickname in their persistent data container. * @receiver The player whose nickname to access. - * @return The player's nickname, or null if not set. + * @return The player's nickname, or their actual name if no nickname is set. */ - var Player.nickname: String? - get() = persistentDataContainer.get(NICKNAME_KEY, PersistentDataType.STRING) + var Player.nickname: String + get() = persistentDataContainer.get(NICKNAME_KEY, PersistentDataType.STRING) ?: name set(value) { - if (value.isNullOrEmpty()) { + if (value.isBlank()) { persistentDataContainer.remove(NICKNAME_KEY) } else { persistentDataContainer.set(NICKNAME_KEY, PersistentDataType.STRING, value) @@ -34,15 +34,9 @@ internal object PlayerPDC { /** * Gets or sets the player's scoreboard visibility preference in their persistent data container. * @receiver The player whose scoreboard visibility to access. - * @return True if the scoreboard is visible, false if hidden, or null if not set. + * @return True if the scoreboard is visible, false otherwise. */ - var Player.scoreboardVisibility: Boolean? - get() = persistentDataContainer.get(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN) - set(value) { - if (value == null) { - persistentDataContainer.remove(SCOREBOARD_VISIBILITY_KEY) - } else { - persistentDataContainer.set(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN, value) - } - } + var Player.scoreboardVisibility: Boolean + get() = persistentDataContainer.get(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN) ?: false + set(value) = persistentDataContainer.set(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN, value) } diff --git a/src/main/kotlin/org/xodium/vanillaplus/recipes/PaintingRecipe.kt b/src/main/kotlin/org/xodium/vanillaplus/recipes/PaintingRecipe.kt new file mode 100644 index 000000000..4937d4937 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/recipes/PaintingRecipe.kt @@ -0,0 +1,34 @@ +package org.xodium.vanillaplus.recipes + +import io.papermc.paper.datacomponent.DataComponentTypes +import io.papermc.paper.registry.RegistryAccess +import io.papermc.paper.registry.RegistryKey +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.StonecuttingRecipe +import org.xodium.vanillaplus.VanillaPlus.Companion.instance +import org.xodium.vanillaplus.interfaces.RecipeInterface + +/** Represents an object handling painting recipe implementation within the system. */ +internal object PaintingRecipe : RecipeInterface { + override val recipes = + buildSet { + val paintingRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.PAINTING_VARIANT) + + paintingRegistry.forEach { variant -> + val variantKey = paintingRegistry.getKey(variant) ?: return@forEach + + add( + StonecuttingRecipe( + NamespacedKey(instance, "painting_${variantKey.value().replace(':', '_')}"), + @Suppress("UnstableApiUsage") + ItemStack.of(Material.PAINTING).apply { + setData(DataComponentTypes.PAINTING_VARIANT, variant) + }, + Material.PAINTING, + ), + ) + } + } +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/recipes/TorchArrowRecipe.kt b/src/main/kotlin/org/xodium/vanillaplus/recipes/TorchArrowRecipe.kt deleted file mode 100644 index 59159d0c3..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/recipes/TorchArrowRecipe.kt +++ /dev/null @@ -1,87 +0,0 @@ -package org.xodium.vanillaplus.recipes - -import io.papermc.paper.datacomponent.DataComponentTypes -import org.bukkit.Color -import org.bukkit.Material -import org.bukkit.NamespacedKey -import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.ShapelessRecipe -import org.bukkit.persistence.PersistentDataType -import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.interfaces.RecipeInterface -import org.xodium.vanillaplus.utils.ExtUtils.mm - -/** Represents an object handling torch arrow recipe implementation within the system. */ -internal object TorchArrowRecipe : RecipeInterface { - /** Namespaced key for identifying torch arrows. */ - val torchArrowKey = NamespacedKey(instance, "torch_arrow") - - /** Represents configuration for a specific torch arrow variant. */ - data class TorchArrowType( - val id: String, - val displayName: String, - val torchMaterial: Material, - val wallTorchMaterial: Material, - val arrowColor: Color, - ) - - /** List of different torch arrow types with their properties. */ - private val torchTypes = - listOf( - TorchArrowType( - "torch", - "Torch Arrow", - Material.TORCH, - Material.WALL_TORCH, - Color.YELLOW, - ), - TorchArrowType( - "soul", - "Soul Torch Arrow", - Material.SOUL_TORCH, - Material.SOUL_WALL_TORCH, - Color.BLUE, - ), - TorchArrowType( - "redstone", - "Redstone Torch Arrow", - Material.REDSTONE_TORCH, - Material.REDSTONE_WALL_TORCH, - Color.RED, - ), - TorchArrowType( - "copper", - "Copper Torch Arrow", - Material.COPPER_TORCH, - Material.COPPER_WALL_TORCH, - Color.ORANGE, - ), - ) - - /** Gets the torch arrow type configuration by ID. */ - fun getTorchArrowTypeById(id: String): TorchArrowType? = torchTypes.find { it.id == id } - - /** - * Creates a torch arrow item stack with the specified type. - * @param type The type of torch arrow to create. - * @return A pair containing the namespaced key and the created item stack. - */ - private fun createTorchArrow(type: TorchArrowType): ItemStack = - ItemStack.of(Material.ARROW).apply { - @Suppress("UnstableApiUsage") - setData(DataComponentTypes.CUSTOM_NAME, type.displayName.mm()) - editPersistentDataContainer { it.set(torchArrowKey, PersistentDataType.STRING, type.id) } - } - - override val recipes = - torchTypes - .map { type -> - ShapelessRecipe( - NamespacedKey(instance, "${type.id}_torch_arrow"), - createTorchArrow(type), - ).apply { - addIngredient(Material.ARROW) - addIngredient(type.torchMaterial) - } - }.toSet() -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/recipes/WoodLogRecipe.kt b/src/main/kotlin/org/xodium/vanillaplus/recipes/WoodLogRecipe.kt index 4615ff6f6..491fb7138 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/recipes/WoodLogRecipe.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/recipes/WoodLogRecipe.kt @@ -42,7 +42,7 @@ internal object WoodLogRecipe : RecipeInterface { .map { (wood, log) -> ShapelessRecipe( NamespacedKey(instance, "${wood.key.key}_to_${log.key.key}"), - ItemStack.of(log), + ItemStack.of(log, 4), ).apply { addIngredient(wood) } }.toSet() } diff --git a/src/main/kotlin/org/xodium/vanillaplus/registries/MaterialRegistry.kt b/src/main/kotlin/org/xodium/vanillaplus/registries/MaterialRegistry.kt deleted file mode 100644 index d8b5fa3c5..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/registries/MaterialRegistry.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.xodium.vanillaplus.registries - -import org.bukkit.Material - -/** Registry for materials. */ -internal object MaterialRegistry { - val SAPLING_LINKS: Map = - mapOf( - Material.ACACIA_SAPLING to "trees/acacia", - Material.BIRCH_SAPLING to "trees/birch", - Material.CHERRY_SAPLING to "trees/cherry", - Material.CRIMSON_FUNGUS to "trees/crimson", - Material.DARK_OAK_SAPLING to "trees/dark_oak", - Material.JUNGLE_SAPLING to "trees/jungle", - Material.MANGROVE_PROPAGULE to "trees/mangrove", - Material.OAK_SAPLING to "trees/oak", - Material.PALE_OAK_SAPLING to "trees/pale_oak", - Material.SPRUCE_SAPLING to "trees/spruce", - Material.WARPED_FUNGUS to "trees/warped", - ) -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt index d4359578d..3b76aa9da 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -11,21 +11,20 @@ internal object BlockUtils { * Get the centre of a block, handling double chests properly. * @return The centre location of the block. */ - val Block.center: Location - get() { - val baseAddition = Location(location.world, location.x + 0.5, location.y + 0.5, location.z + 0.5) - val chestState = state as? Chest ?: return baseAddition - val holder = chestState.inventory.holder as? DoubleChest ?: return baseAddition - val leftBlock = (holder.leftSide as? Chest)?.block - val rightBlock = (holder.rightSide as? Chest)?.block + fun Block.center(): Location { + val baseAddition = Location(location.world, location.x + 0.5, location.y + 0.5, location.z + 0.5) + val chestState = state as? Chest ?: return baseAddition + val holder = chestState.inventory.holder as? DoubleChest ?: return baseAddition + val leftBlock = (holder.leftSide as? Chest)?.block + val rightBlock = (holder.rightSide as? Chest)?.block - if (leftBlock == null || rightBlock == null || leftBlock.world !== rightBlock.world) return baseAddition + if (leftBlock == null || rightBlock == null || leftBlock.world !== rightBlock.world) return baseAddition - val world = leftBlock.world - val cx = (leftBlock.x + rightBlock.x) / 2.0 + 0.5 - val cy = (leftBlock.y + rightBlock.y) / 2.0 + 0.5 - val cz = (leftBlock.z + rightBlock.z) / 2.0 + 0.5 + val world = leftBlock.world + val cx = (leftBlock.x + rightBlock.x) / 2.0 + 0.5 + val cy = (leftBlock.y + rightBlock.y) / 2.0 + 0.5 + val cz = (leftBlock.z + rightBlock.z) / 2.0 + 0.5 - return Location(world, cx, cy, cz) - } + return Location(world, cx, cy, cz) + } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/CommandUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/CommandUtils.kt index 4a3d4fed3..5e2b23071 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/CommandUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/CommandUtils.kt @@ -6,9 +6,10 @@ import com.mojang.brigadier.context.CommandContext import io.papermc.paper.command.brigadier.CommandSourceStack import org.bukkit.entity.Player import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.utils.ExtUtils.mm -import org.xodium.vanillaplus.utils.ExtUtils.prefix +import org.xodium.vanillaplus.utils.Utils.MM +import org.xodium.vanillaplus.utils.Utils.prefix +/** Utility functions for command handling. */ internal object CommandUtils { /** * Registers a command execution handler with an automatic try/catch handling. @@ -29,7 +30,7 @@ internal object CommandUtils { """.trimIndent(), ) (ctx.source.sender as? Player)?.sendMessage( - "${instance.prefix} An error has occurred. Check server logs for details.".mm(), + MM.deserialize("${instance.prefix} An error has occurred. Check server logs for details."), ) } Command.SINGLE_SUCCESS diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt deleted file mode 100644 index 7427acbc9..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt +++ /dev/null @@ -1,146 +0,0 @@ -@file:Suppress("ktlint:standard:no-wildcard-imports") - -package org.xodium.vanillaplus.utils - -import com.google.gson.JsonParser -import io.papermc.paper.registry.TypedKey -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver -import org.bukkit.enchantments.Enchantment -import org.bukkit.entity.Player -import org.xodium.vanillaplus.VanillaPlus -import java.net.URI -import java.util.* -import javax.imageio.ImageIO - -/** Extension utilities. */ -internal object ExtUtils { - private val MM: MiniMessage = MiniMessage.miniMessage() - - private const val FACE_X = 8 - private const val FACE_Y = 8 - private const val FACE_WIDTH = 8 - private const val FACE_HEIGHT = 8 - private const val MAX_COORDINATE = 7 - private const val COLOR_MASK = 0xFF - private const val BLACK_COLOR = "#000000" - private const val PIXEL_CHAR = "█" - private const val ALPHA_SHIFT = 24 - private const val RED_SHIFT = 16 - private const val GREEN_SHIFT = 8 - - /** The standardized prefix for [VanillaPlus] messages. */ - val VanillaPlus.prefix: String - get() = - "[" + - "${this.javaClass.simpleName}" + - "]" - - /** - * Deserializes a [MiniMessage] [String] into a [Component]. - * @param resolvers Optional tag resolvers for custom tags. - * @return The deserialized [Component]. - */ - fun String.mm(vararg resolvers: TagResolver): Component = - if (resolvers.isEmpty()) { - MM.deserialize(this) - } else { - MM.deserialize(this, TagResolver.resolver(*resolvers)) - } - - /** - * Deserializes an iterable collection of [MiniMessage] strings into a list of Components. - * @param resolvers Optional tag resolvers for custom tags. - * @return The list of deserialized Components. - */ - @JvmName("mmStringIterable") - fun Iterable.mm(vararg resolvers: TagResolver): List = map { it.mm(*resolvers) } - - /** - * Performs a command from a [String]. - * @param cmd The command to perform. - * @param hover Optional hover text for the command. Defaults to "Click me!". - * @return The formatted [String] with the command. - */ - fun String.clickRunCmd( - cmd: String, - hover: String? = "Click me!", // FIX - ): String = "$this" - - /** - * Suggests a command from a [String]. - * @param cmd The command to suggest. - * @param hover Optional hover text for the command. Defaults to "Click me!". - * @return The formatted [String] with the suggested command. - */ - fun String.clickSuggestCmd( - cmd: String, - hover: String? = "Click me!", // FIX - ): String = "$this" - - /** - * Opens a URL from a [String]. - * @param url The URL to open. - * @param hover Optional hover text for the URL. Defaults to "Click me!". - * @return The formatted [String] with the URL. - */ - fun String.clickOpenUrl( - url: String, - hover: String? = "Click me!", // FIX - ): String = "$this" - - /** - * Retrieves the player's face as a string. - * @param size The size of the face in pixels (default is 8). - * @return A string representing the player's face. - */ - fun Player.face(size: Int = 8): String { - // 1. fetch skin URL from the playerProfile - val texturesProp = - playerProfile.properties - .firstOrNull { it.name == "textures" } - ?: error("Player has no skin texture") - val json = JsonParser.parseString(String(Base64.getDecoder().decode(texturesProp.value))).asJsonObject - val skinUrl = - json - .getAsJsonObject("textures") - .getAsJsonObject("SKIN") - .get("url") - .asString - - // 2. load and crop - val fullImg = ImageIO.read(URI.create(skinUrl).toURL()) ?: error("Failed to load skin image from URL: $skinUrl") - val face = fullImg.getSubimage(FACE_X, FACE_Y, FACE_WIDTH, FACE_HEIGHT) - - // 3. scale & build MiniMessage - val scale = FACE_WIDTH.toDouble() / size - val builder = StringBuilder() - - for (y in 0 until size) { - for (x in 0 until size) { - val px = (x * scale).toInt().coerceAtMost(MAX_COORDINATE) - val py = (y * scale).toInt().coerceAtMost(MAX_COORDINATE) - val rgb = face.getRGB(px, py) - val a = (rgb ushr ALPHA_SHIFT) and COLOR_MASK - val r = (rgb shr RED_SHIFT) and COLOR_MASK - val g = (rgb shr GREEN_SHIFT) and COLOR_MASK - val b = rgb and COLOR_MASK - - if (a == 0) { - builder.append("$PIXEL_CHAR") - } else { - builder.append("$PIXEL_CHAR".format(r, g, b)) - } - } - builder.append("\n") - } - return builder.toString() - } - - /** Extension function to convert snake_case to Proper Case with spaces. */ - fun String.snakeToProperCase(): String = split('_').joinToString(" ") { word -> word.replaceFirstChar { it.uppercase() } } - - /** Extension function specifically for enchantment keys */ - fun TypedKey.displayName(): Component = value().snakeToProperCase().mm() -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt deleted file mode 100644 index 0d1c34b40..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.xodium.vanillaplus.utils - -import org.bukkit.Tag -import org.bukkit.block.ShulkerBox -import org.bukkit.inventory.Inventory -import org.bukkit.inventory.ItemStack - -/** Inventory utilities. */ -internal object InvUtils { - /** - * Transfer items from source to destination inventory. - * @param source The source inventory. - * @param destination The destination inventory. - * @param startSlot The starting slot in source inventory. - * @param endSlot The ending slot in source inventory. - * @param onlyMatching If true, only transfer items that already exist in the destination. - * @param enchantmentChecker Function to check if enchantments match. - * @return Pair - */ - fun transferItems( - source: Inventory, - destination: Inventory, - startSlot: Int = 9, - endSlot: Int = 35, - onlyMatching: Boolean = false, - enchantmentChecker: (ItemStack, ItemStack) -> Boolean = { _, _ -> true }, - ): Boolean { - var moved = false - - for (i in startSlot..endSlot) { - val item = source.getItem(i) ?: continue - - if (!isValidTransfer(item, destination)) continue - if (onlyMatching && !containsMatchingItem(destination, item, enchantmentChecker)) continue - - val leftovers = destination.addItem(item) - val movedAmount = item.amount - leftovers.values.sumOf { it.amount } - - if (movedAmount > 0) { - moved = true - source.clear(i) - leftovers.values.firstOrNull()?.let { source.setItem(i, it) } - } - } - - return moved - } - - /** - * Check if transferring an item would be valid (not putting shulker in shulker, etc.) - * @param item The item to transfer. - * @param destination The destination inventory. - * @return True if the transfer is valid, false otherwise. - */ - private fun isValidTransfer( - item: ItemStack, - destination: Inventory, - ): Boolean = !(Tag.SHULKER_BOXES.isTagged(item.type) && destination.holder is ShulkerBox) - - /** - * Check if inventory contains an item with matching type and enchantments. - * @param inventory The inventory to check. - * @param item The item to match. - * @param enchantmentChecker Function to check enchantment compatibility. - * @return True if a matching item is found. - */ - private fun containsMatchingItem( - inventory: Inventory, - item: ItemStack, - enchantmentChecker: (ItemStack, ItemStack) -> Boolean, - ): Boolean = - inventory.contents - .asSequence() - .filterNotNull() - .any { it.type == item.type && enchantmentChecker(item, it) } -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/PlayerUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/PlayerUtils.kt index 328597147..dab4ceba1 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/PlayerUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/PlayerUtils.kt @@ -2,20 +2,88 @@ package org.xodium.vanillaplus.utils +import com.google.gson.JsonParser +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor import org.bukkit.Chunk +import org.bukkit.Color import org.bukkit.block.Container import org.bukkit.entity.Player +import org.bukkit.entity.Tameable +import java.net.URI +import javax.imageio.ImageIO +import kotlin.io.encoding.Base64 /** Player utilities. */ internal object PlayerUtils { + private const val FACE_X = 8 + private const val FACE_Y = 8 + private const val FACE_WIDTH = 8 + private const val FACE_HEIGHT = 8 + private const val MAX_COORDINATE = 7 + private const val COLOR_MASK = 0xFF + private const val BLACK_COLOR = "#000000" + private const val PIXEL_CHAR = "█" + private const val ALPHA_SHIFT = 24 + private const val RED_SHIFT = 16 + private const val GREEN_SHIFT = 8 + /** - * Get containers around a player (3x3 area). - * @param player The player. + * Retrieves the player's face as a string. + * @param size The size of the face in pixels (default is 8). + * @return A string representing the player's face. + */ + fun Player.face(size: Int = 8): String { + // 1. fetch skin URL from the playerProfile + val texturesProp = + playerProfile.properties + .firstOrNull { it.name == "textures" } + ?: error("Player has no skin texture") + val json = JsonParser.parseString(Base64.decode(texturesProp.value).decodeToString()).asJsonObject + val skinUrl = + json + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url") + .asString + + // 2. load and crop + val fullImg = ImageIO.read(URI.create(skinUrl).toURL()) ?: error("Failed to load skin image from URL: $skinUrl") + val face = fullImg.getSubimage(FACE_X, FACE_Y, FACE_WIDTH, FACE_HEIGHT) + + // 3. scale & build MiniMessage + val scale = FACE_WIDTH.toDouble() / size + val builder = StringBuilder() + + for (y in 0 until size) { + for (x in 0 until size) { + val px = (x * scale).toInt().coerceAtMost(MAX_COORDINATE) + val py = (y * scale).toInt().coerceAtMost(MAX_COORDINATE) + val rgb = face.getRGB(px, py) + val a = (rgb ushr ALPHA_SHIFT) and COLOR_MASK + val r = (rgb shr RED_SHIFT) and COLOR_MASK + val g = (rgb shr GREEN_SHIFT) and COLOR_MASK + val b = rgb and COLOR_MASK + + if (a == 0) { + builder.append("$PIXEL_CHAR") + } else { + builder.append("$PIXEL_CHAR".format(r, g, b)) + } + } + builder.append("\n") + } + return builder.toString() + } + + /** + * Get containers around this player (3x3 chunk area). + * @receiver The player. * @return Collection of containers around the player. */ - fun getContainersAroundPlayer(player: Player): Set = + fun Player.getContainersAround(): Set = buildSet { - for (chunk in getChunksAroundPlayer(player)) { + for (chunk in getChunksAround()) { for (state in chunk.tileEntities) { if (state is Container) add(state) } @@ -23,19 +91,40 @@ internal object PlayerUtils { } /** - * Get chunks around a player (3x3 area). - * @param player The player. + * Get chunks around this player (3x3 chunk area). + * @receiver The player. * @return Collection of chunks around the player. */ - fun getChunksAroundPlayer(player: Player): Set { - val (baseX, baseZ) = player.location.chunk.run { x to z } + fun Player.getChunksAround(): Set { + val (baseX, baseZ) = location.chunk.run { x to z } return buildSet { for (x in -1..1) { for (z in -1..1) { - add(player.world.getChunkAt(baseX + x, baseZ + z)) + add(world.getChunkAt(baseX + x, baseZ + z)) } } } } + + /** + * Gets the first leashed entity owned by the player within the config radius. + * @receiver The player whose leashed entity is to be found. + * @param radius The radius within which to search for leashed entities. + * @return The found tameable entity or `null` if none exists. + */ + fun Player.getLeashedEntity(radius: Double = 10.0): Tameable? = + getNearbyEntities(radius, radius, radius) + .filterIsInstance() + .firstOrNull { it.isLeashed && it.leashHolder == player } + + /** + * Modifies the colour of a player's waypoint based on the specified parameters. + * @receiver Player The player whose waypoint colour is to be modified. + * @param color The optional named colour to apply to the waypoint. + */ + fun Player.locator(color: TextColor? = null) { + waypointColor = color?.let { Color.fromRGB(it.value()) } + sendActionBar(Component.text("Locator color changed!", color)) + } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/Utils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/Utils.kt new file mode 100644 index 000000000..42d08475e --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/Utils.kt @@ -0,0 +1,46 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + +package org.xodium.vanillaplus.utils + +import io.papermc.paper.registry.TypedKey +import kotlinx.serialization.Serializable +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.enchantments.Enchantment +import org.xodium.vanillaplus.VanillaPlus + +/** General utilities. */ +internal object Utils { + /** MiniMessage instance for parsing formatted strings. */ + val MM: MiniMessage = MiniMessage.miniMessage() + + /** The standardized prefix for [VanillaPlus] messages. */ + val VanillaPlus.prefix: String + get() = + "[" + + "${this.javaClass.simpleName}" + + "]" + + /** Extension function to convert snake_case to Proper Case with spaces. */ + fun String.snakeToProperCase(): String = + split('_').joinToString(" ") { word -> word.replaceFirstChar { it.uppercase() } } + + /** Extension function specifically for enchantment keys */ + fun TypedKey.displayName(): Component = MM.deserialize(value().snakeToProperCase()) + + /** + * Data class representing a numerical range with minimum and maximum values. + * @property min The minimum value of the range. + * @property max The maximum value of the range. + * @throws IllegalArgumentException if [min] is greater than [max]. + */ + @Serializable + data class Range( + val min: Double, + val max: Double, + ) { + init { + require(min <= max) { "min must be less than or equal to max" } + } + } +} diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_01.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_01.schem deleted file mode 100644 index 45d8146b3..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_02.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_02.schem deleted file mode 100644 index 28ce73989..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_03.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_03.schem deleted file mode 100644 index ef95581e7..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_04.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_04.schem deleted file mode 100644 index 7e110b05d..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_05.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_05.schem deleted file mode 100644 index 9266c75f1..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_06.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_06.schem deleted file mode 100644 index ebd28d517..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_07.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_07.schem deleted file mode 100644 index 6b4452a84..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_08.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_08.schem deleted file mode 100644 index 276ec0e60..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_09.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_09.schem deleted file mode 100644 index b38ba9b72..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/acacia/AcaciaTree_10.schem b/src/main/resources/schematics/trees/acacia/AcaciaTree_10.schem deleted file mode 100644 index 0d7ef70e0..000000000 Binary files a/src/main/resources/schematics/trees/acacia/AcaciaTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_01.schem b/src/main/resources/schematics/trees/birch/BirchTree_01.schem deleted file mode 100644 index 54f6914fc..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_02.schem b/src/main/resources/schematics/trees/birch/BirchTree_02.schem deleted file mode 100644 index e8602a2e6..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_03.schem b/src/main/resources/schematics/trees/birch/BirchTree_03.schem deleted file mode 100644 index 867ba0338..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_04.schem b/src/main/resources/schematics/trees/birch/BirchTree_04.schem deleted file mode 100644 index 3a5e95f1a..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_05.schem b/src/main/resources/schematics/trees/birch/BirchTree_05.schem deleted file mode 100644 index f5e8ec8e2..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_06.schem b/src/main/resources/schematics/trees/birch/BirchTree_06.schem deleted file mode 100644 index 54d771463..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_07.schem b/src/main/resources/schematics/trees/birch/BirchTree_07.schem deleted file mode 100644 index 4388870de..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_08.schem b/src/main/resources/schematics/trees/birch/BirchTree_08.schem deleted file mode 100644 index a148c24e7..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_09.schem b/src/main/resources/schematics/trees/birch/BirchTree_09.schem deleted file mode 100644 index 91177c1bf..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/birch/BirchTree_10.schem b/src/main/resources/schematics/trees/birch/BirchTree_10.schem deleted file mode 100644 index 14bcfdcc0..000000000 Binary files a/src/main/resources/schematics/trees/birch/BirchTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_01.schem b/src/main/resources/schematics/trees/cherry/CherryTree_01.schem deleted file mode 100644 index 32657a397..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_02.schem b/src/main/resources/schematics/trees/cherry/CherryTree_02.schem deleted file mode 100644 index f545721ae..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_03.schem b/src/main/resources/schematics/trees/cherry/CherryTree_03.schem deleted file mode 100644 index b776e1dfb..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_04.schem b/src/main/resources/schematics/trees/cherry/CherryTree_04.schem deleted file mode 100644 index b4de363b1..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_05.schem b/src/main/resources/schematics/trees/cherry/CherryTree_05.schem deleted file mode 100644 index d33f211d5..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_06.schem b/src/main/resources/schematics/trees/cherry/CherryTree_06.schem deleted file mode 100644 index 9c97a15c4..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_07.schem b/src/main/resources/schematics/trees/cherry/CherryTree_07.schem deleted file mode 100644 index d1f4621da..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_08.schem b/src/main/resources/schematics/trees/cherry/CherryTree_08.schem deleted file mode 100644 index 4f8ccef6b..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_09.schem b/src/main/resources/schematics/trees/cherry/CherryTree_09.schem deleted file mode 100644 index 5ecb2f6b8..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/cherry/CherryTree_10.schem b/src/main/resources/schematics/trees/cherry/CherryTree_10.schem deleted file mode 100644 index c23ce5095..000000000 Binary files a/src/main/resources/schematics/trees/cherry/CherryTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_01.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_01.schem deleted file mode 100644 index 13a0a3d87..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_02.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_02.schem deleted file mode 100644 index b1b100200..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_03.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_03.schem deleted file mode 100644 index bf1f55739..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_04.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_04.schem deleted file mode 100644 index 57fa3a439..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_05.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_05.schem deleted file mode 100644 index 54ba49e7b..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_06.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_06.schem deleted file mode 100644 index 10cb4bcce..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_07.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_07.schem deleted file mode 100644 index d6c877e30..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_08.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_08.schem deleted file mode 100644 index be63153f9..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_09.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_09.schem deleted file mode 100644 index 0f6200d04..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/crimson/CrimsonTree_10.schem b/src/main/resources/schematics/trees/crimson/CrimsonTree_10.schem deleted file mode 100644 index 786b46a6d..000000000 Binary files a/src/main/resources/schematics/trees/crimson/CrimsonTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_01.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_01.schem deleted file mode 100644 index 8d269dd41..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_02.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_02.schem deleted file mode 100644 index ba8355289..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_03.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_03.schem deleted file mode 100644 index 8950695b0..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_04.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_04.schem deleted file mode 100644 index dc7eec003..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_05.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_05.schem deleted file mode 100644 index bcc6360f6..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_06.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_06.schem deleted file mode 100644 index 07f971f84..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_07.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_07.schem deleted file mode 100644 index 7ca61aabe..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_08.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_08.schem deleted file mode 100644 index 1c7d74527..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_09.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_09.schem deleted file mode 100644 index b57fe5eca..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_10.schem b/src/main/resources/schematics/trees/dark_oak/DarkOakTree_10.schem deleted file mode 100644 index d3e4090ba..000000000 Binary files a/src/main/resources/schematics/trees/dark_oak/DarkOakTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_01.schem b/src/main/resources/schematics/trees/jungle/JungleTree_01.schem deleted file mode 100644 index 329d7999e..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_02.schem b/src/main/resources/schematics/trees/jungle/JungleTree_02.schem deleted file mode 100644 index 458ea7168..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_03.schem b/src/main/resources/schematics/trees/jungle/JungleTree_03.schem deleted file mode 100644 index 194e26882..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_04.schem b/src/main/resources/schematics/trees/jungle/JungleTree_04.schem deleted file mode 100644 index 6795c0f5d..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_05.schem b/src/main/resources/schematics/trees/jungle/JungleTree_05.schem deleted file mode 100644 index a28e7ebf0..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_06.schem b/src/main/resources/schematics/trees/jungle/JungleTree_06.schem deleted file mode 100644 index 8a7fe42e7..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_07.schem b/src/main/resources/schematics/trees/jungle/JungleTree_07.schem deleted file mode 100644 index 85fae7c1a..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_08.schem b/src/main/resources/schematics/trees/jungle/JungleTree_08.schem deleted file mode 100644 index 7f318e6e4..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_09.schem b/src/main/resources/schematics/trees/jungle/JungleTree_09.schem deleted file mode 100644 index 71ee63ac8..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/jungle/JungleTree_10.schem b/src/main/resources/schematics/trees/jungle/JungleTree_10.schem deleted file mode 100644 index 002983d22..000000000 Binary files a/src/main/resources/schematics/trees/jungle/JungleTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_01.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_01.schem deleted file mode 100644 index 0beb23dd8..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_02.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_02.schem deleted file mode 100644 index fba49146c..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_03.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_03.schem deleted file mode 100644 index 3a3d1c857..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_04.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_04.schem deleted file mode 100644 index 4e90c4f75..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_05.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_05.schem deleted file mode 100644 index c3984cbb2..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_06.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_06.schem deleted file mode 100644 index 56351194f..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_07.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_07.schem deleted file mode 100644 index d23ab1f13..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_08.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_08.schem deleted file mode 100644 index f5a8341e1..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_09.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_09.schem deleted file mode 100644 index 91d0c2629..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/mangrove/MangroveTree_10.schem b/src/main/resources/schematics/trees/mangrove/MangroveTree_10.schem deleted file mode 100644 index d687934bf..000000000 Binary files a/src/main/resources/schematics/trees/mangrove/MangroveTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_01.schem b/src/main/resources/schematics/trees/oak/OakTree_01.schem deleted file mode 100644 index ecee36232..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_02.schem b/src/main/resources/schematics/trees/oak/OakTree_02.schem deleted file mode 100644 index 5f041e27d..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_03.schem b/src/main/resources/schematics/trees/oak/OakTree_03.schem deleted file mode 100644 index 5b39b1d1c..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_04.schem b/src/main/resources/schematics/trees/oak/OakTree_04.schem deleted file mode 100644 index 2f6c9704a..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_05.schem b/src/main/resources/schematics/trees/oak/OakTree_05.schem deleted file mode 100644 index 5eef2560b..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_06.schem b/src/main/resources/schematics/trees/oak/OakTree_06.schem deleted file mode 100644 index 3a28103f7..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_07.schem b/src/main/resources/schematics/trees/oak/OakTree_07.schem deleted file mode 100644 index 89d0062ce..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_08.schem b/src/main/resources/schematics/trees/oak/OakTree_08.schem deleted file mode 100644 index 4107bb0d1..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_09.schem b/src/main/resources/schematics/trees/oak/OakTree_09.schem deleted file mode 100644 index 8fcfcedab..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/oak/OakTree_10.schem b/src/main/resources/schematics/trees/oak/OakTree_10.schem deleted file mode 100644 index 7ee9729a6..000000000 Binary files a/src/main/resources/schematics/trees/oak/OakTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_01.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_01.schem deleted file mode 100644 index e38a71737..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_02.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_02.schem deleted file mode 100644 index 5e6ef0557..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_03.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_03.schem deleted file mode 100644 index 9dad718b4..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_04.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_04.schem deleted file mode 100644 index 441452af6..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_05.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_05.schem deleted file mode 100644 index f545cbda9..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_06.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_06.schem deleted file mode 100644 index 8e8f4f3b3..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_07.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_07.schem deleted file mode 100644 index 3accd3ff6..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_08.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_08.schem deleted file mode 100644 index 4c24f42e3..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_09.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_09.schem deleted file mode 100644 index 32dd609af..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_10.schem b/src/main/resources/schematics/trees/pale_oak/PaleOakTree_10.schem deleted file mode 100644 index 8564f5a7f..000000000 Binary files a/src/main/resources/schematics/trees/pale_oak/PaleOakTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_01.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_01.schem deleted file mode 100644 index 41516a287..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_02.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_02.schem deleted file mode 100644 index 6b107a9c5..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_03.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_03.schem deleted file mode 100644 index f2164d6b7..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_04.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_04.schem deleted file mode 100644 index d088fe5b7..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_05.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_05.schem deleted file mode 100644 index e70044a2d..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_06.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_06.schem deleted file mode 100644 index 0888fab37..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_07.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_07.schem deleted file mode 100644 index 4b1d884bb..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_08.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_08.schem deleted file mode 100644 index 3377990c7..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_09.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_09.schem deleted file mode 100644 index 66cc34071..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/spruce/SpruceTree_10.schem b/src/main/resources/schematics/trees/spruce/SpruceTree_10.schem deleted file mode 100644 index 312e0cc13..000000000 Binary files a/src/main/resources/schematics/trees/spruce/SpruceTree_10.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_01.schem b/src/main/resources/schematics/trees/warped/WarpedTree_01.schem deleted file mode 100644 index 3ea16983b..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_01.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_02.schem b/src/main/resources/schematics/trees/warped/WarpedTree_02.schem deleted file mode 100644 index 6664ada5a..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_02.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_03.schem b/src/main/resources/schematics/trees/warped/WarpedTree_03.schem deleted file mode 100644 index 07a3e03d3..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_03.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_04.schem b/src/main/resources/schematics/trees/warped/WarpedTree_04.schem deleted file mode 100644 index d4cd36f6b..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_04.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_05.schem b/src/main/resources/schematics/trees/warped/WarpedTree_05.schem deleted file mode 100644 index 1f6e7c9e0..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_05.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_06.schem b/src/main/resources/schematics/trees/warped/WarpedTree_06.schem deleted file mode 100644 index a5cd96057..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_06.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_07.schem b/src/main/resources/schematics/trees/warped/WarpedTree_07.schem deleted file mode 100644 index 28b5ad765..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_07.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_08.schem b/src/main/resources/schematics/trees/warped/WarpedTree_08.schem deleted file mode 100644 index f4d76f49a..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_08.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_09.schem b/src/main/resources/schematics/trees/warped/WarpedTree_09.schem deleted file mode 100644 index a1d069b9d..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_09.schem and /dev/null differ diff --git a/src/main/resources/schematics/trees/warped/WarpedTree_10.schem b/src/main/resources/schematics/trees/warped/WarpedTree_10.schem deleted file mode 100644 index 239626ce9..000000000 Binary files a/src/main/resources/schematics/trees/warped/WarpedTree_10.schem and /dev/null differ