diff --git a/Ideas.md b/Ideas.md
index 8749e7f..8f20256 100644
--- a/Ideas.md
+++ b/Ideas.md
@@ -1,36 +1,2 @@
-Infinite Item - one that always stays at max stack size.
-
-
-| Action (Event Handler) | Blocks Protected? | Blocks Final? | Notes / Hand Nuance |
-|--------------------------------------------------------------|---------------------------------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| **onCraftItem**
(`CraftItemEvent`) | ✅ result | ✅ ingredients when modifying | Only cancels crafting **Protected** results. For **Final** ingredients it uses a heuristic (`isModifyingCraft`) to see if the recipe would alter the item. |
-| **onSmithingTableUse**
(`PrepareSmithingEvent`) | ✅ result | ✅ base or addition | Smithing always “modifies” the base; both base and addition are checked for **Final**. |
-| **onEnchantItem**
(`EnchantItemEvent`) | ❌ (Protected can be enchanted) | ✅ item | Only checks the single item in the enchanting slot (no off-hand concept). |
-| **onPrepareEnchant**
(`PrepareItemEnchantEvent`) | ❌ | ✅ item | Prevents placing a **Final** item into the table; no hand distinction. |
-| **onAnvilUse**
(`PrepareAnvilEvent`) | ✅ result | ✅ first/second if modifying | Heuristic same as crafting. Cancels creation of **Protected** results and any **Final** input being “modified.” |
-| **onBrew**
(`BrewEvent`) | ✅ ingredient & results | ✅ bottles | Checks the single “ingredient” slot for **Protected**, then all bottle slots for **Final**. |
-| **onBrewingStandFuel**
(`BrewingStandFuelEvent`) | ✅ fuel | ❌ | Only checks the one fuel slot. |
-| **onFurnaceBurn**
(`FurnaceBurnEvent`) | ❌ | ✅ fuel | Only the one fuel slot. |
-| **onFurnaceSmelt**
(`FurnaceSmeltEvent`) | ✅ result | ✅ source | Checks both source (no fuel here) for **Final** and result for **Protected**. |
-| **onSpecialCraft**
(`PrepareItemCraftEvent`) | ✅ result | ✅ various inputs | Covers Loom, Cartography, Grindstone, Stonecutter. Only the specific input slots per inventory type are checked for **Final**. |
-| **onCauldron**
(`CauldronLevelChangeEvent`) | ❌ | ✅ main & off | Explicitly checks both `getItemInMainHand()` **and** `getItemInOffHand()` for **Final**. |
-| **onCommand**
(`PlayerCommandPreprocessEvent`) | ❌ | ✅ main & off | Checks both hands’ items against configured command-regex; if the command could modify a **Final** item in either hand, it’s cancelled. |
-| **onPickUp**
(`EntityPickupItemEvent`) | ✅ non-player only | ❌ | Only blocks non-players from picking up **Protected** items. |
-| **onBlockPlace**
(`BlockPlaceEvent`) | ✅ in-hand item | ❌ | Uses `event.getItemInHand()` (the hand used) to prevent placing **Protected** items. |
-| **onPlayerInteract**
(`PlayerInteractEvent`) | ✅ item | ✅ only when filling bottles | Blocks any use of **Protected** items. For **Final**, only blocks filling a bottle/bucket (checks both material and water target), using `event.getItem()` (hand-sensitive). |
-| **onBucketEmpty**
(`PlayerBucketEmptyEvent`) | ✅ hand item | ✅ hand item | Uses `event.getHand()` to locate the bucket (main vs. off) and blocks emptying either **Protected** or **Final** buckets. |
-| **onBucketFill**
(`PlayerBucketFillEvent`) | ✅ hand item | ✅ hand item | Same as empty: checks bucket in the hand specified by `event.getHand()`. |
-| **onBucketFish**
(`PlayerBucketEntityEvent`) | ✅ original bucket | ✅ original bucket | Uses `event.getOriginalBucket()`, so it covers the bucket used to capture an entity (no main/off distinction here). |
-| **onPlayerInteractEntity**
(`PlayerInteractEntityEvent`) | ✅ hand item | ❌ | Uses `event.getHand()`, but only blocks **Protected** items. |
-| **onItemConsume**
(`PlayerItemConsumeEvent`) | ✅ item | ❌ | Single `event.getItem()`, blocks **Protected** consumables only. |
-| **onBowShoot**
(`EntityShootBowEvent`) | ✅ bow or ammo | ✅ bow | Checks both the bow (main hand item) and the ammo for **Protected**, and stops any **Final** bow use. |
-| **onProjectileLaunch**
(`ProjectileLaunchEvent`) | ✅ held item (incl. projectiles) | ❌ | Looks at `ItemStack` in hand or from the projectile’s `getItem()`. |
-| **onAttack**
(`EntityDamageByEntityEvent`) | ✅ main hand item | ❌ | Only checks `player.getInventory().getItemInMainHand()`. |
-| **onBreakBlock**
(`BlockBreakEvent`) | ✅ main hand item | ❌ | Same: only the main hand tool is checked. |
-| **onDispense**
(`BlockDispenseEvent`) | ✅ dispensed item | ❌ | Does not consider any player hand. |
-| **onItemDamage**
(`PlayerItemDamageEvent`) | ❌ | ✅ item | Prevents durability loss on **Final** items (single item context). |
-| **onItemMend**
(`PlayerItemMendEvent`) | ❌ | ✅ item | Prevents XP-mending of **Final** items. |
-| **onBlockDrop**
(`BlockDropItemEvent`) | ✅ world drops | ❌ | Filters out **Protected** item drops from any block. |
-| **onItemSpawn**
(`ItemSpawnEvent`) | ✅ world spawns | ❌ | Cancels spawning of **Protected** items from non-player sources. |
-| **onInventoryClick**
(`InventoryClickEvent`) | ✅ protected in trade result | ❌ | Only blocks taking **Protected** items from a villager “RESULT” slot (no check on **Final**). |
-| **onInventoryMove**
(`InventoryMoveItemEvent`) | ✅ item | ❌ | Prevents hoppers/droppers from moving **Protected** items only. |
+match based on certain features such as enchant, trim, lore string, or name.
+on death, drop any items stored in chest GUI.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index d936c4f..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,61 +0,0 @@
-plugins {
- id 'java'
- id("xyz.jpenilla.run-paper") version "2.3.1"
- id 'com.gradleup.shadow' version '9.0.0-beta10'
-}
-
-group = 'me.trouper'
-version = '0.0.1'
-
-repositories {
- mavenCentral()
- mavenLocal()
- maven {
- name = "papermc-repo"
- url = "https://repo.papermc.io/repository/maven-public/"
- }
- maven {
- name = "sonatype"
- url = "https://oss.sonatype.org/content/groups/public/"
- }
-}
-
-dependencies {
- compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT")
- implementation("me.trouper:alias:1.0-1.21.1-SNAPSHOT")
-}
-
-tasks {
- runServer {
- minecraftVersion("1.21.5")
- }
-}
-
-def targetJavaVersion = 21
-java {
- def javaVersion = JavaVersion.toVersion(targetJavaVersion)
- if (JavaVersion.current() < javaVersion) {
- toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
- }
-}
-
-tasks.withType(JavaCompile).configureEach {
- options.encoding = 'UTF-8'
-
- if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
- options.release.set(targetJavaVersion)
- }
-}
-
-processResources {
- def props = [version: version]
- inputs.properties props
- filteringCharset 'UTF-8'
- filesMatching('plugin.yml') {
- expand props
- }
-}
-
-shadowJar {
- //minimize()
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..8bbe11b
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+ `java-library`
+ id("io.papermc.paperweight.userdev") version "2.0.0-beta.17"
+ id("xyz.jpenilla.run-paper") version "2.3.1"
+ id("com.gradleup.shadow") version "9.0.0-rc1"
+}
+
+group = "me.trouper.dupealias"
+version = "0.0.1"
+description = "A powerful dupe plugin with niche features for servers looking to stand out."
+
+java {
+ toolchain.languageVersion = JavaLanguageVersion.of(21)
+}
+
+
+repositories {
+ mavenLocal()
+}
+
+dependencies {
+ paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT")
+ implementation("me.trouper:alias:1.0-1.21.5-SNAPSHOT")
+}
+
+tasks {
+ shadowJar {
+ archiveClassifier.set("")
+ }
+
+ build {
+ dependsOn(shadowJar)
+ }
+
+ compileJava {
+ options.release = 21
+ }
+ javadoc {
+ options.encoding = Charsets.UTF_8.name()
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index e69de29..20c6ff6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configuration-cache=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e644113..1b33c55 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 a441313..ca025c8 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-8.8-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
old mode 100755
new mode 100644
index b740cf1..23d15a9
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -112,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -203,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 25da30d..db3a6ac 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -68,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+set CLASSPATH=
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index 9f982d3..0000000
--- a/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'DupeAlias'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..8f66004
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
+}
+
+rootProject.name = "DupeAlias"
diff --git a/src/main/java/me/trouper/dupealias/DupeAlias.java b/src/main/java/me/trouper/dupealias/DupeAlias.java
index f85c0cd..a8ff1be 100644
--- a/src/main/java/me/trouper/dupealias/DupeAlias.java
+++ b/src/main/java/me/trouper/dupealias/DupeAlias.java
@@ -3,9 +3,9 @@ package me.trouper.dupealias;
import me.trouper.alias.AliasContext;
import me.trouper.alias.AliasContextProvider;
import me.trouper.alias.data.Common;
-import me.trouper.dupealias.data.CommonConfig;
-import me.trouper.dupealias.data.DupeConfig;
-import me.trouper.dupealias.data.PlayerData;
+import me.trouper.dupealias.data.files.CommonConfig;
+import me.trouper.dupealias.data.files.DupeConfig;
+import me.trouper.dupealias.data.files.NBTStorage;
import me.trouper.dupealias.server.DupeManager;
import org.bukkit.plugin.java.JavaPlugin;
@@ -29,7 +29,7 @@ public final class DupeAlias extends JavaPlugin {
alias.initialize();
alias.getDataManager().load(CommonConfig.class).save();
alias.getDataManager().load(DupeConfig.class).save();
- alias.getDataManager().load(PlayerData.class).save();
+ alias.getDataManager().load(NBTStorage.class).save();
updateCommon();
dupe = new DupeManager();
@@ -39,7 +39,7 @@ public final class DupeAlias extends JavaPlugin {
public void onDisable() {
alias.getDataManager().save(CommonConfig.class);
alias.getDataManager().save(DupeConfig.class);
- alias.getDataManager().save(PlayerData.class);
+ alias.getDataManager().save(NBTStorage.class);
alias.shutdown();
}
diff --git a/src/main/java/me/trouper/dupealias/DupeContext.java b/src/main/java/me/trouper/dupealias/DupeContext.java
index 11b84aa..6d9e53a 100644
--- a/src/main/java/me/trouper/dupealias/DupeContext.java
+++ b/src/main/java/me/trouper/dupealias/DupeContext.java
@@ -2,8 +2,9 @@ package me.trouper.dupealias;
import me.trouper.alias.server.ContextAware;
import me.trouper.alias.server.events.listeners.GuiInputListener;
-import me.trouper.dupealias.data.CommonConfig;
-import me.trouper.dupealias.data.DupeConfig;
+import me.trouper.dupealias.data.files.CommonConfig;
+import me.trouper.dupealias.data.files.DupeConfig;
+import me.trouper.dupealias.data.files.NBTStorage;
import me.trouper.dupealias.server.DupeManager;
import org.bukkit.plugin.java.JavaPlugin;
@@ -24,6 +25,11 @@ public interface DupeContext extends ContextAware {
default DupeConfig getConfig() {
return getDataManager().get(DupeConfig.class);
}
+
+ default NBTStorage getNbtStorage() {
+ return getDataManager().get(NBTStorage.class);
+ }
+
default GuiInputListener getGuiListener() {
return getContext().getGuiInputListener();
}
diff --git a/src/main/java/me/trouper/dupealias/data/GlobalRule.java b/src/main/java/me/trouper/dupealias/data/GlobalRule.java
new file mode 100644
index 0000000..29f7603
--- /dev/null
+++ b/src/main/java/me/trouper/dupealias/data/GlobalRule.java
@@ -0,0 +1,168 @@
+package me.trouper.dupealias.data;
+
+import me.trouper.alias.data.enums.*;
+import me.trouper.alias.utils.misc.MapUtils;
+import me.trouper.dupealias.server.ItemTag;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.Material;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ArmorMeta;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.inventory.meta.trim.ArmorTrim;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class GlobalRule {
+
+ public enum MatchMode {
+ AND, OR, NAND, XOR
+ }
+
+ public enum MaterialMatchMode {
+ WHITELIST,
+ BLACKLIST,
+ IGNORE
+ }
+
+ public MatchMode matchMode = MatchMode.AND;
+ public MaterialMatchMode materialMode = MaterialMatchMode.IGNORE;
+ public Set effectedMaterials = EnumSet.noneOf(Material.class);
+
+ public String nameContainsRegex = "";
+ public String loreContainsRegex = "";
+ public Set legacyModelData = new HashSet<>();
+ public Set itemFlags = EnumSet.noneOf(ItemFlag.class);
+ public Map enchantments = new HashMap<>();
+ public Map potionEffects = new HashMap<>();
+ public Map attributes = new HashMap<>();
+ public Set trimPatterns = EnumSet.noneOf(ValidTrimPattern.class);
+ public Set trimMaterials = EnumSet.noneOf(ValidTrimMaterial.class);
+
+ public Set appliedTags = EnumSet.noneOf(ItemTag.class);
+
+ @SuppressWarnings("deprecation")
+ public boolean doesMatch(ItemStack item) {
+ if (item == null || item.getType() == Material.AIR) return false;
+
+ switch (materialMode) {
+ case WHITELIST -> {
+ if (!effectedMaterials.contains(item.getType())) return false;
+ }
+ case BLACKLIST -> {
+ if (effectedMaterials.contains(item.getType())) return false;
+ }
+ case IGNORE -> {}
+ }
+
+ ItemMeta meta = item.getItemMeta();
+ if (meta == null) return false;
+
+ List results = new ArrayList<>();
+
+ if (!nameContainsRegex.isEmpty()) {
+ Component nameComponent = meta.displayName();
+ Pattern namePattern = safeCompileRegex(nameContainsRegex);
+ String name = nameComponent != null ? LegacyComponentSerializer.legacyAmpersand().serialize(nameComponent) : "";
+ results.add(namePattern.matcher(name).find());
+ }
+
+ if (!loreContainsRegex.isEmpty() && meta.hasLore()) {
+ List lore = meta.lore().stream().map(line-> LegacyComponentSerializer.legacyAmpersand().serialize(line)).toList();
+ Pattern lorePattern = safeCompileRegex(loreContainsRegex);
+ boolean found = !lore.isEmpty() && lore.stream().anyMatch(line -> lorePattern.matcher(line).find());
+ results.add(found);
+ }
+
+ if (!enchantments.isEmpty()) {
+ Map itemEnchants = item.getEnchantments();
+ results.add(MapUtils.allValuesMatch(itemEnchants, enchantments.entrySet().stream().collect(Collectors.toMap(
+ entry -> entry.getKey().getCanonical(),
+ Map.Entry::getValue
+ ))));
+ }
+
+ if (!potionEffects.isEmpty() && meta instanceof PotionMeta potionMeta) {
+ Map itemPotions = potionMeta.getAllEffects().stream()
+ .collect(Collectors.toMap(
+ PotionEffect::getType,
+ PotionEffect::getAmplifier
+ ));
+ results.add(MapUtils.allValuesMatch(itemPotions, potionEffects.entrySet().stream().collect(Collectors.toMap(
+ entry -> entry.getKey().getCanonical(),
+ Map.Entry::getValue
+ ))));
+ }
+
+ if (!attributes.isEmpty() && meta.hasAttributeModifiers() && meta.getAttributeModifiers() != null) {
+ Map itemModifiers = meta.getAttributeModifiers().entries().stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ e -> e.getValue().getAmount()
+ ));
+ results.add(MapUtils.allValuesMatch(itemModifiers, attributes.entrySet().stream().collect(Collectors.toMap(
+ entry -> entry.getKey().getCanonical(),
+ Map.Entry::getValue
+ ))));
+ }
+
+ if (!legacyModelData.isEmpty()) {
+ results.add(meta.hasCustomModelData() && legacyModelData.contains(meta.getCustomModelData()));
+ }
+
+ if (!itemFlags.isEmpty()) {
+ results.add(meta.getItemFlags().containsAll(itemFlags));
+ }
+
+ if (!trimMaterials.isEmpty() && meta instanceof ArmorMeta armorMeta) {
+ ArmorTrim actualTrim = armorMeta.hasTrim() ? armorMeta.getTrim() : null;
+ results.add(actualTrim != null && trimMaterials.stream().anyMatch(material-> material.getCanonical().equals(actualTrim.getMaterial())));
+ }
+
+ if (!trimPatterns.isEmpty() && meta instanceof ArmorMeta armorMeta) {
+ ArmorTrim actualTrim = armorMeta.hasTrim() ? armorMeta.getTrim() : null;
+ results.add(actualTrim != null && trimPatterns.stream().anyMatch(pattern-> pattern.getCanonical().equals(actualTrim.getPattern())));
+ }
+
+ int trueCount = (int) results.stream().filter(Boolean::booleanValue).count();
+ int total = results.size();
+
+ return switch (matchMode) {
+ case AND -> trueCount == total;
+ case OR -> trueCount > 0;
+ case NAND -> trueCount != total;
+ case XOR -> trueCount == 1;
+ };
+ }
+
+ private Pattern safeCompileRegex(String input) {
+ try {
+ return Pattern.compile(input);
+ } catch (Exception e) {
+ return Pattern.compile(Pattern.quote(input));
+ }
+ }
+
+
+ public int getCriteriaCount() {
+ int criteriaCount = 0;
+ if (!nameContainsRegex.isEmpty()) criteriaCount++;
+ if (!loreContainsRegex.isEmpty()) criteriaCount++;
+ if (!enchantments.isEmpty()) criteriaCount++;
+ if (!potionEffects.isEmpty()) criteriaCount++;
+ if (!attributes.isEmpty()) criteriaCount++;
+ if (!itemFlags.isEmpty()) criteriaCount++;
+ if (!legacyModelData.isEmpty()) criteriaCount++;
+ if (!trimPatterns.isEmpty()) criteriaCount++;
+ if (!trimMaterials.isEmpty()) criteriaCount++;
+ return criteriaCount;
+ }
+}
diff --git a/src/main/java/me/trouper/dupealias/data/ItemCapture.java b/src/main/java/me/trouper/dupealias/data/ItemCapture.java
new file mode 100644
index 0000000..c6a1300
--- /dev/null
+++ b/src/main/java/me/trouper/dupealias/data/ItemCapture.java
@@ -0,0 +1,81 @@
+package me.trouper.dupealias.data;
+
+import me.trouper.alias.utils.ItemSimilarity;
+import me.trouper.dupealias.server.ItemTag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.io.ByteArrayInputStream;
+import java.util.*;
+
+public class ItemCapture {
+
+ private String serializedItem;
+ private double similarityThreshold;
+ private final ItemSimilarity.SimilarityConfiguration configuration;
+ private final Map tags;
+
+ public ItemCapture() {
+ this.similarityThreshold = 1;
+ this.configuration = new ItemSimilarity.SimilarityConfiguration();
+ this.tags = new HashMap<>();
+ }
+
+ public ItemCapture(ItemStack stack) {
+ this.serializedItem = serialize(stack);
+ this.similarityThreshold = 1;
+ this.configuration = new ItemSimilarity.SimilarityConfiguration();
+ this.tags = new HashMap<>();
+ }
+
+ private String serialize(ItemStack itemStack) {
+ try {
+ return Base64.getEncoder().encodeToString(itemStack.serializeAsBytes());
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to serialize ItemStack", e);
+ }
+ }
+
+ private ItemStack deserialize(String serializedItemStack) {
+ try {
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(serializedItemStack));
+ byte[] itemBytes = inputStream.readAllBytes();
+ return ItemStack.deserializeBytes(itemBytes);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to deserialize ItemStack", e);
+ }
+ }
+
+ public Map getTags() {
+ return tags;
+ }
+
+ public ItemStack getStack() {
+ return deserialize(serializedItem);
+ }
+
+ public ItemMeta getMeta() {
+ return getStack().getItemMeta();
+ }
+
+ public boolean matches(ItemStack item) {
+ if (similarityThreshold >= 1) return item.isSimilar(getStack());
+ return similarityThreshold <= similarityTo(item);
+ }
+
+ public double similarityTo(ItemStack item) {
+ return ItemSimilarity.calculateSimilarity(item,getStack());
+ }
+
+ public double getThreshold() {
+ return similarityThreshold;
+ }
+
+ public ItemSimilarity.SimilarityConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public void setThreshold(double similarityThreshold) {
+ this.similarityThreshold = similarityThreshold;
+ }
+}
diff --git a/src/main/java/me/trouper/dupealias/data/PlayerData.java b/src/main/java/me/trouper/dupealias/data/PlayerData.java
deleted file mode 100644
index 99e162e..0000000
--- a/src/main/java/me/trouper/dupealias/data/PlayerData.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package me.trouper.dupealias.data;
-
-import me.trouper.alias.data.JsonSerializable;
-import me.trouper.dupealias.DupeContext;
-
-import java.io.File;
-
-public class PlayerData implements JsonSerializable, DupeContext {
- @Override
- public File getFile() {
- return new File(getInstance().getDataFolder(), "playerdata.json");
- }
-}
diff --git a/src/main/java/me/trouper/dupealias/data/CommonConfig.java b/src/main/java/me/trouper/dupealias/data/files/CommonConfig.java
similarity index 96%
rename from src/main/java/me/trouper/dupealias/data/CommonConfig.java
rename to src/main/java/me/trouper/dupealias/data/files/CommonConfig.java
index bcdfc87..e33452f 100644
--- a/src/main/java/me/trouper/dupealias/data/CommonConfig.java
+++ b/src/main/java/me/trouper/dupealias/data/files/CommonConfig.java
@@ -1,4 +1,4 @@
-package me.trouper.dupealias.data;
+package me.trouper.dupealias.data.files;
import me.trouper.alias.data.Common;
import me.trouper.alias.data.JsonSerializable;
diff --git a/src/main/java/me/trouper/dupealias/data/DupeConfig.java b/src/main/java/me/trouper/dupealias/data/files/DupeConfig.java
similarity index 50%
rename from src/main/java/me/trouper/dupealias/data/DupeConfig.java
rename to src/main/java/me/trouper/dupealias/data/files/DupeConfig.java
index 1e463b2..dfb0717 100644
--- a/src/main/java/me/trouper/dupealias/data/DupeConfig.java
+++ b/src/main/java/me/trouper/dupealias/data/files/DupeConfig.java
@@ -1,7 +1,8 @@
-package me.trouper.dupealias.data;
+package me.trouper.dupealias.data.files;
import me.trouper.alias.data.JsonSerializable;
import me.trouper.dupealias.DupeContext;
+import me.trouper.dupealias.data.GlobalRule;
import me.trouper.dupealias.server.ItemTag;
import org.bukkit.Material;
@@ -23,13 +24,36 @@ public class DupeConfig implements JsonSerializable, DupeContext {
"\"(?:itemlore|lore|elore|ilore|eilore|eitemlore)\"gmi"
));
- public Map tagLore = new HashMap<>(Map.of(
+ public Map trueTagLore = new HashMap<>(Map.of(
ItemTag.PROTECTED, "| Protected",
ItemTag.FINAL, "| Final",
ItemTag.UNIQUE, "| Unique",
ItemTag.INFINITE, "| Infinite"
));
+
+ public Map falseTagLore = new HashMap<>(Map.of(
+ ItemTag.PROTECTED, "| Unprotected",
+ ItemTag.FINAL, "| Mutable",
+ ItemTag.UNIQUE, "| Dupeable",
+ ItemTag.INFINITE, "| Finite"
+ ));
+
+ public List globalRules = new ArrayList<>();
+
+ public Replicator replicator = new Replicator();
+ public Chest chest = new Chest();
+ public Inventory inventory = new Inventory();
- public Map> globalMaterials = new HashMap<>();
-
+ public class Replicator {
+ public int baseRefreshDelayTicks = 1;
+ public int baseInputCooldownTicks = 20;
+ }
+
+ public class Chest {
+ public int baseRefreshDelayTicks = 1;
+ }
+
+ public class Inventory {
+ public int baseRefreshDelayTicks = 1;
+ }
}
diff --git a/src/main/java/me/trouper/dupealias/data/files/NBTStorage.java b/src/main/java/me/trouper/dupealias/data/files/NBTStorage.java
new file mode 100644
index 0000000..e0bdbeb
--- /dev/null
+++ b/src/main/java/me/trouper/dupealias/data/files/NBTStorage.java
@@ -0,0 +1,41 @@
+package me.trouper.dupealias.data.files;
+
+import me.trouper.alias.data.JsonSerializable;
+import me.trouper.dupealias.DupeContext;
+import me.trouper.dupealias.data.ItemCapture;
+import org.bukkit.inventory.ItemStack;
+
+import java.io.File;
+import java.util.*;
+
+public class NBTStorage implements JsonSerializable, DupeContext {
+ @Override
+ public File getFile() {
+ return new File(getInstance().getDataFolder(), ".nbtstorage.json");
+ }
+
+ public List captures = new ArrayList<>();
+
+ public ItemCapture getCapture(ItemStack input) {
+ if (getNbtStorage().captures.isEmpty()) return null;
+ ItemCapture match = null;
+ double closest = -1;
+
+ for (ItemCapture capture : getNbtStorage().captures) {
+ boolean isSimilar = capture.getStack().isSimilar(input);
+ if (isSimilar) return capture;
+ double threshold = capture.getThreshold();
+
+ if (threshold >= 1) continue; // Don't bother calculating similarity if the item isn't similar.
+ double sim = capture.similarityTo(input);
+ if (sim >= threshold && sim >= closest) {
+ closest = sim;
+ match = capture;
+ }
+ }
+
+ if (closest == -1) return null;
+
+ return match;
+ }
+}
diff --git a/src/main/java/me/trouper/dupealias/server/DupeManager.java b/src/main/java/me/trouper/dupealias/server/DupeManager.java
index 5d289f7..051ad33 100644
--- a/src/main/java/me/trouper/dupealias/server/DupeManager.java
+++ b/src/main/java/me/trouper/dupealias/server/DupeManager.java
@@ -2,21 +2,51 @@ package me.trouper.dupealias.server;
import me.trouper.alias.utils.ItemBuilder;
import me.trouper.dupealias.DupeContext;
+import me.trouper.dupealias.data.GlobalRule;
+import me.trouper.dupealias.data.ItemCapture;
import me.trouper.dupealias.server.functions.UniqueCheck;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.persistence.PersistentDataType;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
public class DupeManager implements DupeContext {
+ /**
+ * @return false if the item was modified.
+ */
+ public boolean verifyTag(ItemStack item) {
+ if (getNbtStorage().captures.isEmpty()) return true;
+ ItemCapture capture = getNbtStorage().getCapture(item);
+ if (capture == null) return true;
+ boolean modified = false;
+
+ for (Map.Entry tagEntry : capture.getTags().entrySet()) {
+ ItemTag tag = tagEntry.getKey();
+ boolean value = tagEntry.getValue();
+ boolean set = hasIndividualTag(item,tag);
+ if (set && checkIndividualTag(item,tag) == value) {
+ continue;
+ } else if (!set) {
+ setTag(item,tagEntry.getKey(),value);
+ } else {
+ removeTag(item,tag);
+ setTag(item,tag,value);
+ }
+
+ modified = true;
+ }
+
+ return !modified;
+ }
+
public boolean isUnique(ItemStack item) {
return !new UniqueCheck().passes(item);
}
@@ -31,101 +61,202 @@ public class DupeManager implements DupeContext {
return Boolean.TRUE.equals(input.getPersistentDataContainer().get(tag.getKey(), PersistentDataType.BOOLEAN));
}
- public boolean checkGlobalTag(Material material, ItemTag tag) {
- Set tags = getConfig().globalMaterials.getOrDefault(material,new HashSet<>());
- return tags.contains(tag);
- }
-
public boolean checkEffectiveTag(ItemStack input, ItemTag tag) {
if (tag == null || input == null) return false;
if (input.isEmpty()) return false;
+
boolean set = hasIndividualTag(input,tag);
- boolean global = getDupe().checkGlobalTag(input.getType(),tag);
- boolean individual = Boolean.TRUE.equals(input.getPersistentDataContainer().get(tag.getKey(), PersistentDataType.BOOLEAN));
+ boolean individual = set && Boolean.TRUE.equals(input.getPersistentDataContainer().get(tag.getKey(), PersistentDataType.BOOLEAN));
+ // Check individual tag first
if (set) return individual;
- return global;
+
+ // Check global rules
+ return checkGlobalRuleTag(input, tag);
}
- public boolean addGlobalTag(Material material, ItemTag tag) {
- Set tags = getConfig().globalMaterials.getOrDefault(material,new HashSet<>());
- boolean result = tags.add(tag);
- getConfig().globalMaterials.put(material,tags);
- getConfig().save();
- return result;
+ /**
+ * Gets all global rules that apply to a given material
+ */
+ public List getApplicableRules(Material material) {
+ return getConfig().globalRules.stream()
+ .filter(rule -> {
+ return switch (rule.materialMode) {
+ case WHITELIST -> rule.effectedMaterials.contains(material);
+ case BLACKLIST -> !rule.effectedMaterials.contains(material);
+ case IGNORE -> true;
+ default -> false;
+ };
+ })
+ .toList();
}
- public boolean removeGlobalTag(Material material, ItemTag tag) {
- Set tags = getConfig().globalMaterials.getOrDefault(material,new HashSet<>());
- boolean result = tags.remove(tag);
- getConfig().globalMaterials.put(material,tags);
+
+ /**
+ * Gets all global rules that apply to a specific item
+ */
+ public List getMatchingRules(ItemStack item) {
+ return getConfig().globalRules.stream()
+ .filter(rule -> rule.doesMatch(item))
+ .toList();
+ }
+
+ /**
+ * Checks if any global rule applies this tag to the given item
+ */
+ public boolean checkGlobalRuleTag(ItemStack input, ItemTag tag) {
+ for (GlobalRule rule : getConfig().globalRules) {
+ if (rule.appliedTags.contains(tag) && rule.doesMatch(input)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a new global rule that applies the specified tag to items matching the criteria
+ */
+ public GlobalRule createGlobalRule(ItemTag tag) {
+ GlobalRule rule = new GlobalRule();
+ rule.appliedTags.add(tag);
+ getConfig().globalRules.add(rule);
getConfig().save();
- return result;
+ return rule;
+ }
+
+ /**
+ * Removes all global rules that apply the specified tag to the specified material
+ */
+ public boolean removeGlobalRulesForMaterial(Material material, ItemTag tag) {
+ boolean removed = false;
+ Iterator iterator = getConfig().globalRules.iterator();
+
+ while (iterator.hasNext()) {
+ GlobalRule rule = iterator.next();
+ if (rule.appliedTags.contains(tag) &&
+ (rule.materialMode == GlobalRule.MaterialMatchMode.WHITELIST && rule.effectedMaterials.contains(material))) {
+ iterator.remove();
+ removed = true;
+ }
+ }
+
+ if (removed) {
+ getConfig().save();
+ }
+ return removed;
+ }
+
+ /**
+ * Adds a global rule for a specific material and tag
+ */
+ public boolean addGlobalRuleForMaterial(Material material, ItemTag tag) {
+ // Check if rule already exists
+ for (GlobalRule rule : getConfig().globalRules) {
+ if (rule.appliedTags.contains(tag) &&
+ rule.materialMode == GlobalRule.MaterialMatchMode.WHITELIST &&
+ rule.effectedMaterials.contains(material)) {
+ return false; // Rule already exists
+ }
+ }
+
+ GlobalRule rule = new GlobalRule();
+ rule.materialMode = GlobalRule.MaterialMatchMode.WHITELIST;
+ rule.effectedMaterials.add(material);
+ rule.appliedTags.add(tag);
+ getConfig().globalRules.add(rule);
+ getConfig().save();
+ return true;
}
public boolean addTag(ItemStack item, ItemTag tag) {
- if (hasIndividualTag(item,tag) && getDupe().checkIndividualTag(item,tag)) return false;
+ if (hasIndividualTag(item, tag) && getDupe().checkIndividualTag(item, tag)) return false;
+
ItemBuilder builder = ItemBuilder.of(item);
- builder.loreMiniMessage(getConfig().tagLore.get(tag));
+ builder.loreMiniMessage(getConfig().trueTagLore.get(tag));
builder.modifyMeta(itemMeta -> {
- itemMeta.getPersistentDataContainer().set(tag.getKey(), PersistentDataType.BOOLEAN,true);
+ itemMeta.getPersistentDataContainer().set(tag.getKey(), PersistentDataType.BOOLEAN, true);
return itemMeta;
});
ItemStack result = builder.buildAndGet();
-
item.setItemMeta(result.getItemMeta());
return true;
}
public boolean removeTag(ItemStack item, ItemTag tag) {
ItemBuilder builder = ItemBuilder.of(item);
- if (hasIndividualTag(item,tag) && !checkIndividualTag(item,tag)) return false;
+
+ if (hasIndividualTag(item, tag) && !checkIndividualTag(item, tag)) return false;
+
+ builder.modifyMeta(itemMeta->{
+ if (itemMeta.hasLore()) {
+ removeTagLore(itemMeta,tag);
+ }
+
+ return itemMeta;
+ });
+
try {
builder.modifyMeta(itemMeta -> {
itemMeta.getPersistentDataContainer().remove(tag.getKey());
- if (itemMeta.hasLore()) {
- List lore = item.lore();
- if (lore == null) return itemMeta;
- int lines = lore.size();
- for (int i = 0; i < lines - 1; i++) {
- for (Map.Entry entry : getConfig().tagLore.entrySet()) {
- if (tag.equals(entry.getKey())) continue;
- String search = entry.getValue();
- String searchPlain = search.replaceAll("<[^>]+>", "");
- String componentPlain = PlainTextComponentSerializer.plainText().serialize(lore.get(i));
- if (componentPlain.equals(searchPlain)) {
- lore.remove(i);
- break;
- }
- }
- }
- itemMeta.lore(lore);
- }
return itemMeta;
});
} catch (IllegalArgumentException ex) {
return false;
}
- ItemStack result = builder.buildAndGet();
+ ItemStack result = builder.buildAndGet();
item.setItemMeta(result.getItemMeta());
return true;
}
public void setTag(ItemStack item, ItemTag tag, boolean value) {
ItemBuilder builder = ItemBuilder.of(item);
- if (value) builder.loreMiniMessage(getConfig().tagLore.get(tag));
+
builder.modifyMeta(itemMeta -> {
- itemMeta.getPersistentDataContainer().set(tag.getKey(), PersistentDataType.BOOLEAN,value);
+ if (itemMeta.hasLore()) {
+ removeTagLore(itemMeta,tag);
+ }
+ return itemMeta;
+ });
+
+ if (value && getConfig().trueTagLore.containsKey(tag)) {
+ builder.loreMiniMessage(getConfig().trueTagLore.get(tag));
+ } else if (!value && getConfig().falseTagLore.containsKey(tag)) {
+ builder.loreMiniMessage(getConfig().falseTagLore.get(tag));
+ }
+
+ builder.modifyMeta(itemMeta -> {
+ itemMeta.getPersistentDataContainer().set(tag.getKey(), PersistentDataType.BOOLEAN, value);
return itemMeta;
});
ItemStack result = builder.buildAndGet();
-
item.setItemMeta(result.getItemMeta());
}
+ public void removeTagLore(ItemMeta meta, ItemTag tag) {
+ List lore = meta.lore();
+ if (lore != null) {
+ List removeLores = new ArrayList<>();
+ if (getConfig().trueTagLore.containsKey(tag)) {
+ removeLores.add(getConfig().trueTagLore.get(tag));
+ }
+ if (getConfig().falseTagLore.containsKey(tag)) {
+ removeLores.add(getConfig().falseTagLore.get(tag));
+ }
+
+ lore.removeIf(component -> {
+ String componentPlain = PlainTextComponentSerializer.plainText().serialize(component);
+ return removeLores.stream().anyMatch(loreStr ->
+ componentPlain.equals(loreStr.replaceAll("<[^>]+>", ""))
+ );
+ });
+
+ meta.lore(lore);
+ }
+ }
+
public ItemTag getTag(NamespacedKey key) {
for (ItemTag value : ItemTag.values()) {
if (value.getKey().equals(key)) return value;
@@ -133,4 +264,23 @@ public class DupeManager implements DupeContext {
throw new IllegalArgumentException("Invalid NameSpacedKey '%s'".formatted(key.value()));
}
-}
+ public int getPermissionValue(Player player, String rootPermission, int fallback) {
+ int lowestCooldown = Integer.MAX_VALUE;
+
+ for (PermissionAttachmentInfo permInfo : player.getEffectivePermissions()) {
+ String perm = permInfo.getPermission();
+
+ if (perm.startsWith(rootPermission)) {
+ String valueStr = perm.substring(rootPermission.length());
+ try {
+ int value = Integer.parseInt(valueStr);
+ if (value < lowestCooldown) {
+ lowestCooldown = value;
+ }
+ } catch (NumberFormatException ignored) {}
+ }
+ }
+
+ return (lowestCooldown == Integer.MAX_VALUE) ? fallback : lowestCooldown;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java b/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java
index 555742c..0a72cfb 100644
--- a/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java
+++ b/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java
@@ -6,8 +6,10 @@ import me.trouper.alias.server.commands.Permission;
import me.trouper.alias.server.commands.QuickCommand;
import me.trouper.alias.server.commands.completions.CompletionBuilder;
import me.trouper.dupealias.DupeContext;
+import me.trouper.dupealias.data.GlobalRule;
import me.trouper.dupealias.server.ItemTag;
-import me.trouper.dupealias.server.gui.admin.AdminGui;
+import me.trouper.dupealias.server.gui.admin.AdminPanelManager;
+import me.trouper.dupealias.server.gui.admin.MainAdminGui;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -17,7 +19,7 @@ import org.bukkit.inventory.ItemStack;
@CommandRegistry(
value = "dupealias",
permission = @Permission(value = "dupealias.admin", message = "Only server administrators can use this command."),
- usage = "/da [unique|final|infinite|useless] [|global|remove] [remove]",
+ usage = "/da [unique|final|infinite|useless] [|global|remove] [remove]",
blocksAllowed = false,
printStackTrace = true
)
@@ -43,6 +45,10 @@ public class AdminCommand implements QuickCommand, DupeContext {
handleTag(sender,args);
}
+ case "rule" -> {
+ handleRule(sender,args);
+ }
+
default -> {
errorAny(sender,"Invalid subcommand!");
}
@@ -52,78 +58,206 @@ public class AdminCommand implements QuickCommand, DupeContext {
@Override
public void handleCompletion(CommandSender sender, Command command, String label, Args args, CompletionBuilder b) {
quickDebugArgs(b,getCommonConfig().debuggerExclusions.stream().toList())
- .then(
- b.arg("tag")
- .then(b.argEnum(ItemTag.class)
- .then(
- b.argEnum(Material.class)
- .then(
- b.arg("remove") // Global Scope
- )
- ).then(
- b.arg("global")
- .then(
- b.arg("remove") // Global Scope
- )
- ).then(
- b.arg("remove","false")
+ .then(
+ b.arg("tag")
+ .then(b.argEnum(ItemTag.class)
+ .then(
+ b.argEnum(Material.class)
+ .then(
+ b.arg("remove") // Global Scope
+ )
+ ).then(
+ b.arg("global")
+ .then(
+ b.arg("remove") // Global Scope
+ )
+ ).then(
+ b.arg("remove","false")
+ )
)
- )
- ).then(
- b.arg("gui")
- );
+ ).then(
+ b.arg("rule")
+ .then(
+ b.arg("create")
+ .then(
+ b.argEnum(ItemTag.class)
+ )
+ ).then(
+ b.arg("list")
+ )
+ .then(
+ b.arg("remove")
+ .then(
+ b.arg("")
+ )
+ )
+ ).then(
+ b.arg("gui")
+ );
}
private void handleDebug(CommandSender sender, Args args) {
- if (args.getSize() < 2) {
- errorAny(sender, "Usage: debug ");
- return;
- }
+ if (args.getSize() < 2) {
+ errorAny(sender, "Usage: debug ");
+ return;
+ }
- final String sub = args.get(1).toString();
+ final String sub = args.get(1).toString();
- switch (sub) {
- case "toggle" -> {
- boolean result = false;
- getCommonConfig().debugMode = result = !getCommonConfig().debugMode;
- getCommonConfig().save();
+ switch (sub) {
+ case "toggle" -> {
+ boolean result = false;
+ getCommonConfig().debugMode = result = !getCommonConfig().debugMode;
+ getCommonConfig().save();
- getInstance().updateCommon();
+ getInstance().updateCommon();
- successAny(sender,"Toggled debug mode {0}.",result ? "on" : "off");
- }
- case "exclude" -> {
- if (args.getSize() < 3) {
- errorAny(sender, "Usage: debug exclude ");
- return;
- }
- final String exclusion = args.get(2).toString();
- getCommonConfig().debuggerExclusions.add(exclusion);
- getCommonConfig().save();
+ successAny(sender,"Toggled debug mode {0}.",result ? "on" : "off");
+ }
+ case "exclude" -> {
+ if (args.getSize() < 3) {
+ errorAny(sender, "Usage: debug exclude ");
+ return;
+ }
+ final String exclusion = args.get(2).toString();
+ getCommonConfig().debuggerExclusions.add(exclusion);
+ getCommonConfig().save();
- getInstance().updateCommon();
+ getInstance().updateCommon();
- successAny(sender, "Excluded {0} from the debugger.", exclusion);
- }
- case "include" -> {
- if (args.getSize() < 3) {
- errorAny(sender, "Usage: debug include ");
- return;
- }
- final String exclusion = args.get(2).toString();
- getCommonConfig().debuggerExclusions.remove(exclusion);
- getCommonConfig().save();
+ successAny(sender, "Excluded {0} from the debugger.", exclusion);
+ }
+ case "include" -> {
+ if (args.getSize() < 3) {
+ errorAny(sender, "Usage: debug include ");
+ return;
+ }
+ final String exclusion = args.get(2).toString();
+ getCommonConfig().debuggerExclusions.remove(exclusion);
+ getCommonConfig().save();
- getInstance().updateCommon();
+ getInstance().updateCommon();
- successAny(sender, "Removed exclusion for {0} on the debugger.", exclusion);
- }
- }
+ successAny(sender, "Removed exclusion for {0} on the debugger.", exclusion);
+ }
+ }
}
+
+ private void handleRule(CommandSender sender, Args args) {
+ if (args.getSize() < 2) {
+ errorAny(sender, "Usage: /da rule ...");
+ return;
+ }
+
+ String subCommand = args.get(1).toString().toLowerCase();
+
+ switch (subCommand) {
+ case "create" -> {
+ if (args.getSize() < 3) {
+ errorAny(sender, "Usage: /da rule create ");
+ return;
+ }
+
+ final ItemTag tag;
+ try {
+ tag = args.get(2).toEnum(ItemTag.class);
+ } catch (IllegalArgumentException e) {
+ errorAny(sender, "Argument '{0}' is not a valid item tag.", args.get(2).toString());
+ return;
+ }
+
+ GlobalRule rule = getDupe().createGlobalRule(tag);
+ successAny(sender, "Created new global rule (#{0}) that applies {1} tag. Use /da gui to configure matching criteria.",
+ getConfig().globalRules.indexOf(rule), tag.getName());
+ }
+
+ case "list" -> {
+ if (getConfig().globalRules.isEmpty()) {
+ infoAny(sender, "No global rules are currently configured.");
+ return;
+ }
+
+ infoAny(sender, "Global Rules ({0}):", getConfig().globalRules.size());
+ for (int i = 0; i < getConfig().globalRules.size(); i++) {
+ GlobalRule rule = getConfig().globalRules.get(i);
+ StringBuilder tagList = new StringBuilder();
+ for (ItemTag tag : rule.appliedTags) {
+ if (tagList.length() > 0) tagList.append(", ");
+ tagList.append(tag.getName());
+ }
+ infoAny(sender, " #{0}: Tags: {1}, Match Mode: {2}, Material Mode: {3}",
+ i, tagList.toString(), rule.matchMode, rule.materialMode);
+ }
+ }
+
+ case "remove" -> {
+ if (args.getSize() < 3) {
+ errorAny(sender, "Usage: /da rule remove ");
+ return;
+ }
+
+ try {
+ int index = Integer.parseInt(args.get(2).toString());
+ if (index < 0 || index >= getConfig().globalRules.size()) {
+ errorAny(sender, "Invalid rule index. Use '/da rule list' to see available rules.");
+ return;
+ }
+
+ GlobalRule removedRule = getConfig().globalRules.remove(index);
+ getConfig().save();
+
+ StringBuilder tagList = new StringBuilder();
+ for (ItemTag tag : removedRule.appliedTags) {
+ if (tagList.length() > 0) tagList.append(", ");
+ tagList.append(tag.getName());
+ }
+
+ successAny(sender, "Removed global rule #{0} (Tags: {1}).", index, tagList.toString());
+ } catch (NumberFormatException e) {
+ errorAny(sender, "'{0}' is not a valid number.", args.get(2).toString());
+ }
+ }
+
+ case "info" -> {
+ if (args.getSize() < 3) {
+ errorAny(sender, "Usage: /da rule info ");
+ return;
+ }
+
+ try {
+ int index = Integer.parseInt(args.get(2).toString());
+ if (index < 0 || index >= getConfig().globalRules.size()) {
+ errorAny(sender, "Invalid rule index. Use '/da rule list' to see available rules.");
+ return;
+ }
+
+ GlobalRule rule = getConfig().globalRules.get(index);
+ infoAny(sender, "Global Rule #{0}:", index);
+ infoAny(sender, " Applied Tags: {0}", rule.appliedTags.stream().map(ItemTag::getName).reduce((a,b) -> a + ", " + b).orElse("None"));
+ infoAny(sender, " Match Mode: {0}", rule.matchMode);
+ infoAny(sender, " Material Mode: {0}", rule.materialMode);
+ infoAny(sender, " Affected Materials: {0}", rule.effectedMaterials.size());
+ infoAny(sender, " Name Regex: {0}", rule.nameContainsRegex.isEmpty() ? "None" : rule.nameContainsRegex);
+ infoAny(sender, " Lore Regex: {0}", rule.loreContainsRegex.isEmpty() ? "None" : rule.loreContainsRegex);
+ infoAny(sender, " Enchantments: {0}", rule.enchantments.size());
+ infoAny(sender, " Potion Effects: {0}", rule.potionEffects.size());
+ infoAny(sender, " Attributes: {0}", rule.attributes.size());
+ infoAny(sender, "Use the GUI for detailed configuration.");
+ } catch (NumberFormatException e) {
+ errorAny(sender, "'{0}' is not a valid number.", args.get(2).toString());
+ }
+ }
+
+ default -> {
+ errorAny(sender, "Invalid subcommand '{0}'. Valid options: create, list, remove, info", subCommand);
+ }
+ }
+ }
+
private void handleTag(CommandSender sender, Args args) {
if (args.getSize() < 2) {
- errorAny(sender, "You must specify an item tag. Usage: /gui tag ...");
+ errorAny(sender, "You must specify an item tag. Usage: /da tag ...");
return;
}
@@ -136,7 +270,7 @@ public class AdminCommand implements QuickCommand, DupeContext {
return;
}
- // gui tag
+ // da tag
if (args.getSize() == 2) {
if (!(sender instanceof Player player)) {
errorAny(sender, "This command can only be run by a player to tag a held item. To manage material tags, specify a material or 'global'.");
@@ -159,11 +293,11 @@ public class AdminCommand implements QuickCommand, DupeContext {
// Argument 2
String subCommand = args.get(2).toString().toLowerCase();
- // gui tag remove|false
+ // da tag remove|false
switch (subCommand) {
case "remove" -> {
if (args.getSize() != 3) {
- errorAny(sender, "Invalid arguments. Usage: /gui tag remove");
+ errorAny(sender, "Invalid arguments. Usage: /da tag remove");
return;
}
if (!(sender instanceof Player player)) {
@@ -184,11 +318,11 @@ public class AdminCommand implements QuickCommand, DupeContext {
}
case "false" -> {
if (args.getSize() != 3) {
- errorAny(sender, "Invalid arguments. Usage: /gui tag remove");
+ errorAny(sender, "Invalid arguments. Usage: /da tag false");
return;
}
if (!(sender instanceof Player player)) {
- errorAny(sender, "This command can only be run by a player to add a tag from a held item.");
+ errorAny(sender, "This command can only be run by a player to set a tag on a held item.");
return;
}
ItemStack heldItem = player.getInventory().getItemInMainHand();
@@ -197,12 +331,12 @@ public class AdminCommand implements QuickCommand, DupeContext {
return;
}
getDupe().setTag(heldItem, tag, false);
- successAny(sender, "Set tag {0} from your {1} to {2}.", tag.getName(), heldItem.getType(), "false");
+ successAny(sender, "Set tag {0} on your {1} to {2}.", tag.getName(), heldItem.getType(), "false");
return;
}
- // gui tag global [remove]
+ // da tag global [remove]
case "global" -> {
if (!(sender instanceof Player player)) {
errorAny(sender, "The 'global' subcommand must be run by a player.");
@@ -217,54 +351,53 @@ public class AdminCommand implements QuickCommand, DupeContext {
boolean isRemove = args.getSize() > 3 && "remove".equalsIgnoreCase(args.get(3).toString());
if (isRemove) {
if (args.getSize() != 4) {
- errorAny(sender, "Invalid arguments. Usage: /gui tag global remove");
+ errorAny(sender, "Invalid arguments. Usage: /da tag global remove");
return;
}
- if (getDupe().removeGlobalTag(heldMaterial, tag)) {
- successAny(sender, "Removed global tag {0} from all {1} items.", tag.getName(), heldMaterial);
+ if (getDupe().removeGlobalRulesForMaterial(heldMaterial, tag)) {
+ successAny(sender, "Removed global rules applying tag {0} to {1} items.", tag.getName(), heldMaterial);
} else {
- infoAny(sender, "{0} is not globally tagged as {1}.", heldMaterial, tag.getName());
+ infoAny(sender, "No global rules found applying {0} tag to {1}.", tag.getName(), heldMaterial);
}
} else {
if (args.getSize() != 3) {
- errorAny(sender, "Invalid arguments. Usage: /gui tag global");
+ errorAny(sender, "Invalid arguments. Usage: /da tag global");
return;
}
- if (getDupe().addGlobalTag(heldMaterial, tag)) {
- successAny(sender, "All {0} items are now globally tagged as {1} and {2}.", heldMaterial, tag.getName(), tag.getDesc());
+ if (getDupe().addGlobalRuleForMaterial(heldMaterial, tag)) {
+ successAny(sender, "Created global rule: all {0} items are now tagged as {1} and {2}.", heldMaterial, tag.getName(), tag.getDesc());
} else {
- infoAny(sender, "All {0} items are already tagged as {1} and {2}.", heldMaterial, tag.getName(), tag.getDesc());
+ infoAny(sender, "A global rule already exists that tags {0} items as {1}.", heldMaterial, tag.getName());
}
}
return;
}
}
- // gui tag [remove]
+ // da tag [remove]
try {
Material material = args.get(2).toEnum(Material.class);
boolean isRemove = args.getSize() > 3 && "remove".equalsIgnoreCase(args.get(3).toString());
if (isRemove) {
if (args.getSize() != 4) {
- errorAny(sender, "Invalid arguments. Usage: /gui tag remove");
+ errorAny(sender, "Invalid arguments. Usage: /da tag remove");
return;
}
- if (getDupe().removeGlobalTag(material, tag)) {
- successAny(sender, "Removed global tag {0} from {1}.", tag.getName(), material);
+ if (getDupe().removeGlobalRulesForMaterial(material, tag)) {
+ successAny(sender, "Removed global rules applying tag {0} to {1}.", tag.getName(), material);
} else {
- infoAny(sender, "{0} is not tagged as {1} globally.", material, tag.getName());
+ infoAny(sender, "No global rules found applying {0} tag to {1}.", tag.getName(), material);
}
} else {
if (args.getSize() != 3) {
- errorAny(sender, "Invalid arguments. Usage: /gui tag ");
+ errorAny(sender, "Invalid arguments. Usage: /da tag ");
return;
}
- getDupe().addGlobalTag(material, tag);
- if (getDupe().addGlobalTag(material, tag)) {
- successAny(sender, "All {0} items are now tagged as {1} and {2}.", material, tag.getName(), tag.getDesc());
+ if (getDupe().addGlobalRuleForMaterial(material, tag)) {
+ successAny(sender, "Created global rule: all {0} items are now tagged as {1} and {2}.", material, tag.getName(), tag.getDesc());
} else {
- infoAny(sender, "All {0} items are already tagged as {1} and {2}.", material, tag.getName(), tag.getDesc());
+ infoAny(sender, "A global rule already exists that tags {0} items as {1}.", material, tag.getName());
}
}
} catch (IllegalArgumentException e) {
@@ -275,10 +408,10 @@ public class AdminCommand implements QuickCommand, DupeContext {
public void openBaseGui(CommandSender sender) {
if (sender instanceof Player player) {
- new AdminGui().openMainGui(player);
+ new MainAdminGui(new AdminPanelManager()).open(player);
} else {
errorAny(sender, "Console may not open a GUI.");
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java b/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java
index 840bf63..5b9bd23 100644
--- a/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java
+++ b/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java
@@ -37,7 +37,7 @@ public class DupeCommand implements QuickCommand, DupeContext {
}
if (args.isEmpty()) {
- if (dupeHeld(player,0)) {
+ if (dupeHeld(player,1)) {
dupeCooldown.setCooldown(player.getUniqueId(), getConfig().dupeCooldownMillis);
} else {
dupeGui.openDefaultGui(player);
@@ -94,7 +94,7 @@ public class DupeCommand implements QuickCommand, DupeContext {
int baseCount = inHand.getAmount();
int maxPerStack = inHand.getMaxStackSize();
- for (int i = 0; i <= amount; i++) {
+ for (int i = 0; i <= amount - 1; i++) {
int remaining = baseCount * (1 << i);
while (remaining > 0) {
@@ -111,7 +111,7 @@ public class DupeCommand implements QuickCommand, DupeContext {
}
}
- int totalGiven = baseCount * ((1 << (amount + 1)) - 1);
+ int totalGiven = baseCount * ((1 << amount) - 1);
successAny(player,"You have duplicated {0} items!", totalGiven);
return true;
}
diff --git a/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java b/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java
index a23d1f5..a8424cd 100644
--- a/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java
+++ b/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java
@@ -7,7 +7,7 @@ import org.bukkit.persistence.PersistentDataType;
public class UniqueCheck implements Check {
@Override
public boolean passes(ItemStack input) {
- boolean globallyUnique = getDupe().checkGlobalTag(input.getType(),ItemTag.UNIQUE);
+ boolean globallyUnique = getDupe().checkGlobalRuleTag(input,ItemTag.UNIQUE);
boolean set = input.hasItemMeta() && input.getPersistentDataContainer().has(ItemTag.UNIQUE.getKey());
boolean individuallyUnique = Boolean.TRUE.equals(input.getPersistentDataContainer().get(ItemTag.UNIQUE.getKey(), PersistentDataType.BOOLEAN));
diff --git a/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java b/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java
index dd4b246..9b53db4 100644
--- a/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java
+++ b/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java
@@ -4,9 +4,14 @@ import me.trouper.alias.utils.FormatUtils;
import me.trouper.alias.utils.ItemBuilder;
import me.trouper.dupealias.DupeAlias;
import me.trouper.dupealias.DupeContext;
+import net.kyori.adventure.text.format.TextColor;
+import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.data.type.Light;
import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.persistence.PersistentDataType;
public interface CommonItems extends DupeContext {
@@ -14,7 +19,7 @@ public interface CommonItems extends DupeContext {
default NamespacedKey CANCEL_CLICK() {
return new NamespacedKey(DupeAlias.getDupeAlias(),"CANCEL_CLICK");
}
-
+
default ItemStack EMPTY() {
return ItemBuilder.of(Material.LIGHT_GRAY_STAINED_GLASS_PANE)
.displayName("")
@@ -24,7 +29,7 @@ public interface CommonItems extends DupeContext {
})
.build();
}
-
+
default ItemStack EMPTY(Material display) {
return EMPTY().withType(display);
}
@@ -46,7 +51,13 @@ public interface CommonItems extends DupeContext {
&& Boolean.TRUE.equals(item.getItemMeta().getPersistentDataContainer().get(CANCEL_CLICK(), PersistentDataType.BOOLEAN)));
}
- default ItemStack createPopulatedItem(ItemStack item) {
+ default ItemStack createPopulatedItem(ItemStack item, double progress) {
+ if (progress < 1) {
+ return ItemBuilder.of(EMPTY(Material.RED_STAINED_GLASS_PANE))
+ .displayName("Item Refilling...")
+ .loreComponent(getTextSystem().createProgressBar(progress,(char) '|',20, TextColor.color(0x5AFF89),TextColor.color(0x6F6F6F)))
+ .build();
+ }
if (item == null || item.isEmpty()) return EMPTY(Material.GRAY_STAINED_GLASS_PANE);
ItemStack clone = item.clone();
if (getDupe().isUnique(clone)) return ItemBuilder.of(EMPTY(Material.BARRIER))
diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/AdminGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/AdminGui.java
deleted file mode 100644
index b5507f8..0000000
--- a/src/main/java/me/trouper/dupealias/server/gui/admin/AdminGui.java
+++ /dev/null
@@ -1,681 +0,0 @@
-package me.trouper.dupealias.server.gui.admin;
-
-import me.trouper.alias.server.systems.gui.QuickGui;
-import me.trouper.alias.utils.ItemBuilder;
-import me.trouper.dupealias.DupeContext;
-import me.trouper.dupealias.server.ItemTag;
-import me.trouper.dupealias.server.gui.CommonItems;
-import org.bukkit.Material;
-import org.bukkit.Sound;
-import org.bukkit.entity.Player;
-import org.bukkit.event.inventory.ClickType;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.persistence.PersistentDataType;
-
-import java.util.*;
-
-public class AdminGui implements DupeContext, CommonItems {
-
- public void openMainGui(Player player) {
- QuickGui gui = QuickGui.create()
- .titleMini("DupeAlias Admin Panel")
- .rows(5)
-
- .item(11, ItemBuilder.create(player.getInventory().getItemInMainHand().isEmpty() ? Material.BARRIER : Material.DIAMOND_SWORD)
- .displayName("Held Item Actions")
- .loreMiniMessage(
- "Manage tags for the item",
- "you're currently holding",
- "",
- "▶ Click to open menu"
- )
- .hideAllFlags()
- .build(),
- (q, event) -> openHeldItemGui(player))
-
- .item(13, ItemBuilder.create(Material.BOOKSHELF)
- .displayName("Global Material Tags")
- .loreMiniMessage(
- "Configure global tags that apply",
- "to all items of specific materials",
- "",
- "▶ Click to open menu"
- )
- .build(),
- (q, event) -> openGlobalMaterialGui(player,player.getInventory().getItemInMainHand().getType()))
-
- .item(15, ItemBuilder.create(Material.KNOWLEDGE_BOOK)
- .displayName("Information & Help")
- .loreMiniMessage(
- "Learn about item tags and",
- "how to use this system",
- "",
- "▶ Click to view help"
- )
- .build(),
- (q, event) -> openHelpGui(player))
-
- .item(29, ItemBuilder.create(Material.COMPARATOR)
- .displayName("Configuration")
- .loreMiniMessage(
- "Modify plugin parameters",
- "name and colors",
- "",
- "▶ Click to open config"
- )
- .build(), (q,event) -> new ConfigGui().open(player, q))
-
- .item(31, createPreviewItem(player.getInventory().getItemInMainHand()))
-
- .item(33, ItemBuilder.create(Material.DIAMOND)
- .displayName("<#AAAAFF>Dupe<#00DDFF>Alias Credits")
- .loreMiniMessage(
- "| Built with Alias Development Kit",
- "|",
- "| Written by obvWolf",
- " ",
- "Copyright © 2025 DupeAlias",
- "Do Not Redistribute"
- )
- .build())
-
- .fillEmpty(EMPTY())
- .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f)
- .build();
-
- gui.open(player);
- }
-
- public void openHeldItemGui(Player player) {
- ItemStack heldItem = player.getInventory().getItemInMainHand();
-
- if (heldItem.getType().isAir()) {
- errorAny(player, "You must be holding an item to use this menu!");
- return;
- }
-
- QuickGui gui = QuickGui.create()
- .titleMini("Held Item: " + heldItem.getType().name() + "")
- .rows(4)
- .fillBorder(EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE))
-
- .item(0, BACK(),
- (g, e) -> openMainGui(player))
-
- .item(13, ItemBuilder.create(heldItem.getType())
- .displayName("" + heldItem.getType().name() + "")
- .loreMiniMessage(getItemTagStatus(heldItem))
- .build())
-
- .item(11, ItemBuilder.create(Material.EMERALD)
- .displayName("Add UNIQUE Tag")
- .loreMiniMessage(Arrays.asList(
- "Makes this specific item",
- "unable to be duplicated",
- "",
- "▶ Left click to apply tag",
- "▶ Right click to remove tag",
- "▶ Shift click to set tag to false"
- ))
- .build(),
- (g, e) -> tagHeldItem(player, ItemTag.UNIQUE, e.getClick()))
-
- .item(20, ItemBuilder.create(Material.BARRIER)
- .displayName("Add FINAL Tag")
- .loreMiniMessage(Arrays.asList(
- "Makes this specific item",
- "unable to be modified",
- "",
- "▶ Left click to apply tag",
- "▶ Right click to remove tag",
- "▶ Shift click to set tag to false"
- ))
- .build(),
- (g, e) -> tagHeldItem(player, ItemTag.FINAL, e.getClick()))
-
- .item(15, ItemBuilder.create(Material.WATER_BUCKET)
- .displayName("Add INFINITE Tag")
- .loreMiniMessage(Arrays.asList(
- "Makes this specific item",
- "always have max stack size",
- "",
- "▶ Left click to apply tag",
- "▶ Right click to remove tag",
- "▶ Shift click to set tag to false"
- ))
- .build(),
- (g, e) -> tagHeldItem(player, ItemTag.INFINITE, e.getClick()))
-
- .item(24, ItemBuilder.create(Material.STRUCTURE_VOID)
- .displayName("Add PROTECTED Tag")
- .loreMiniMessage(Arrays.asList(
- "Makes this specific item",
- "not able to be manually created",
- "",
- "▶ Left click to apply tag",
- "▶ Right click to remove tag",
- "▶ Shift click to set tag to false"
- ))
- .build(),
- (g, e) -> tagHeldItem(player, ItemTag.PROTECTED, e.getClick()))
-
- .item(22, ItemBuilder.create(Material.TNT)
- .displayName("Remove All Tags")
- .loreMiniMessage(Arrays.asList(
- "Removes all tags from",
- "this specific item",
- "",
- "⚠ This cannot be undone!",
- "▶ Click to remove tags"
- ))
- .build(),
- (g, e) -> removeAllTagsFromHeld(player))
-
- .fillEmpty(EMPTY())
- .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f)
- .build();
-
- gui.open(player);
- }
-
- public void openGlobalMaterialGui(Player player, Material material) {
- if (material == null) {
- material = Material.AIR;
- }
- final Material mat = material;
-
- QuickGui gui = QuickGui.create()
- .titleMini("Global Material Tags")
- .rows(4)
- .fillBorder(EMPTY(Material.ORANGE_STAINED_GLASS_PANE))
-
- // Back button
- .item(0, BACK(),
- (g, e) -> openMainGui(player))
-
- .item(13, createMaterialTagItem(mat))
-
- .item(11, ItemBuilder.create(Material.EMERALD_BLOCK)
- .displayName("Global UNIQUE Tag")
- .loreMiniMessage(Arrays.asList(
- "Apply UNIQUE tag to ALL items",
- "of the held material type",
- "",
- "▶ Left-click to add",
- "▶ Right-click to remove"
- ))
- .build(),
- (g, e) -> handleGlobalTag(player, mat, ItemTag.UNIQUE, e.isLeftClick()))
-
- .item(20, ItemBuilder.create(Material.REDSTONE_BLOCK)
- .displayName("Global FINAL Tag")
- .loreMiniMessage(Arrays.asList(
- "Apply FINAL tag to ALL items",
- "of the held material type",
- "",
- "▶ Left-click to add",
- "▶ Right-click to remove"
- ))
- .build(),
- (g, e) -> handleGlobalTag(player, mat, ItemTag.FINAL, e.isLeftClick()))
-
- .item(15, ItemBuilder.create(Material.LAPIS_BLOCK)
- .displayName("Global INFINITE Tag")
- .loreMiniMessage(Arrays.asList(
- "Apply INFINITE tag to ALL items",
- "of the held material type",
- "",
- "▶ Left-click to add",
- "▶ Right-click to remove"
- ))
- .build(),
- (g, e) -> handleGlobalTag(player, mat, ItemTag.INFINITE, e.isLeftClick()))
-
- .item(24, ItemBuilder.create(Material.STRUCTURE_BLOCK)
- .displayName("Global PROTECTED Tag")
- .loreMiniMessage(Arrays.asList(
- "Apply PROTECTED tag to ALL items",
- "of the held material type",
- "",
- "▶ Left-click to add",
- "▶ Right-click to remove"
- ))
- .build(),
- (g, e) -> handleGlobalTag(player, mat, ItemTag.PROTECTED, e.isLeftClick()))
-
- .item(22, ItemBuilder.create(Material.COAL_BLOCK)
- .displayName("Material Browser")
- .loreMiniMessage(Arrays.asList(
- "Browse and manage tags",
- "for any material type",
- "",
- "▶ Click to open browser"
- ))
- .build(),
- (g, e) -> openMaterialBrowser(player))
-
- .fillEmpty(EMPTY())
- .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f)
- .build();
-
- gui.open(player);
- }
-
- public void openHelpGui(Player player) {
- QuickGui gui = QuickGui.create()
- .titleMini("DupeAlias Help")
- .rows(6)
- .fillBorder(EMPTY(Material.PURPLE_STAINED_GLASS_PANE))
-
- .item(0, BACK(),
- (g, e) -> openMainGui(player))
-
- .item(20, ItemBuilder.create(Material.EMERALD)
- .displayName("UNIQUE Tag")
- .loreMiniMessage(Arrays.asList(
- "What it does:",
- "• Prevents item duplication",
- "• Works globally or per item",
- "",
- "Use cases:",
- "• Crate Keys",
- "• Special or rare items",
- "• Admin-only gear",
- "",
- "⚠ Conflict:",
- "• Avoid combining with INFINITE"
- ))
- .build())
-
- .item(21, ItemBuilder.create(Material.BARRIER)
- .displayName("FINAL Tag")
- .loreMiniMessage(Arrays.asList(
- "What it does:",
- "• Blocks all item modifications",
- "• Prevents renaming, enchanting, etc.",
- "",
- "Use cases:",
- "• Name-dependent items",
- "• Rank kits or prizes",
- "• Event rewards",
- "",
- "✔ Can be combined safely with all tags"
- ))
- .build())
-
- .item(22, ItemBuilder.create(Material.WATER_BUCKET)
- .displayName("INFINITE Tag")
- .loreMiniMessage(Arrays.asList(
- "What it does:",
- "• Enforces max stack size (99)",
- "• Item instantly refills when used",
- "",
- "Use cases:",
- "• Infinite building blocks",
- "• 'Infinity' for tipped arrows",
- "• Creative-like resource flow",
- "",
- "⚠ Conflicts:",
- "• Avoid combining with UNIQUE",
- "• Avoid combining with PROTECTED"
- ))
- .build())
-
- .item(23, ItemBuilder.create(Material.STRUCTURE_VOID)
- .displayName("PROTECTED Tag")
- .loreMiniMessage(Arrays.asList(
- "What it does:",
- "• Blocks all use: crafting, consuming, enchanting",
- "• Makes item functionally inert",
- "• This does NOT prevent duping",
- "",
- "Use cases:",
- "• Crate keys or Coupons",
- "• Decorative/admin-only items",
- "",
- "⚠ Conflict:",
- "• Avoid combining with INFINITE"
- ))
- .build())
-
- .item(24, ItemBuilder.create(Material.REDSTONE_TORCH)
- .displayName("