diff --git a/.gitignore b/.gitignore index ee2f798..1fbe46b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,6 @@ # Compiled class file *.class -# Eclipse classpath file -*.classpath - -# Eclipse project file -*.project - -# Eclipse settings -*.settings - -# Gradle build output -bin/ - # Log file *.log @@ -33,7 +21,17 @@ bin/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +replay_pid* -# Gradle post-build folders -.gradle +# macOS system files +.DS_Store + +# gradle build output files build/ +.gradle/ + +# eclipse project files +.settings/ +.project +.classpath +bin/ diff --git a/README.md b/README.md index 57e5853..c07e3e0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ To build: The resulting .jar file will be in build/libs/ +*Version 3.0 Changelog:* + +- Setting the age now applies to all items. + +- Added more logic for persistence. + +- Added support for human readable time format. + *Version 2.1 Changelog:* - Fixed NBT-API import in gradle. @@ -44,9 +52,9 @@ The resulting .jar file will be in build/libs/ - Change the amount of time it takes for items to despawn. -- Set in-game warnings for when items are about to despawn. (Coming in 3.0) +- Set in-game warnings for when items are about to despawn. (Coming in 3.1) -- Clear items with an in-game command. (Coming in 3.0) +- Clear items with an in-game command. (Coming in 3.1) Uses [NBTAPI](https://github.com/tr7zw/Item-NBT-API). diff --git a/build.gradle b/build.gradle deleted file mode 100644 index ee94a2a..0000000 --- a/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -plugins { - - id "java" // Tell gradle this is a java project. - id "com.github.johnrengelman.shadow" version "7.1.2" // Import utility to package libraries into .jar file. - -} - -group = "net.trueog.nodespawnog" // Declare package name. -version = "2.1" // Declare package version. - -def apiVersion = "1.19.4" // Declare minecraft server target version. -def targetJavaVersion = 17 // Declare java compatibility minimum. - -apply plugin: "eclipse" // Enable integration with Eclipse IDE for development. - -processResources { - - def props = [ - version: version, - apiVersion: apiVersion, - ] - - inputs.properties props // Indicates to rerun if version changes. - - filesMatching("plugin.yml") { - - expand props - - } - -} - -repositories { - - mavenCentral() - - maven { - - url "https://repo.purpurmc.org/snapshots" // Get purpur API from purpur maven repository. - - } - - maven { - - name = "CodeMC" - url = uri("https://repo.codemc.org/repository/maven-public/") // Get NBT-API from codemc maven repository. - - } - -} - -dependencies { - - compileOnly "org.purpurmc.purpur:purpur-api:1.18.2-R0.1-SNAPSHOT" // Declare purpur API version to be packaged. - - implementation("de.tr7zw:item-nbt-api:2.11.1") // Declare NBT-API version to be packaged. - -} - -shadowJar { - - minimize() - - relocate 'de.tr7zw', 'net.trueog.nodespawnog.item-nbt-api' // Prevent conflicts with other NBT-API utilizing plugins. - -} - -jar.dependsOn shadowJar diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..984faad --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,93 @@ +plugins { + java // Tell gradle this is a java project. + id("io.github.goooler.shadow") version "8.1.8" + eclipse // Import eclipse plugin for IDE integration. + kotlin("jvm") version "1.9.23" // Import kotlin jvm plugin for kotlin/java integration. +} + +java { + // Declare java version. + sourceCompatibility = JavaVersion.VERSION_17 +} + +group = "net.trueog.nodespawnog" // Declare bundle identifier. +version = "3.0" // Declare plugin version (will be in .jar). +val apiVersion = "1.19" // Declare minecraft server target version. + +tasks.named("processResources") { + val props = mapOf( + "version" to version, + "apiVersion" to apiVersion + ) + + inputs.properties(props) // Indicates to rerun if version changes. + + filesMatching("plugin.yml") { + expand(props) + } +} + +repositories { + mavenCentral() + + maven { + url = uri("https://repo.purpurmc.org/snapshots") + } + + maven { + + url = uri("https://repo.codemc.org/repository/maven-public/") // Get NBT-API from codemc maven repository. + + } + +} + +dependencies { + compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare purpur API version to be packaged. + compileOnly("io.github.miniplaceholders:miniplaceholders-api:2.2.3") // Import MiniPlaceholders API. + implementation("de.tr7zw:item-nbt-api:2.11.1") // Declare NBT-API version to be packaged. +} + +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true +} + +tasks.shadowJar { + exclude("io.github.miniplaceholders.*") // Exclude the MiniPlaceholders package from being shadowed. + relocate("de.tr7zw", "net.trueog.nodespawnog.item-nbt-api") // Prevent conflicts with other NBT-API utilizing plugins + minimize() +} + +tasks.jar { + dependsOn(tasks.shadowJar) + archiveClassifier.set("part") +} + +tasks.shadowJar { + archiveClassifier.set("") // Use empty string instead of null + from("LICENSE") { + into("/") + } +} + +tasks.jar { + dependsOn("shadowJar") +} + +tasks.withType().configureEach { + options.compilerArgs.add("-parameters") + options.encoding = "UTF-8" + options.isFork = true +} + +kotlin { + jvmToolchain(17) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + vendor = JvmVendorSpec.GRAAL_VM + } +} diff --git a/gradlew b/gradlew index b740cf1..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/settings.gradle b/settings.gradle.kts similarity index 100% rename from settings.gradle rename to settings.gradle.kts diff --git a/src/main/java/net/trueog/nodespawnog/EventListener.java b/src/main/java/net/trueog/nodespawnog/EventListener.java index 5ff6c72..dc488f4 100644 --- a/src/main/java/net/trueog/nodespawnog/EventListener.java +++ b/src/main/java/net/trueog/nodespawnog/EventListener.java @@ -1,13 +1,23 @@ package net.trueog.nodespawnog; +import org.bukkit.Chunk; +import org.bukkit.NamespacedKey; +import org.bukkit.World; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Entity; import org.bukkit.entity.Item; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; -import de.tr7zw.changeme.nbtapi.NBTEntity; +import de.tr7zw.changeme.nbtapi.NBTItem; // Class to watch for in-game events. public class EventListener implements Listener { @@ -18,87 +28,190 @@ public class EventListener implements Listener { // If despawn time is set to this, it never changes. public short minimumValue = -32768; - // Get the user-set death despawn time value. - private short valueOfDespawnTime() { + // Determine the despawn time using the number taken from the config file. + private short calculateDespawnTime() { - // Declare an empty short object for holding the death despawn time from the config file after input validation. - short valueOfDespawnTime = 0; + // Get the user-defined despawn time from the config file. + String timeString = config.getString("time-to-death-despawns"); - // Read config for death despawn in such a way as to validate that it would be a valid 'short'. - try { + // If the config value is empty, do this... + if (timeString == null || timeString.isEmpty()) { - // TODO: Allow for specifying time in seconds, minutes, or hours, instead of ticks. - // Get value for time to death despawns from config file. - int intOfDespawnTime = config.getInt("time-to-death-despawns"); - - // Convert the integer specified by the user to a negative short via a string for data type compliance and error handling. - valueOfDespawnTime = Short.valueOf(String.valueOf(-1 * intOfDespawnTime)); - - // Set short to valid user-set death despawn time. - // This will only run if the short the user specified in the config was in range. - return valueOfDespawnTime; + // Default to 5 minutes (using ticks). + return Short.valueOf(String.valueOf(-1 * 6000)); } - // If the value the user set in the config is out of range, do this. - catch(NumberFormatException error) { - // Return default value of 6000 ticks (5 minutes) to replace invalid user-set value in the config. + // Deconstruct and interpret the user-specified time string. + try { + + // Declare a variable to hold the time. + long time; + + // Extract the unit (s, m, h). + String unit = timeString.replaceAll("\\d", ""); + + // Extract the numeric value. + int value = Integer.parseInt(timeString.replaceAll("\\D", "")); + + switch (unit.toLowerCase()) { + case "s": + // Convert seconds to ticks. + time = value * 20L; + break; + case "m": + // Convert minutes to ticks. + time = value * 20L * 60L; + break; + case "h": + // Convert hours to ticks. + time = value * 20L * 60L * 60L; + break; + default: + // Default to ticks if no unit is provided. + time = value; + } + + // Start the countdown to despawn. + return Short.valueOf(String.valueOf(-1 * time)); + + } + // If the user-specified time string is in an invalid format, do this... + catch (NumberFormatException error) { + + // Default to 5 minutes (using ticks). return Short.valueOf(String.valueOf(-1 * 6000)); } } - // Override entityDeathEvent with the highest possible priority. + // Prioritize this event listener over all other plugins. @EventHandler(priority = EventPriority.HIGHEST) - public void entityDeathEvent(EntityDeathEvent PlayerDeathEvent) { + // Runs whenever an entity dies. + public void entityDeathEvent(EntityDeathEvent event) { - // If a player died, do this. - if (PlayerDeathEvent.getEntity().getType().equals(org.bukkit.entity.EntityType.PLAYER)) { + // Check if the entity is a player. + if (! (event.getEntity() instanceof Player)) { - // If the player's inventory was not empty, do this. - if(! PlayerDeathEvent.getDrops().isEmpty()) { + // If the entity is not a player, skip this event. + return; - // If disable death despawns option is enabled in config file, do this. - if(config.getBoolean("disable-death-despawns") == true) { + } - // For each intended drop item, do this. - PlayerDeathEvent.getDrops().forEach(element -> { + // If the player's inventory was not empty... + if (! event.getDrops().isEmpty()) { - // Get what the item was. - Item droppedItem = PlayerDeathEvent.getEntity().getWorld().dropItem(PlayerDeathEvent.getEntity().getLocation(), element); + // Loop through every item in the death pile. + event.getDrops().forEach(itemStack -> { - // Create a new item drop of the same thing using NBT. - NBTEntity newDropNBT = new NBTEntity(droppedItem); + // Get each item from the dropped entities. + Item droppedItem = event.getEntity().getWorld().dropItem(event.getEntity().getLocation(), itemStack); + NBTItem nbtItem = new NBTItem(itemStack); - // Set items upon death to stay spawned in infinitely - newDropNBT.setShort("Age", minimumValue); + // Set the Age tag based on the value set in the config file. + nbtItem.setShort("Age", config.getBoolean("disable-death-despawns") ? minimumValue : calculateDespawnTime()); - }); + // Save the modified NBT data. + PersistentDataContainer pdc = droppedItem.getPersistentDataContainer(); + NamespacedKey key = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_age"); + pdc.set(key, PersistentDataType.INTEGER, (int) nbtItem.getShort("Age")); - // Delete the original entities to prevent duplication. - PlayerDeathEvent.getDrops().clear(); + // Apply the modified NBTItem back to the ItemStack and update the dropped Item. + itemStack = nbtItem.getItem(); + droppedItem.setItemStack(itemStack); + + }); + + // Clear the original drops. + event.getDrops().clear(); + + } + + } + + // Runs every time a chunk is loaded. + @EventHandler + public void onChunkLoad(ChunkLoadEvent event) { + + // Get the chunk that was loaded. + Chunk chunk = event.getChunk(); + // Loop through every entity in the chunk. + for (Entity entity : chunk.getEntities()) { + + // If the entity is an Item, do this... + if (entity instanceof Item) { + + // Cast the entity to an Item since it is one. + Item item = (Item) entity; + + // Declare a container to hold item data. + PersistentDataContainer pdc = item.getPersistentDataContainer(); + NamespacedKey key = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_age"); + // Check for item age. + if (pdc.has(key, PersistentDataType.INTEGER)) { + + // Derive the item age. + short savedAge = pdc.get(key, PersistentDataType.INTEGER).shortValue(); + + // Get the ItemStack from the Item. + ItemStack itemStack = item.getItemStack(); + + // Apply the saved age using NBTItem. + NBTItem nbtItem = new NBTItem(itemStack); + nbtItem.setShort("Age", savedAge); + itemStack = nbtItem.getItem(); + + // Update the Item with the modified ItemStack. + item.setItemStack(itemStack); } - // If disable death despawns is disabled in the config file, do this. - else { - // For each intended drop item, do this. - PlayerDeathEvent.getDrops().forEach(element -> { + } - // Get what the item was. - Item droppedItem = PlayerDeathEvent.getEntity().getWorld().dropItem(PlayerDeathEvent.getEntity().getLocation(), element); + } - // Create a new item drop of the same thing using NBT. - NBTEntity newDropNBT = new NBTEntity(droppedItem); + } - // Set items upon death to stay spawned in for the calculated amount of time. - newDropNBT.setShort("Age", valueOfDespawnTime()); + // Runs every time a world is loaded. + @EventHandler + public void onWorldLoad(WorldLoadEvent event) { - }); + // Get the world that was loaded. + World world = event.getWorld(); + // Loop through all loaded chunks in the world. + for (Chunk chunk : world.getLoadedChunks()) { - // Delete the original entities to prevent duplication. - PlayerDeathEvent.getDrops().clear(); + // Loop through every entity in the chunk. + for (Entity entity : chunk.getEntities()) { + + // If the entity is an Item, do this... + if (entity instanceof Item) { + + // Cast the entity to an Item since it is one. + Item item = (Item) entity; + + // Declare a container to hold item data. + PersistentDataContainer pdc = item.getPersistentDataContainer(); + NamespacedKey key = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_age"); + // Check for item age. + if (pdc.has(key, PersistentDataType.INTEGER)) { + + // Derive the item age. + short savedAge = pdc.get(key, PersistentDataType.INTEGER).shortValue(); + + // Get the ItemStack from the Item. + ItemStack itemStack = item.getItemStack(); + + // Apply the saved age using NBTItem. + NBTItem nbtItem = new NBTItem(itemStack); + nbtItem.setShort("Age", savedAge); + itemStack = nbtItem.getItem(); + + // Update the Item with the modified ItemStack. + item.setItemStack(itemStack); + + } } diff --git a/src/main/java/net/trueog/nodespawnog/NoDespawnOG.java b/src/main/java/net/trueog/nodespawnog/NoDespawnOG.java index 7b62c72..1755804 100644 --- a/src/main/java/net/trueog/nodespawnog/NoDespawnOG.java +++ b/src/main/java/net/trueog/nodespawnog/NoDespawnOG.java @@ -23,7 +23,7 @@ public class NoDespawnOG extends JavaPlugin { saveDefaultConfig(); // Log valid plugin launch to console. - getLogger().info("NoDespawn 2.1 Loaded."); + getLogger().info("NoDespawn 2.2 Loaded."); } @@ -31,7 +31,7 @@ public class NoDespawnOG extends JavaPlugin { @Override public void onDisable() { // Log valid plugin shut-down to console. - getLogger().info("NoDespawn 2.1 Shut Down without errors."); + getLogger().info("NoDespawn 2.2 Shut Down without errors."); } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 767112b..6c71d54 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,14 +1,14 @@ -# NoDespawn-OG 2.1 Config. +# NoDespawn-OG 3.0 Config. # Should item despawning be completely disabled on player death? disable-death-despawns: true -# How long should all other entities take to despawn? (in ticks). +# How long should all other entities take to despawn? (in ticks, seconds (s), minutes (m), or hours (h)) # The vanilla behavior is 300 seconds, or 5 Minutes (roughly 6000 ticks). -# This will not impact non-death-pile entities until 3.0. -time-to-despawn: 6000 +# If the format of this option is invalid, NoDespawn-OG will default to 5 minutes. +time-to-despawn: 5m -# If disable despawns is set to false, how long (in ticks) should items dropped on death last until despawn? -# Values range from 0 (instant despawn) to 32767 ticks (roughly 27 minutes). +# If disable despawns is set to false, how long (in ticks, seconds (s), minutes (m), or hours (h)) +# should items dropped on death last until despawn? # If higher than the max or lower than the min, defaults to the value of time-to-despawn above. -time-to-death-despawns: 12000 \ No newline at end of file +time-to-death-despawns: 20m \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4e4665f..9bb8f10 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,5 +3,5 @@ name: NoDespawn-OG authors: [BadPylot, NotAlexNoyle] description: A Spigot plugin that allows you to easily determine when exactly you or others first joined the server! website: https://github.com/NotAlexNoyle/NoDespawn-OG -version: 2.1 -api-version: 1.19 +version: '${version}' +api-version: 1.19 \ No newline at end of file