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("Important Notes") - .loreMiniMessage(Arrays.asList( - "Things to remember:", - "• Individual tags override global tags", - "• Global tags affect ALL of a material", - "• PROTECTED items are not UNIQUE by default", - "• UNIQUE items can still be duped with external exploits", - "", - "Tag Combinations:", - "FINAL + PROTECTED = Immutable Inert item", - "PROTECTED + UNIQUE = Inert Dupe-Proof Item", - "INFINITE + UNIQUE = Paradox", - "INFINITE + PROTECTED = Contradiction" - )) - .build()) - - .item(30, ItemBuilder.create(Material.WRITABLE_BOOK) - .displayName("Individual vs Global Tags") - .loreMiniMessage(Arrays.asList( - "Individual Tags:", - "• Stored directly on the item", - "• Apply only to that one instance", - "• Use 'Held Item Actions' menu", - "", - "Global Tags:", - "• Apply to ALL items of a material", - "• Managed via config", - "• Use 'Global Material Tags' menu", - "", - "⚠ No per-world or per-player support" - )) - .build()) - .item(32, ItemBuilder.create(Material.COMMAND_BLOCK) - .displayName("Command Equivalents") - .loreMiniMessage(Arrays.asList( - "This GUI replaces these commands:", - "/da tag ", - "/da tag remove", - "/da tag global", - "/da tag ", - "", - "💡 GUI is easier and safer!" - )) - .build()) - - .item(37, ItemBuilder.create(Material.NAME_TAG) - .displayName("Tag Glossary") - .loreMiniMessage(Arrays.asList( - "UNIQUE: Prevents intended duplication", - "FINAL: Cancels editing/modification", - "PROTECTED: Blocks all use (crafting, consuming)", - "INFINITE: Always max stack size (99)", - "", - "Tags can be combined, but some conflict!" - )) - .build()) - - .item(43, ItemBuilder.create(Material.TRIAL_KEY) - .displayName("Permissions Guide") - .loreMiniMessage(Arrays.asList( - "Access Permissions:", - "dupealias.dupe - Use /dupe", - "dupealias.gui - Use duplication GUI", - "• Includes replicator, chest, inventory menus", - "", - "Bypass Permissions:", - "dupealias.unique.bypass - Dupe unique items", - "dupealias.final.bypass - Modify final items", - "dupealias.protected.bypass - Use protected items", - "", - "Other:", - "dupealias.infinite - Use infinite-tagged items", - "", - "⚠ Misuse Warning:", - "Giving bypass perms to players allows", - "them to ignore tag protections entirely!" - )) - .build()) - - .item(49, createExplainedItem(player.getInventory().getItemInMainHand())) - - .fillEmpty(EMPTY()) - .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f) - .build(); - - gui.open(player); - } - - - private ItemStack createExplainedItem(ItemStack item) { - if (item == null || item.isEmpty()) { - return ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) - .displayName("No item held") - .loreMiniMessage("💡 Hold an item to get information on it") - .build(); - } - - List lore = new ArrayList<>(); - lore.add("Held Item Explanation:"); - - Set activeTags = new HashSet<>(); - - for (ItemTag tag : ItemTag.values()) { - boolean global = getDupe().checkGlobalTag(item.getType(), tag); - boolean hasMeta = item.hasItemMeta() && - item.getItemMeta().getPersistentDataContainer().has(tag.getKey()); - - if (hasMeta) { - Boolean individual = item.getItemMeta() - .getPersistentDataContainer() - .get(tag.getKey(), PersistentDataType.BOOLEAN); - if (Boolean.TRUE.equals(individual)) { - lore.add("" + tag.getName() + " (Individual): " + tag.getDesc()); - activeTags.add(tag); - } else { - lore.add("" + tag.getName() + " (Individually false)"); - if (global) { - lore.add(" - Global is active: " + tag.getDesc()); - lore.add(" - Global is overridden by Individual tag."); - activeTags.add(tag); - } - } - } else if (global) { - lore.add("" + tag.getName() + " (Global): " + tag.getDesc()); - activeTags.add(tag); - } - } - - if (getDupe().isUnique(item)) { - lore.add("• Detected UNIQUE by UniqueCheck"); - activeTags.add(ItemTag.UNIQUE); - } - - if (lore.size() == 1) { - lore.add("• No DupeAlias tags apply to this item"); - } - - List conflicts = new ArrayList<>(); - if (activeTags.contains(ItemTag.INFINITE) && activeTags.contains(ItemTag.UNIQUE)) { - conflicts.add("INFINITE ↔ UNIQUE"); - } - if (activeTags.contains(ItemTag.INFINITE) && activeTags.contains(ItemTag.PROTECTED)) { - conflicts.add("INFINITE ↔ PROTECTED"); - } - - if (!conflicts.isEmpty()) { - lore.add(""); - lore.add("Conflicts detected:"); - for (String c : conflicts) { - lore.add("• " + c); - } - lore.add("Consider removing one of the above tags."); - } - - return ItemBuilder.of(item) - .displayName("Item Details") - .loreMiniMessage(lore) - .build(); - } - - - public void openMaterialBrowser(Player player) { - QuickGui gui = new MaterialBrowserGui().createGUI(player); - gui.open(player); - } - - public ItemStack createMaterialTagItem(Material material) { - if (material.isAir()) { - return ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) - .displayName("No Material Selected") - .loreMiniMessage(Arrays.asList( - "Hold an item to see", - "its current tag status or", - "select one in the browser", - "", - "💡 Hold an item and reopen this GUI" - )) - .build(); - } - List lore = new ArrayList<>(); - lore.add("Material: " + material.name()); - lore.add(""); - - boolean hasUnique = getDupe().checkGlobalTag(material, ItemTag.UNIQUE); - boolean hasFinal = getDupe().checkGlobalTag(material, ItemTag.FINAL); - boolean hasInfinite = getDupe().checkGlobalTag(material, ItemTag.INFINITE); - boolean hasProtected = getDupe().checkGlobalTag(material, ItemTag.PROTECTED); - - if (hasUnique || hasFinal || hasInfinite || hasProtected) { - lore.add("Global Tags:"); - if (hasUnique) lore.add("✓ UNIQUE"); - if (hasFinal) lore.add("✓ FINAL"); - if (hasInfinite) lore.add("✓ INFINITE"); - if (hasProtected) lore.add("✓ PROTECTED"); - } else { - lore.add("No global tags applied"); - } - - lore.add(""); - lore.add("Left-click to manage tags"); - - return ItemBuilder.of(material) - .displayName("" + material.name() + "") - .loreMiniMessage(lore) - .build(); - } - - private void tagHeldItem(Player player, ItemTag tag, ClickType click) { - ItemStack heldItem = player.getInventory().getItemInMainHand(); - - if (heldItem.getType().isAir()) { - errorAny(player, "You must be holding an item to tag it!"); - return; - } - - switch (click) { - case LEFT -> { - getDupe().addTag(heldItem, tag); - successAny(player, "Added {0} tag to your {1}. {2}", tag.getName(), heldItem.getType(), tag.getDesc()); - } - case RIGHT -> { - getDupe().removeTag(heldItem, tag); - successAny(player, "Removed {0} tag from your {1}.", tag.getName(), heldItem.getType()); - } - case SHIFT_LEFT, SHIFT_RIGHT -> { - getDupe().setTag(heldItem, tag, false); - successAny(player, "Set {0} tag from your {1} to {2}.", tag.getName(), heldItem.getType(), "false"); - } - } - - player.closeInventory(); - openHeldItemGui(player); - } - - private void removeAllTagsFromHeld(Player player) { - ItemStack heldItem = player.getInventory().getItemInMainHand(); - - if (heldItem.getType().isAir()) { - errorAny(player, "You must be holding an item to remove tags from it!"); - return; - } - - for (ItemTag tag : ItemTag.values()) { - getDupe().removeTag(heldItem, tag); - } - - successAny(player, "Removed all tags from your {0}.", heldItem.getType()); - - player.closeInventory(); - openHeldItemGui(player); - } - - private void handleGlobalTag(Player player, Material material, ItemTag tag, boolean isAdd) { - if (material.isAir()) { - errorAny(player, "You must have a material selected to use global material tagging!"); - return; - } - - if (isAdd) { - if (getDupe().addGlobalTag(material, tag)) { - successAny(player, "All {0} items are now globally tagged as {1}. {2}", material, tag.getName(), tag.getDesc()); - } else { - infoAny(player, "All {0} items are already tagged as {1}.", material, tag.getName()); - } - } else { - if (getDupe().removeGlobalTag(material, tag)) { - successAny(player, "Removed global {0} tag from all {1} items.", tag.getName(), material); - } else { - infoAny(player, "{0} is not globally tagged as {1}.", material, tag.getName()); - } - } - - openGlobalMaterialGui(player,material); - } - - private ItemStack createPreviewItem(ItemStack stack) { - if (stack.getType().isAir()) { - return ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) - .displayName("No Item Held") - .loreMiniMessage(Arrays.asList( - "Hold an item to see", - "its current tag status", - "", - "💡 Hold an item and reopen this GUI" - )) - .build(); - } - - return ItemBuilder.create(stack.getType()) - .displayName("Currently Held: " + stack.getType().name() + "") - .loreMiniMessage(getItemTagStatus(stack)) - .build(); - } - - private List getItemTagStatus(ItemStack item) { - List lore = new ArrayList<>(); - lore.add("Item: " + item.getType().name()); - lore.add(""); - - List individualTags = new ArrayList<>(); - for (ItemTag tag : ItemTag.values()) { - if (getDupe().hasIndividualTag(item,tag)) { - individualTags.add("<" + getTagColor(tag) + ">" + (getDupe().checkIndividualTag(item,tag) ? "✔" : "❌") + " " + tag.getName()); - } - } - - List globalTags = new ArrayList<>(); - for (ItemTag tag : ItemTag.values()) { - if (getDupe().checkGlobalTag(item.getType(), tag)) { - globalTags.add("<" + getTagColor(tag) + ">🌍 " + tag.getName()); - } - } - - if (!individualTags.isEmpty()) { - lore.add("Individual Tags:"); - lore.addAll(individualTags); - } - - if (!globalTags.isEmpty()) { - if (!individualTags.isEmpty()) lore.add(""); - lore.add("Global Tags:"); - lore.addAll(globalTags); - } - - if (individualTags.isEmpty() && globalTags.isEmpty()) { - lore.add("No tags applied"); - } - - return lore; - } - - private String getTagColor(ItemTag tag) { - return switch (tag) { - case UNIQUE -> "green"; - case FINAL -> "red"; - case INFINITE -> "blue"; - case PROTECTED -> "dark_purple"; - }; - } -} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/AdminPanelManager.java b/src/main/java/me/trouper/dupealias/server/gui/admin/AdminPanelManager.java new file mode 100644 index 0000000..00b9841 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/AdminPanelManager.java @@ -0,0 +1,358 @@ +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.data.GlobalRule; +import me.trouper.dupealias.server.ItemTag; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.config.ConfigGui; +import me.trouper.dupealias.server.gui.admin.globalrule.*; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +import java.util.*; +import java.util.stream.Collectors; + +public class AdminPanelManager implements DupeContext, CommonItems { + + public void openMainGui(Player player) { + new MainAdminGui(this).open(player); + } + + public void openHeldItemGui(Player player) { + new HeldItemGui(this).open(player); + } + + public void openHelpGui(Player player) { + new HelpGui(this).open(player); + } + + public void openGlobalRuleList(Player player) { + new GlobalRuleListGui(this).createGUI(player).open(player); + } + + public void openConfigGui(Player player) { + new ConfigGui(this).open(player); + } + + public void openGlobalRuleEditor(Player player, GlobalRule rule) { + new GlobalRuleEditorGui(this, rule).open(player); + } + + public void openMaterialSelector(Player player, GlobalRule rule) { + new GlobalRuleMaterialSelector(this, rule).createGUI(player).open(player); + } + + public void openNameCriteriaEditor(Player player, GlobalRule rule) { + QuickGui gui = QuickGui.create() + .titleMini("Name Contains") + .rows(3) + .item(13, ItemBuilder.create(Material.NAME_TAG) + .displayName("Current Pattern") + .loreMiniMessage(Arrays.asList( + "Set a regex pattern to match", + "against item display names", + "", + "Current: " + (rule.nameContainsRegex.isEmpty() ? "Not set" : rule.nameContainsRegex), + "", + "Click to set pattern", + "Right-click to clear" + )) + .build(), (g, e) -> { + if (e.isRightClick()) { + rule.nameContainsRegex = ""; + getConfig().save(); + successAny(player, "Cleared name pattern"); + openGlobalRuleEditor(player, rule); + } else { + g.requestInput(player, "modelData"); + } + }) + .item(31, BACK(), (g, e) -> openGlobalRuleEditor(player, rule)) + .fillEmpty(EMPTY()) + .callback("modelData", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + try { + int value = Integer.parseInt(input); + if (rule.legacyModelData.contains(value)) { + infoAny(player, "Model data value {0} already exists", value); + } else { + rule.legacyModelData.add(value); + getConfig().save(); + successAny(player, "Added model data value: {0}", value); + } + } catch (NumberFormatException ex) { + errorAny(player, "Invalid number: {0}", input); + } + openModelDataEditor(player, rule); + } + }) + .build(); + + int slot = 19; + for (Integer value : rule.legacyModelData.stream().limit(5).toList()) { + gui.updateItem(slot++, ItemBuilder.create(Material.FILLED_MAP) + .displayName("Value: " + value) + .loreMiniMessage(Arrays.asList( + "Model data value", + "", + "Click to remove" + )) + .build(), (g, e) -> { + rule.legacyModelData.remove(value); + getConfig().save(); + successAny(player, "Removed model data value: {0}", value); + openModelDataEditor(player, rule); + }); + } + + gui.open(player); + } + + public void openPotionEffectEditor(Player player, GlobalRule rule) { + new GlobalRulePotionEffectEditor(this, rule).createGUI(player).open(player); + } + + public void openArmorTrimEditor(Player player, GlobalRule rule) { + new GlobalRuleArmorTrimEditor(this, rule).open(player); + } + + public void openLoreCriteriaEditor(Player player, GlobalRule rule) { + QuickGui gui = QuickGui.create() + .titleMini("Lore Contains") + .rows(3) + .item(13, ItemBuilder.create(Material.WRITABLE_BOOK) + .displayName("Current Pattern") + .loreMiniMessage(Arrays.asList( + "Set a regex pattern to match", + "against item lore lines", + "", + "Current: " + (rule.loreContainsRegex.isEmpty() ? "Not set" : rule.loreContainsRegex), + "", + "Click to set pattern", + "Right-click to clear" + )) + .build(), (g, e) -> { + if (e.isRightClick()) { + rule.loreContainsRegex = ""; + getConfig().save(); + successAny(player, "Cleared lore pattern"); + openGlobalRuleEditor(player, rule); + } else { + g.requestInput(player, "lorePattern"); + } + }) + .item(22, BACK(), (g, e) -> openGlobalRuleEditor(player, rule)) + .fillEmpty(EMPTY()) + .callback("lorePattern", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + rule.loreContainsRegex = input; + getConfig().save(); + successAny(player, "Set lore pattern to: " + input); + openGlobalRuleEditor(player, rule); + } + }) + .build(); + gui.open(player); + } + + public void openEnchantmentEditor(Player player, GlobalRule rule) { + new GlobalRuleEnchantmentEditor(this, rule).createGUI(player).open(player); + } + + public void openAttributeEditor(Player player, GlobalRule rule) { + new GlobalRuleAttributeEditor(this, rule).createGUI(player).open(player); + } + + public void openItemFlagEditor(Player player, GlobalRule rule) { + new GlobalRuleItemFlagEditor(this, rule).open(player); + } + + public void openModelDataEditor(Player player, GlobalRule rule) { + QuickGui gui = QuickGui.create() + .titleMini("Model Data Values") + .rows(4) + .item(13, ItemBuilder.create(Material.COMPASS) + .displayName("Model Data Values") + .loreMiniMessage(Arrays.asList( + "Manage custom model data values", + "that items must have", + "", + "Current values: " + rule.legacyModelData.size(), + rule.legacyModelData.isEmpty() ? "" : "" + rule.legacyModelData.stream() + .limit(5) + .map(String::valueOf) + .collect(Collectors.joining(", ")), + rule.legacyModelData.size() > 5 ? "... and " + (rule.legacyModelData.size() - 5) + " more" : "", + "", + "Click to add value", + "Right-click to clear all" + )) + .build(), (g, e) -> { + if (e.isRightClick()) { + rule.legacyModelData.clear(); + getConfig().save(); + successAny(player, "Cleared all model data values"); + openModelDataEditor(player, rule); + } else { + g.requestInput(player,"namePattern"); + } + }) + .item(22, BACK(), (g, e) -> openGlobalRuleEditor(player, rule)) + .fillEmpty(EMPTY()) + .callback("namePattern", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + rule.nameContainsRegex = input; + getConfig().save(); + successAny(player, "Set name pattern to: " + input); + openGlobalRuleEditor(player, rule); + } + }) + .build(); + gui.open(player); + } + + public ItemStack createExplainedItem(ItemStack item) { + if (item == null || item.isEmpty()) { + return ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) + .displayName("No item held") + .loreMiniMessage("💡 Hold an item to get information on it") + .build(); + } + + List lore = new ArrayList<>(); + lore.add("Held Item Explanation:"); + + Set activeTags = new HashSet<>(); + + for (ItemTag tag : ItemTag.values()) { + boolean global = getDupe().checkGlobalRuleTag(item, tag); + boolean hasMeta = item.hasItemMeta() && + item.getItemMeta().getPersistentDataContainer().has(tag.getKey()); + + if (hasMeta) { + Boolean individual = item.getItemMeta() + .getPersistentDataContainer() + .get(tag.getKey(), PersistentDataType.BOOLEAN); + if (Boolean.TRUE.equals(individual)) { + lore.add("" + tag.getName() + " (Individual): " + tag.getDesc()); + activeTags.add(tag); + } else { + lore.add("" + tag.getName() + " (Individually false)"); + if (global) { + lore.add(" - Global is active: " + tag.getDesc()); + lore.add(" - Global is overridden by Individual tag."); + activeTags.add(tag); + } + } + } else if (global) { + lore.add("" + tag.getName() + " (Global): " + tag.getDesc()); + activeTags.add(tag); + } + } + + if (getDupe().isUnique(item)) { + lore.add("• Detected UNIQUE by UniqueCheck"); + activeTags.add(ItemTag.UNIQUE); + } + + if (lore.size() == 1) { + lore.add("• No DupeAlias tags apply to this item"); + } + + List conflicts = new ArrayList<>(); + if (activeTags.contains(ItemTag.INFINITE) && activeTags.contains(ItemTag.UNIQUE)) { + conflicts.add("INFINITE ↔ UNIQUE"); + } + if (activeTags.contains(ItemTag.INFINITE) && activeTags.contains(ItemTag.PROTECTED)) { + conflicts.add("INFINITE ↔ PROTECTED"); + } + + if (!conflicts.isEmpty()) { + lore.add(""); + lore.add("Conflicts detected:"); + for (String c : conflicts) { + lore.add("• " + c); + } + lore.add("Consider removing one of the above tags."); + } + + return ItemBuilder.of(item) + .displayName("Item Details") + .loreMiniMessage(lore) + .build(); + } + + public ItemStack createPreviewItem(ItemStack stack) { + if (stack.getType().isAir()) { + return ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) + .displayName("No Item Held") + .loreMiniMessage(Arrays.asList( + "Hold an item to see", + "its current tag status", + "", + "💡 Hold an item and reopen this GUI" + )) + .build(); + } + + return ItemBuilder.create(stack.getType()) + .displayName("Currently Held: " + stack.getType().name() + "") + .loreMiniMessage(getItemTagStatus(stack)) + .build(); + } + + public List getItemTagStatus(ItemStack item) { + List lore = new ArrayList<>(); + lore.add("Item: " + item.getType().name()); + lore.add(""); + + List individualTags = new ArrayList<>(); + for (ItemTag tag : ItemTag.values()) { + if (getDupe().hasIndividualTag(item,tag)) { + individualTags.add("<" + getTagColor(tag) + ">" + (getDupe().checkIndividualTag(item,tag) ? "✔" : "❌") + " " + tag.getName()); + } + } + + List globalTags = new ArrayList<>(); + for (ItemTag tag : ItemTag.values()) { + if (getDupe().checkGlobalRuleTag(item, tag)) { + globalTags.add("<" + getTagColor(tag) + ">🌍 " + tag.getName()); + } + } + + if (!individualTags.isEmpty()) { + lore.add("Individual Tags:"); + lore.addAll(individualTags); + } + + if (!globalTags.isEmpty()) { + if (!individualTags.isEmpty()) lore.add(""); + lore.add("Global Tags:"); + lore.addAll(globalTags); + } + + if (individualTags.isEmpty() && globalTags.isEmpty()) { + lore.add("No tags applied"); + } + + return lore; + } + + public String getTagColor(ItemTag tag) { + return switch (tag) { + case UNIQUE -> "green"; + case FINAL -> "red"; + case INFINITE -> "blue"; + case PROTECTED -> "dark_purple"; + }; + } + + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/ConfigGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/ConfigGui.java deleted file mode 100644 index bd97898..0000000 --- a/src/main/java/me/trouper/dupealias/server/gui/admin/ConfigGui.java +++ /dev/null @@ -1,57 +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.gui.CommonItems; -import org.bukkit.Material; -import org.bukkit.entity.Player; - -import java.util.List; - -public class ConfigGui implements DupeContext, CommonItems { - public void open(Player player, QuickGui backGui) { - QuickGui gui = QuickGui.create() - .titleMini("DupeAlias Config") - .defaultTimeout(30000) - .rows(6) - .item(0,BACK(),(g,e) -> backGui.open(player)) - .callback("dupe_cooldown", new QuickGui.GuiCallback() { - @Override - public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { - try { - long millis = Long.parseLong(input); - infoAny(player,"You have set the dupe cooldown to {0}ms.",input); - getDupe().getConfig().dupeCooldownMillis = millis; - getDupe().getConfig().save(); - open(player,backGui); - } catch (NumberFormatException ex) { - errorAny(player,"Please input a valid long number of milliseconds."); - requestInput(gui,player,"dupe_cooldown","Number format error, please input a value."); - } - } - }) - .item(13, ItemBuilder.integerItem(Material.DIAMOND,"Dupe Command Cooldown", List.of( - "How long players have", - "to wait before running", - "the /dupe command again.", - " ", - "Click to set value."), (int) getConfig().dupeCooldownMillis),(g, e)->{ - Player p = (Player) e.getWhoClicked(); - requestInput(g,p,"dupe_cooldown","Insert a long value of Milliseconds.\n 1000ms = 1 Second\n\n The current value is set to " + getConfig().dupeCooldownMillis + "\n"); - }) - .fillEmpty(EMPTY()) - .build(); - - player.openInventory(gui.getInventory()); - } - - - private void requestInput(QuickGui gui, Player player, String callbackId, String prompt) { - getDupe().getGuiListener().registerWaitingPlayer(player, gui); - - gui.requestInput(player, callbackId); - - getDupe().getGuiListener().sendInputInstructions(player, prompt); - } -} diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/HeldItemGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/HeldItemGui.java new file mode 100644 index 0000000..9029711 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/HeldItemGui.java @@ -0,0 +1,160 @@ +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 java.util.Arrays; + +public class HeldItemGui implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + + public HeldItemGui(AdminPanelManager manager) { + this.manager = manager; + } + + public void open(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) -> manager.openMainGui(player)) + + .item(13, ItemBuilder.create(heldItem.getType()) + .displayName("" + heldItem.getType().name() + "") + .loreMiniMessage(manager.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); + } + + private void tagHeldItem(Player player, ItemTag tag, ClickType click) { + ItemStack heldItem = player.getInventory().getItemInMainHand(); + + if (heldItem.getType().isAir()) { + errorAny(player, "You must be holding an item to tag it!"); + return; + } + + switch (click) { + case LEFT -> { + getDupe().addTag(heldItem, tag); + successAny(player, "Added {0} tag to your {1}. {2}", tag.getName(), heldItem.getType(), tag.getDesc()); + } + case RIGHT -> { + getDupe().removeTag(heldItem, tag); + successAny(player, "Removed {0} tag from your {1}.", tag.getName(), heldItem.getType()); + } + case SHIFT_LEFT, SHIFT_RIGHT -> { + getDupe().setTag(heldItem, tag, false); + successAny(player, "Set {0} tag from your {1} to {2}.", tag.getName(), heldItem.getType(), "false"); + } + } + + player.closeInventory(); + open(player); // Re-open the GUI to update + } + + private void removeAllTagsFromHeld(Player player) { + ItemStack heldItem = player.getInventory().getItemInMainHand(); + + if (heldItem.getType().isAir()) { + errorAny(player, "You must be holding an item to remove tags from it!"); + return; + } + + for (ItemTag tag : ItemTag.values()) { + getDupe().removeTag(heldItem, tag); + } + + successAny(player, "Removed all tags from your {0}.", heldItem.getType()); + + player.closeInventory(); + open(player); // Re-open the GUI to update + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/HelpGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/HelpGui.java new file mode 100644 index 0000000..3091177 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/HelpGui.java @@ -0,0 +1,224 @@ +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.server.gui.CommonItems; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.Arrays; + +public class HelpGui implements CommonItems { + + private final AdminPanelManager manager; + + public HelpGui(AdminPanelManager manager) { + this.manager = manager; + } + + public void open(Player player) { + QuickGui gui = QuickGui.create() + .titleMini("DupeAlias Help") + .rows(6) + .fillBorder(EMPTY(Material.PURPLE_STAINED_GLASS_PANE)) + + .item(0, BACK(), + (g, e) -> manager.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("Important Notes") + .loreMiniMessage(Arrays.asList( + "Things to remember:", + "• Individual tags override global rules", + "• Global rules can match complex criteria", + "• PROTECTED items are not UNIQUE by default", + "• UNIQUE items can still be duped with external exploits", + "", + "Tag Combinations:", + "FINAL + PROTECTED = Immutable Inert item", + "PROTECTED + UNIQUE = Inert Dupe-Proof Item", + "INFINITE + UNIQUE = Paradox", + "INFINITE + PROTECTED = Contradiction" + )) + .build()) + + .item(30, ItemBuilder.create(Material.WRITABLE_BOOK) + .displayName("Individual vs Global Tags") + .loreMiniMessage(Arrays.asList( + "Individual Tags:", + "• Stored directly on the item", + "• Apply only to that one instance", + "• Use 'Held Item Actions' menu", + "", + "Global Rules:", + "• Apply tags based on item properties", + "• Match by material, name, enchants, etc.", + "• Use 'Global Rules' menu", + "", + "⚠ Individual tags override global rules" + )) + .build()) + + .item(31, ItemBuilder.create(Material.COMPARATOR) + .displayName("Global Rules System") + .loreMiniMessage(Arrays.asList( + "Match items by:", + "Materials (whitelist/blacklist)", + "Name/Lore (regex patterns)", + "Enchantments (type & level)", + "Attributes (exact values)", + "Model Data (custom values)", + "Potion Effects (type & amp)", + "Armor Trim (pattern & material)", + "Item Flags (hide tooltips)", + "", + "Match Modes:", + "AND: All criteria must match", + "OR: Any criteria matches", + "NAND: Not all match", + "XOR: Exactly one matches" + )) + .build()) + + .item(32, ItemBuilder.create(Material.COMMAND_BLOCK) + .displayName("Rule Examples") + .loreMiniMessage(Arrays.asList( + "Example Criteria:", + "", + "1. Prevent Duping Netherite:", + "• Material: [NETHERITE_INGOT, ANCIENT_DEBRIS, ...]", + "• Tag: UNIQUE", + "", + "2. Protect Crate Keys by Name", + "• Name Regex: 'key'", + "• Tags: PROTECTED, UNIQUE", + "", + "3. Lock Silence Trim Armor:", + "• Material: Ignore", + "• Trim: [Silence]", + "• Match Mode: AND", + "• Tag: FINAL" + )) + .build()) + + .item(37, ItemBuilder.create(Material.NAME_TAG) + .displayName("Tag Glossary") + .loreMiniMessage(Arrays.asList( + "UNIQUE: Prevents intended duplication", + "FINAL: Cancels editing/modification", + "PROTECTED: Blocks all use (crafting, consuming)", + "INFINITE: Always max stack size (99)", + "", + "Tags can be combined, but some conflict!" + )) + .build()) + + .item(43, ItemBuilder.create(Material.TRIAL_KEY) + .displayName("Permissions Guide") + .loreMiniMessage(Arrays.asList( + "Access Permissions:", + "The root permission node is dupealias", + ".dupe - Use /dupe command", + ".gui - Access duplication GUI", + "", + "Dupe GUI & Sessions:", + ".gui..refresh. - GUI refill time", + ".gui..keep - Retain items in GUI session", + ".gui.replicator - Shift-click duplicate", + ".gui.replicator.cooldown - Item input cooldown", + ".gui.inventory - View personal inventory", + ".gui.chest - Dupe via container", + "", + "Bypass Permissions:", + ".unique.bypass - Dupe unique items", + ".final.bypass - Modify final items", + ".protected.bypass - Use protected items", + ".dupe.cooldownbypass - Skip /dupe cooldown", + "", + "Other:", + ".infinite - Use infinite-tagged items", + "", + "⚠ Misuse Warning:", + "Bypass perms override protections!", + "Use caution when assigning them." + )) + .build()) + + .item(49, manager.createExplainedItem(player.getInventory().getItemInMainHand())) + + .fillEmpty(EMPTY()) + .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f) + .build(); + + gui.open(player); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/MainAdminGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/MainAdminGui.java new file mode 100644 index 0000000..fda009f --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/MainAdminGui.java @@ -0,0 +1,90 @@ +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.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.config.ConfigGui; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +public class MainAdminGui implements CommonItems { + + private final AdminPanelManager manager; + + public MainAdminGui(AdminPanelManager manager) { + this.manager = manager; + } + + public void open(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) -> manager.openHeldItemGui(player)) + + .item(13, ItemBuilder.create(Material.BOOKSHELF) + .displayName("Global Rules") + .loreMiniMessage( + "Configure global rules to apply", + "tags based on item properties", + "", + "Rules: " + getConfig().globalRules.size(), + "", + "Click to manage rules" + ) + .build(), + (q, event) -> manager.openGlobalRuleList(player)) + + .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) -> manager.openHelpGui(player)) + + .item(29, ItemBuilder.create(Material.COMPARATOR) + .displayName("Configuration") + .loreMiniMessage( + "Modify plugin parameters", + "name and colors", + "", + "Click to open config" + ) + .build(), (q,event) -> manager.openConfigGui(player)) + + .item(31, manager.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); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/MaterialBrowserGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/MaterialBrowserGui.java deleted file mode 100644 index 6a3903c..0000000 --- a/src/main/java/me/trouper/dupealias/server/gui/admin/MaterialBrowserGui.java +++ /dev/null @@ -1,70 +0,0 @@ -package me.trouper.dupealias.server.gui.admin; - -import me.trouper.alias.server.systems.gui.QuickGui; -import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; -import me.trouper.dupealias.DupeContext; -import me.trouper.dupealias.server.ItemTag; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class MaterialBrowserGui extends QuickPaginatedGUI implements DupeContext { - - private Material PICKED_MATERIAL = Material.AIR; - - @Override - protected String getTitle(Player player) { - return "Material Browser"; - } - - @Override - protected List getAllItems(Player player) { - return Arrays.stream(Material.values()).filter(material -> !material.isLegacy() && !material.isAir()).filter(Material::isItem).toList(); - } - - @Override - protected ItemStack createDisplayItem(Material item) { - return new AdminGui().createMaterialTagItem(item); - } - - @Override - protected void handleItemClick(Player player, Material item, InventoryClickEvent event) { - PICKED_MATERIAL = item; - new AdminGui().openGlobalMaterialGui(player,PICKED_MATERIAL); - } - - @Override - protected void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters) { - filterGui.item(0, createFilterToggleItem("Infinite",Material.LAPIS_BLOCK,filters.contains("I")), (gui, event) -> - toggleFilter(player,"I")); - filterGui.item(1, createFilterToggleItem("Unique",Material.EMERALD_BLOCK,filters.contains("U")), (gui, event) -> - toggleFilter(player,"U")); - filterGui.item(2, createFilterToggleItem("Final",Material.REDSTONE_BLOCK,filters.contains("F")), (gui, event) -> - toggleFilter(player,"F")); - filterGui.item(3, createFilterToggleItem("Protected",Material.COMMAND_BLOCK,filters.contains("P")), (gui, event) -> - toggleFilter(player,"P")); - - } - - @Override - protected boolean testFilter(Player player, Material item, String filterKey) { - return switch (filterKey) { - case "I" -> getConfig().globalMaterials.getOrDefault(item,new HashSet<>()).contains(ItemTag.INFINITE); - case "U" -> getConfig().globalMaterials.getOrDefault(item,new HashSet<>()).contains(ItemTag.UNIQUE); - case "F" -> getConfig().globalMaterials.getOrDefault(item,new HashSet<>()).contains(ItemTag.FINAL); - case "P" -> getConfig().globalMaterials.getOrDefault(item,new HashSet<>()).contains(ItemTag.PROTECTED); - default -> false; - }; - } - - @Override - protected void openBackGUI(Player player) { - new AdminGui().openGlobalMaterialGui(player,PICKED_MATERIAL); - } -} diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/config/CommandRegexGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/config/CommandRegexGui.java new file mode 100644 index 0000000..1c2c8f8 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/config/CommandRegexGui.java @@ -0,0 +1,108 @@ +package me.trouper.dupealias.server.gui.admin.config; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class CommandRegexGui extends QuickPaginatedGUI implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + + public CommandRegexGui(AdminPanelManager manager) { + this.manager = manager; + } + + @Override + protected String getTitle(Player player) { + return "Final Command Regex"; + } + + @Override + protected List getAllItems(Player player) { + return getConfig().finalCommandRegex; + } + + @Override + protected ItemStack createDisplayItem(String pattern) { + return ItemBuilder.create(Material.PAPER) + .displayName("Blocked Pattern") + .loreMiniMessage(Arrays.asList( + "" + pattern, + "", + "Right-click to remove" + )) + .build(); + } + + @Override + public QuickGui createGUI(Player player) { + QuickGui gui = super.createGUI(player); + gui.updateItem(51,ItemBuilder.create(Material.LIME_DYE) + .displayName("+ Add Regex Pattern") + .loreMiniMessage(Arrays.asList( + "Add a new regex pattern for", + "commands to block with FINAL items", + "", + "Click to add pattern" + )) + .build(),(g,e)->{ + QuickGui inputGui = QuickGui.create() + .titleMini("Set Regex Pattern") + .rows(3) + .callback("add_regex", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + getConfig().finalCommandRegex.add(input); + getConfig().save(); + successAny(player, "Added {0} to the final command regex."); + createGUI(player).open(player); + } + }) + .item(13, ItemBuilder.create(Material.EXPERIENCE_BOTTLE) + .displayName("Click me!") + .loreMiniMessage(Arrays.asList( + "Enter the regex of", + "the command to block" + )) + .build(), (q, ev) -> getDupe().getGuiListener().requestChatInput(q, player, "add_regex", + "Enter a regex pattern for commands to block:\nExample: \"(?:itemname|iname)\"gmi")) + .item(22, BACK(), (q, ev) -> createGUI(player).open(player)) + .fillEmpty(EMPTY()) + .build(); + + inputGui.open(player); + }); + return gui; + } + + @Override + protected void handleItemClick(Player player, String regex, InventoryClickEvent e) { + if (e.isRightClick()) { + getConfig().finalCommandRegex.remove(regex); + getConfig().save(); + successAny(player, "Removed regex pattern"); + createGUI(player).open(player); + } + } + + @Override + protected void addFilterItems(QuickGui.GuiBuilder guiBuilder, Player player, Set set) { + + } + + @Override + protected void openBackGUI(Player player) { + manager.openConfigGui(player); + } +} diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/config/CommonConfigGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/config/CommonConfigGui.java new file mode 100644 index 0000000..6d74ef2 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/config/CommonConfigGui.java @@ -0,0 +1,172 @@ +package me.trouper.dupealias.server.gui.admin.config; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeAlias; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.files.CommonConfig; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.List; + +/** + * A GUI for modifying the settings within the common.json file. + * This class provides an interface for changing general plugin settings + * such as colors, prefixes, and display names. + */ +public class CommonConfigGui implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + + /** + * Constructs a new CommonConfigGui. + * + * @param manager The AdminPanelManager to handle navigation. + */ + public CommonConfigGui(AdminPanelManager manager) { + this.manager = manager; + } + + /** + * Opens the Common Config GUI for a player. + * + * @param player The player to open the GUI for. + */ + public void open(Player player) { + // Retrieve the current common configuration instance. + CommonConfig config = getCommonConfig(); + + // Create the GUI using the QuickGui builder. + QuickGui gui = QuickGui.create() + .titleMini("Common Config") + .defaultTimeout(30000) + .rows(3) + .clickSound(Sound.UI_BUTTON_CLICK, 0.5f, 1.0f) + + // Back button to return to the main admin panel. + .item(0, BACK(), (g, e) -> manager.openMainGui(player)) + + // Item and callback for modifying the main color. + .callback("main_color", (g, p, input, source) -> { + try { + // Parse the hex string into an integer color value. + int color = Integer.parseInt(input.replace("#", ""), 16); + config.mainColor = color; + config.save(); + // Reload the common settings in the core to apply changes immediately. + getInstance().getCommon().update(config.generateCommon()); + successAny(p, "Main color set to <#{0}>#{0}.", Integer.toHexString(color)); + open(p); // Re-open the GUI to show the updated value. + } catch (NumberFormatException ex) { + errorAny(p, "Invalid hex color format. Use RRGGBB (e.g., AAAAFF)."); + } + }) + .item(10, ItemBuilder.create(Material.BLUE_WOOL) + .displayName("Main Color") + .loreMiniMessage(List.of( + "The color for the message border.", + "", + String.format("Current: <#%s>#%s", Integer.toHexString(config.mainColor), Integer.toHexString(config.mainColor), Integer.toHexString(config.mainColor)), + "", + "Click to modify" + )).build(), (g, e) -> + // Request chat input from the player. + getDupe().getGuiListener().requestChatInput(g, player, "main_color", + "Enter a hex color code for the main color.\nExample: AAAAFF")) + + // Item and callback for modifying the secondary color. + .callback("secondary_color", (g, p, input, source) -> { + try { + int color = Integer.parseInt(input.replace("#", ""), 16); + config.secondaryColor = color; + config.save(); + getInstance().getCommon().update(config.generateCommon()); + successAny(p, "Secondary color set to <#{0}>#{0}.", Integer.toHexString(color)); + open(p); + } catch (NumberFormatException ex) { + errorAny(p, "Invalid hex color format. Use RRGGBB (e.g., 00DDFF)."); + } + }) + .item(11, ItemBuilder.create(Material.CYAN_WOOL) + .displayName("Secondary Color") + .loreMiniMessage(List.of( + "The color used for the plugin's name.", + "", + String.format("Current: <#%s>#%s", Integer.toHexString(config.secondaryColor), Integer.toHexString(config.secondaryColor), Integer.toHexString(config.secondaryColor)), + "", + "Click to modify" + )).build(), (g, e) -> + getDupe().getGuiListener().requestChatInput(g, player, "secondary_color", + "Enter a hex color code for the secondary color.\nExample: 00DDFF")) + + // Item and callback for modifying the plugin name. + .callback("plugin_name", (g, p, input, source) -> { + config.pluginName = input; + config.save(); + getInstance().getCommon().update(config.generateCommon()); + successAny(p, "Plugin name set to: {0}", input); + open(p); + }) + .item(12, ItemBuilder.create(Material.NAME_TAG) + .displayName("Plugin Name") + .loreMiniMessage(List.of( + "The name of the plugin displayed in messages.", + "", + "Current: " + config.pluginName, + "", + "Click to modify" + )).build(), (g, e) -> + getDupe().getGuiListener().requestChatInput(g, player, "plugin_name", + "Enter the new plugin name.")) + + // Item and callback for modifying the flat prefix. + .callback("flat_prefix", (g, p, input, source) -> { + config.flatPrefix = input; + config.save(); + getInstance().getCommon().update(config.generateCommon()); + successAny(p, "Flat prefix set to: {0}", input); + open(p); + }) + .item(13, ItemBuilder.create(Material.PAPER) + .displayName("Flat Prefix") + .loreMiniMessage(List.of( + "The prefix used when 'flat' mode is enabled.", + "Uses legacy '&' color codes.", + "", + "Current: " + config.flatPrefix, + "", + "Click to modify" + )).build(), (g, e) -> + getDupe().getGuiListener().requestChatInput(g, player, "flat_prefix", + "Enter the new flat prefix.")) + + // Item and click handler for toggling flat mode. + .item(14, ItemBuilder.create(config.flat ? Material.LIME_DYE : Material.GRAY_DYE) + .displayName("Flat Mode") + .loreMiniMessage(List.of( + "If true, uses the simple flat message system", + "instead of the complex line wrapping feature.", + "", + "Current: " + config.flat, + "", + "Click to toggle" + )).build(), (g, e) -> { + config.flat = !config.flat; + config.save(); + getInstance().getCommon().update(config.generateCommon()); + infoAny(player, "Flat mode set to: {0}", config.flat); + open(player); // Re-open to update the item's appearance. + }) + + // Fill the rest of the GUI with empty panes. + .fillEmpty(EMPTY()) + .build(); + + // Display the GUI to the player. + gui.open(player); + } +} diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/config/ConfigGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/config/ConfigGui.java new file mode 100644 index 0000000..dc9d266 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/config/ConfigGui.java @@ -0,0 +1,389 @@ +package me.trouper.dupealias.server.gui.admin.config; + +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 me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.List; + +public class ConfigGui implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + + public ConfigGui(AdminPanelManager manager) { + this.manager = manager; + } + + public void open(Player player) { + QuickGui gui = QuickGui.create() + .titleMini("DupeAlias Config") + .defaultTimeout(30000) + .rows(5) + .clickSound(Sound.UI_BUTTON_CLICK, 0.5f, 1.0f) + + // Back button + .item(0, BACK(), (g, e) -> manager.openMainGui(player)) + + // Dupe Cooldown + .callback("dupe_cooldown", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + try { + long millis = Long.parseLong(input); + if (millis < 0) { + errorAny(player, "Cooldown cannot be negative!"); + return; + } + infoAny(player, "You have set the dupe cooldown to {0}ms.", input); + getConfig().dupeCooldownMillis = millis; + getConfig().save(); + open(player); + } catch (NumberFormatException ex) { + errorAny(player, "Please input a valid number of milliseconds."); + getDupe().getGuiListener().requestChatInput(gui, player, "dupe_cooldown", "Number format error, please input a valid value."); + } + } + }) + .item(11, ItemBuilder.integerItem(Material.DIAMOND, "Dupe Command Cooldown", List.of( + "How long players have to wait", + "before running the /dupe command again.", + "", + "Current: " + getConfig().dupeCooldownMillis + "ms", + "", + "Click to modify" + ), (int) getConfig().dupeCooldownMillis), (g, e) -> + getDupe().getGuiListener().requestChatInput(g, player, "dupe_cooldown", + "Insert a long value of Milliseconds.\n 1000ms = 1 Second\n\nCurrent value: " + getConfig().dupeCooldownMillis + "ms")) + + // Default Dupe GUI + .item(12, ItemBuilder.create(Material.CHEST) + .displayName("Default Dupe GUI") + .loreMiniMessage(Arrays.asList( + "The GUI type that opens when", + "players use the /dupe command", + "", + "Options: REPLICATOR, INVENTORY, CHEST, MENU", + "", + "Current: " + getConfig().defaultDupeGui, + "", + "Click to cycle options" + )) + .build(), (g, e) -> cycleDefaultGui(player)) + + // Final Command Regex + .item(13, ItemBuilder.create(Material.BARRIER) + .displayName("Final Command Regex") + .loreMiniMessage(Arrays.asList( + "Regex patterns for commands that", + "are blocked when holding FINAL items", + "", + "Patterns: " + getConfig().finalCommandRegex.size(), + "", + "Click to manage patterns" + )) + .build(), (g, e) -> openFinalCommandRegexGui(player)) + + // Global Rules Editor + .item(14, ItemBuilder.create(Material.COMMAND_BLOCK) + .displayName("Global Rules Editor") + .loreMiniMessage(Arrays.asList( + "Advanced rule system for applying", + "tags based on item properties", + "", + "Rules: " + getConfig().globalRules.size(), + "", + "Click to manage rules" + )) + .build(), (g, e) -> openGlobalRulesGui(player)) + + // Tag Lore Settings + .item(15, ItemBuilder.create(Material.NAME_TAG) + .displayName("Tag Lore Settings") + .loreMiniMessage(Arrays.asList( + "Configure the lore text that", + "appears on tagged items", + "", + "Click to configure lore" + )) + .build(), (g, e) -> openTagLoreGui(player, g)) + + // Common Settings + .item(22,ItemBuilder.create(Material.LIGHT) + .displayName("Common Config") + .loreMiniMessage( + "Generic plugin configuration", + "Like plugin name and colors", + "", + "Click to modify" + ) + .build(), + (g,e) -> openCommonGui(player, g)) + + // Replicator Settings + .item(30, ItemBuilder.create(Material.REPEATER) + .displayName("Replicator Settings") + .loreMiniMessage(Arrays.asList( + "Configure replicator GUI behavior", + "", + "Refresh Delay: " + getConfig().replicator.baseRefreshDelayTicks + " ticks", + "Input Cooldown: " + getConfig().replicator.baseInputCooldownTicks + " ticks", + "", + "Click to configure" + )) + .build(), (g, e) -> openReplicatorGui(player, g)) + + // Chest Settings + .item(31, ItemBuilder.create(Material.CHEST) + .displayName("Chest GUI Settings") + .loreMiniMessage(Arrays.asList( + "Configure chest GUI behavior", + "", + "Refresh Delay: " + getConfig().chest.baseRefreshDelayTicks + " ticks", + "", + "Click to configure" + )) + .build(), (g, e) -> openChestGui(player, g)) + + // Inventory Settings + .item(32, ItemBuilder.create(Material.ENDER_CHEST) + .displayName("Inventory GUI Settings") + .loreMiniMessage(Arrays.asList( + "Configure inventory GUI behavior", + "", + "Refresh Delay: " + getConfig().inventory.baseRefreshDelayTicks + " ticks", + "", + "Click to configure" + )) + .build(), (g, e) -> openInventoryGui(player, g)) + + .fillEmpty(EMPTY()) + .build(); + + gui.open(player); + } + + private void openCommonGui(Player player, QuickGui g) { + new CommonConfigGui(manager).open(player); + } + + private void cycleDefaultGui(Player player) { + String[] options = {"REPLICATOR", "INVENTORY", "CHEST", "MENU"}; + String current = getConfig().defaultDupeGui; + + int currentIndex = Arrays.asList(options).indexOf(current); + int nextIndex = (currentIndex + 1) % options.length; + + getConfig().defaultDupeGui = options[nextIndex]; + getConfig().save(); + + infoAny(player, "Default GUI changed to: {0}", options[nextIndex]); + open(player); + } + + private void openFinalCommandRegexGui(Player player) { + new CommandRegexGui(manager).createGUI(player).open(player); + } + + private void openGlobalRulesGui(Player player) { + manager.openGlobalRuleList(player); + } + + private void openTagLoreGui(Player player, QuickGui backGui) { + QuickGui gui = QuickGui.create() + .titleMini("Tag Lore Settings") + .rows(1) + .item(8, BACK(), (g, e) -> open(player)) + .fillEmpty(EMPTY()) + .build(); + + int slot = 0; + for (ItemTag tag : ItemTag.values()) { + String trueLore = getConfig().trueTagLore.get(tag); + String falseLore = getConfig().falseTagLore.get(tag); + + gui.updateItem(slot, ItemBuilder.create(getMaterialForTag(tag)) + .displayName("<" + getTagColor(tag) + ">" + tag.getName() + " Tag Lore") + .loreMiniMessage(Arrays.asList( + "True Lore: " + (trueLore != null ? trueLore : "Not set"), + "False Lore: " + (falseLore != null ? falseLore : "Not set"), + "", + "Left-click to edit true lore", + "Right-click to edit false lore" + )) + .build(), (g, e) -> editTagLore(player, tag, e.isLeftClick(), g, backGui)); + + slot++; + } + + gui.open(player); + } + + private void editTagLore(Player player, ItemTag tag, boolean isTrue, QuickGui gui, QuickGui backGui) { + String callbackId = "edit_lore_" + tag.name() + "_" + isTrue; + + gui.updateItem(0, gui.getSlotItems().get(0)); + + QuickGui.GuiCallback callback = new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + if (isTrue) { + getConfig().trueTagLore.put(tag, input); + } else { + getConfig().falseTagLore.put(tag, input); + } + getConfig().save(); + successAny(player, "Updated {0} {1} lore", tag.getName(), isTrue ? "true" : "false"); + openTagLoreGui(player, backGui); + } + }; + + // Create a temporary GUI with the callback + QuickGui tempGui = QuickGui.create() + .titleMini("Editing " + tag.getName() + " Lore") + .rows(1) + .callback(callbackId, callback) + .build(); + + getDupe().getGuiListener().requestChatInput(tempGui, player, callbackId, + "Enter the " + (isTrue ? "true" : "false") + " lore for " + tag.getName() + ":\n" + + "Use MiniMessage format (e.g., text)\n" + + "Current: " + (isTrue ? getConfig().trueTagLore.get(tag) : getConfig().falseTagLore.get(tag))); + } + + private void openReplicatorGui(Player player, QuickGui backGui) { + openTicksConfigGui(player, backGui, "Replicator", Material.REPEATER, + getConfig().replicator.baseRefreshDelayTicks, + getConfig().replicator.baseInputCooldownTicks, + (refresh, input) -> { + getConfig().replicator.baseRefreshDelayTicks = refresh; + getConfig().replicator.baseInputCooldownTicks = input; + }); + } + + private void openChestGui(Player player, QuickGui backGui) { + openTicksConfigGui(player, backGui, "Chest", Material.CHEST, + getConfig().chest.baseRefreshDelayTicks, + null, + (refresh, input) -> { + getConfig().chest.baseRefreshDelayTicks = refresh; + }); + } + + private void openInventoryGui(Player player, QuickGui backGui) { + openTicksConfigGui(player, backGui, "Inventory", Material.ENDER_CHEST, + getConfig().inventory.baseRefreshDelayTicks, + null, + (refresh, input) -> { + getConfig().inventory.baseRefreshDelayTicks = refresh; + }); + } + + private void openTicksConfigGui(Player player, QuickGui backGui, String name, Material icon, + int refreshTicks, Integer inputTicks, TicksConfigSetter setter) { + QuickGui gui = QuickGui.create() + .titleMini("" + name + " Settings") + .rows(1) + .item(8, BACK(), (g, e) -> open(player)) + + .callback("refresh_ticks", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + try { + int ticks = Integer.parseInt(input); + if (ticks < 1) { + errorAny(player, "Ticks must be at least 1!"); + return; + } + setter.setRefresh(ticks, inputTicks); + getConfig().save(); + successAny(player, "Set refresh delay to {0} ticks", ticks); + openTicksConfigGui(player, backGui, name, icon, ticks, inputTicks, setter); + } catch (NumberFormatException ex) { + errorAny(player, "Please input a valid number of ticks."); + } + } + }) + + .callback("input_ticks", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + try { + int ticks = Integer.parseInt(input); + if (ticks < 1) { + errorAny(player, "Ticks must be at least 1!"); + return; + } + setter.setRefresh(refreshTicks, ticks); + getConfig().save(); + successAny(player, "Set input cooldown to {0} ticks", ticks); + openTicksConfigGui(player, backGui, name, icon, refreshTicks, ticks, setter); + } catch (NumberFormatException ex) { + errorAny(player, "Please input a valid number of ticks."); + } + } + }) + + .item(0, ItemBuilder.integerItem(icon, "Refresh Delay", List.of( + "How many ticks between item", + "replenishment in the GUI", + "", + "Current: " + refreshTicks + " ticks", + "(" + (refreshTicks / 20.0) + " seconds)", + "", + "Click to modify" + ), refreshTicks), (g, e) -> + getDupe().getGuiListener().requestChatInput(g, player, "refresh_ticks", + "Enter the refresh delay in ticks:\n20 ticks = 1 second\n\nCurrent: " + refreshTicks + " ticks")) + + + .fillEmpty(EMPTY()) + .build(); + + if (inputTicks != null) { + gui.updateItem(1, ItemBuilder.integerItem(Material.CLOCK, "Input Cooldown", List.of( + "How many ticks players must wait", + "before changing the input again", + "", + "Current: " + inputTicks + " ticks", + "(" + (inputTicks / 20.0) + " seconds)", + "", + "Click to modify" + ), inputTicks), (g, e) -> { + getDupe().getGuiListener().requestChatInput(g, player, "input_ticks", + "Enter the input cooldown in ticks:\n20 ticks = 1 second\n\nCurrent: " + inputTicks + " ticks"); + }); + } + + gui.open(player); + } + + private Material getMaterialForTag(ItemTag tag) { + return switch (tag) { + case UNIQUE -> Material.EMERALD_BLOCK; + case FINAL -> Material.REDSTONE_BLOCK; + case INFINITE -> Material.LAPIS_BLOCK; + case PROTECTED -> Material.STRUCTURE_BLOCK; + }; + } + + private String getTagColor(ItemTag tag) { + return switch (tag) { + case UNIQUE -> "green"; + case FINAL -> "red"; + case INFINITE -> "blue"; + case PROTECTED -> "dark_purple"; + }; + } + + @FunctionalInterface + private interface TicksConfigSetter { + void setRefresh(int refreshTicks, Integer inputTicks); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleArmorTrimEditor.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleArmorTrimEditor.java new file mode 100644 index 0000000..c7e94b8 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleArmorTrimEditor.java @@ -0,0 +1,190 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.data.enums.ValidTrimMaterial; +import me.trouper.alias.data.enums.ValidTrimPattern; +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.utils.FormatUtils; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class GlobalRuleArmorTrimEditor implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRuleArmorTrimEditor(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + public void open(Player player) { + QuickGui gui = QuickGui.create() + .titleMini("Armor Trim Criteria") + .rows(4) + .fillBorder(EMPTY(Material.BROWN_STAINED_GLASS_PANE)) + + // Back button + .item(0, BACK(), (g, e) -> manager.openGlobalRuleEditor(player, rule)) + + // Pattern section + .item(11, ItemBuilder.create(Material.NETHERITE_UPGRADE_SMITHING_TEMPLATE) + .displayName("Trim Patterns") + .loreMiniMessage(Arrays.asList( + "Select required armor", + "trim patterns", + "", + "Selected: " + rule.trimPatterns.size() + " patterns", + !rule.trimPatterns.isEmpty() ? "" + rule.trimPatterns.stream() + .limit(3) + .map(FormatUtils::formatEnum) + .collect(Collectors.joining(", ")) : "", + rule.trimPatterns.size() > 3 ? "... and " + (rule.trimPatterns.size() - 3) + " more" : "", + "", + "Click to manage patterns" + )) + .build(), (g, e) -> openPatternSelector(player)) + + // Material section + .item(15, ItemBuilder.create(Material.COPPER_INGOT) + .displayName("Trim Materials") + .loreMiniMessage(Arrays.asList( + "Select required armor", + "trim materials", + "", + "Selected: " + rule.trimMaterials.size() + " materials", + !rule.trimMaterials.isEmpty() ? "" + rule.trimMaterials.stream() + .limit(3) + .map(FormatUtils::formatEnum) + .collect(Collectors.joining(", ")) : "", + rule.trimMaterials.size() > 3 ? "... and " + (rule.trimMaterials.size() - 3) + " more" : "", + "", + "Click to manage materials" + )) + .build(), (g, e) -> openMaterialSelector(player)) + + // Clear all button + .item(22, ItemBuilder.create(Material.BARRIER) + .displayName("Clear All Trim Requirements") + .loreMiniMessage(Arrays.asList( + "Remove all trim criteria", + "", + "Click to clear" + )) + .build(), (g, e) -> { + rule.trimPatterns.clear(); + rule.trimMaterials.clear(); + getConfig().save(); + successAny(player, "Cleared all armor trim requirements"); + open(player); + }) + + .fillEmpty(EMPTY()) + .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f) + .build(); + + gui.open(player); + } + + private void openPatternSelector(Player player) { + QuickGui.GuiBuilder builder = QuickGui.create() + .titleMini("Select Trim Patterns") + .rows(6) + .fillBorder(EMPTY(Material.ORANGE_STAINED_GLASS_PANE)) + .item(0, BACK(), (g, e) -> open(player)); + + List patterns = Arrays.asList(ValidTrimPattern.values()); + int[] slots = { + 11, 12, 13, 14, 15, + 20, 21, 22, 23, 24, + 29, 30, 31, 32, 33, + 39, 40, 41 + }; + + for (int i = 0; i < patterns.size(); i++) { + ValidTrimPattern pattern = patterns.get(i); + boolean selected = rule.trimPatterns.contains(pattern); + ItemBuilder item = ItemBuilder.create(pattern.getMaterial()) + .displayName((selected ? "" : "") + "" + pattern.name()) + .loreMiniMessage("Pattern: " + pattern.name(),"", selected ? "Status: SELECTED" : "Status: NOT SELECTED","","Click to toggle") + .hideAllFlags(); + + if (selected) item.enchant(Enchantment.MENDING); + + builder.item(slots[i], item.build(), (g, e) -> { + if (rule.trimPatterns.contains(pattern)) { + rule.trimPatterns.remove(pattern); + infoAny(player, "Removed {0} pattern requirement", pattern.name()); + } else { + rule.trimPatterns.add(pattern); + successAny(player, "Added {0} pattern requirement", pattern.name()); + } + getConfig().save(); + openPatternSelector(player); + }); + } + + builder.fillEmpty(EMPTY()).build().open(player); + } + + private void openMaterialSelector(Player player) { + QuickGui.GuiBuilder builder = QuickGui.create() + .titleMini("Select Trim Materials") + .rows(5) + .fillBorder(EMPTY(Material.CYAN_STAINED_GLASS_PANE)) + .item(0, BACK(), (g, e) -> open(player)); + + List materials = Arrays.asList(ValidTrimMaterial.values()); + + int[] slots = { + 11, 13, 15, + 20, 21, 22, 23, 24, + 29, 31, 33 + }; + + + for (int i = 0; i < materials.size(); i++) { + ValidTrimMaterial material = materials.get(i); + boolean selected = rule.trimMaterials.contains(material); + + ItemBuilder item = ItemBuilder.create(materials.get(i).getMaterial()) + .displayName((selected ? "" : "") + "" + material.name()) + .loreMiniMessage( + "Material: " + material.name(), + "", + "Status: " + (selected ? "SELECTED" : "NOT SELECTED"), + "", + "Click to toggle" + ) + .hideAllFlags(); + + if (selected) item.enchant(Enchantment.MENDING); + + builder.item(slots[i], item.build(), (g, e) -> { + if (rule.trimMaterials.contains(material)) { + rule.trimMaterials.remove(material); + infoAny(player, "Removed {0} material requirement", material.name()); + } else { + rule.trimMaterials.add(material); + successAny(player, "Added {0} material requirement", material.name()); + } + getConfig().save(); + openMaterialSelector(player); + }); + } + + builder.fillEmpty(EMPTY()).build().open(player); + } + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleAttributeEditor.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleAttributeEditor.java new file mode 100644 index 0000000..b593d8f --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleAttributeEditor.java @@ -0,0 +1,136 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.data.enums.ValidAttribute; +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class GlobalRuleAttributeEditor extends QuickPaginatedGUI implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRuleAttributeEditor(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + @Override + protected String getTitle(Player player) { + return "Attribute Criteria"; + } + + @Override + protected List getAllItems(Player player) { + return Arrays.asList(ValidAttribute.values()); + } + + @Override + protected ItemStack createDisplayItem(ValidAttribute attribute) { + Double value = rule.attributes.get(attribute); + boolean hasAttribute = value != null; + + ItemBuilder builder = ItemBuilder.create(hasAttribute ? Material.ENCHANTED_BOOK : Material.BOOK) + .displayName((hasAttribute ? "" : "") + "" + attribute.name()) + .loreMiniMessage("Attribute: " + attribute.name()); + + if (hasAttribute) { + builder.loreMiniMessage( + "", + "Required Value: " + String.format("%.2f", value), + "", + "Left-click to change value", + "Right-click to remove" + ); + } else { + builder.loreMiniMessage( + "", + "Not required", + "", + "Click to add requirement" + ); + } + + return builder.build(); + } + + @Override + protected void handleItemClick(Player player, ValidAttribute attribute, InventoryClickEvent event) { + if (event.isRightClick() && rule.attributes.containsKey(attribute)) { + rule.attributes.remove(attribute); + getConfig().save(); + successAny(player, "Removed {0} requirement", attribute.name()); + createGUI(player).open(player); + return; + } + + QuickGui inputGui = QuickGui.create() + .titleMini("Set Attribute Value") + .rows(3) + .item(13, ItemBuilder.create(Material.EXPERIENCE_BOTTLE) + .displayName("" + attribute.name()) + .loreMiniMessage(Arrays.asList( + "Enter the minimum value", + "required for this attribute.", + "", + "Current: " + + (rule.attributes.containsKey(attribute) ? String.format("%.2f", rule.attributes.get(attribute)) : "Not set"), + "", + "Click to set value" + )) + .build(), (g, e) -> getDupe().getGuiListener().requestChatInput(g, player, "attributeValue", "Enter minimum attribute value.")) + .item(22, BACK(), (g, e) -> createGUI(player).open(player)) // Back button. + .fillEmpty(EMPTY()) + .callback("attributeValue", (gui, p, input, source) -> { + try { + double value = Double.parseDouble(input); + rule.attributes.put(attribute, value); + getConfig().save(); + successAny(player, "Set {0} requirement to {1}", attribute.name(), String.format("%.2f", value)); + } catch (NumberFormatException ex) { + if (input.contains("cancel")) { + successAny(player,"Canceled."); + } else { + errorAny(player, "Invalid number: {0}", input); + getDupe().getGuiListener().requestChatInput(gui, player, "attributeValue", "Enter minimum attribute value."); + return; + } + } + createGUI(player).open(player); + }) + .build(); + inputGui.open(player); + } + + @Override + protected void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters) { + filterGui.item(0, createFilterToggleItem("Selected Only", Material.LIME_DYE, filters.contains("S")), + (gui, event) -> toggleFilter(player, "S")); + } + + @Override + protected boolean testFilter(Player player, ValidAttribute attribute, String filterKey) { + return switch (filterKey) { + case "S" -> rule.attributes.containsKey(attribute); + default -> false; + }; + } + + @Override + protected void openBackGUI(Player player) { + manager.openGlobalRuleEditor(player, rule); + } +} diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEditorGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEditorGui.java new file mode 100644 index 0000000..a78f820 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEditorGui.java @@ -0,0 +1,269 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.ItemTag; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class GlobalRuleEditorGui implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRuleEditorGui(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + public void open(Player player) { + QuickGui gui = QuickGui.create() + .titleMini("Rule Editor") + .rows(6) + .fillBorder(EMPTY(Material.ORANGE_STAINED_GLASS_PANE)) + + // Back button + .item(0, BACK(), (g, e) -> manager.openGlobalRuleList(player)) + + // Applied Tags Section + .item(10, createTagItem(ItemTag.UNIQUE), + (g, e) -> toggleTag(player, ItemTag.UNIQUE)) + .item(11, createTagItem(ItemTag.FINAL), + (g, e) -> toggleTag(player, ItemTag.FINAL)) + .item(12, createTagItem(ItemTag.INFINITE), + (g, e) -> toggleTag(player, ItemTag.INFINITE)) + .item(13, createTagItem(ItemTag.PROTECTED), + (g, e) -> toggleTag(player, ItemTag.PROTECTED)) + + // Match Mode + .item(16, createMatchModeItem(), + (g, e) -> cycleMatchMode(player)) + + // Criteria Items + .item(19, createCriteriaItem("Name Regex", Material.NAME_TAG, + !rule.nameContainsRegex.isEmpty(), rule.nameContainsRegex), + (g, e) -> manager.openNameCriteriaEditor(player, rule)) + + .item(20, createCriteriaItem("Lore Regex", Material.WRITABLE_BOOK, + !rule.loreContainsRegex.isEmpty(), rule.loreContainsRegex), + (g, e) -> manager.openLoreCriteriaEditor(player, rule)) + + .item(21, createCriteriaItem("Enchantments", Material.ENCHANTED_BOOK, + !rule.enchantments.isEmpty(), rule.enchantments.size() + " enchants"), + (g, e) -> manager.openEnchantmentEditor(player, rule)) + + .item(22, createCriteriaItem("Attributes", Material.GOLDEN_APPLE, + !rule.attributes.isEmpty(), rule.attributes.size() + " attributes"), + (g, e) -> manager.openAttributeEditor(player, rule)) + + .item(23, createCriteriaItem("Item Flags", Material.WHITE_BANNER, + !rule.itemFlags.isEmpty(), rule.itemFlags.size() + " flags"), + (g, e) -> manager.openItemFlagEditor(player, rule)) + + .item(24, createCriteriaItem("Model Data", Material.COMPASS, + !rule.legacyModelData.isEmpty(), rule.legacyModelData.size() + " values"), + (g, e) -> manager.openModelDataEditor(player, rule)) + + .item(25, createCriteriaItem("Potion Effects", Material.POTION, + !rule.potionEffects.isEmpty(), rule.potionEffects.size() + " effects"), + (g, e) -> manager.openPotionEffectEditor(player, rule)) + + // Material Settings + .item(30, createMaterialModeItem(), + (g, e) -> cycleMaterialMode(player)) + + .item(31, createMaterialListItem(), + (g, e) -> { + if (rule.materialMode != GlobalRule.MaterialMatchMode.IGNORE) { + manager.openMaterialSelector(player, rule); + } + }) + + // Armor Trim + .item(32, createCriteriaItem("Armor Trim", Material.NETHERITE_CHESTPLATE, + !rule.trimPatterns.isEmpty() || !rule.trimMaterials.isEmpty(), + (rule.trimPatterns.size() + rule.trimMaterials.size()) + " selected"), + (g, e) -> manager.openArmorTrimEditor(player, rule)) + + // Save button + .item(40, ItemBuilder.create(Material.LIME_DYE) + .displayName("Save & Return") + .loreMiniMessage(Arrays.asList( + "Save changes and return", + "to the rule list", + "", + "Click to save" + )) + .build(), (g, e) -> { + getConfig().save(); + successAny(player, "Saved global rule"); + manager.openGlobalRuleList(player); + }) + + .fillEmpty(EMPTY()) + .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f) + .build(); + + gui.open(player); + } + + private ItemStack createTagItem(ItemTag tag) { + boolean active = rule.appliedTags.contains(tag); + Material material = switch (tag) { + case UNIQUE -> Material.EMERALD_BLOCK; + case FINAL -> Material.REDSTONE_BLOCK; + case INFINITE -> Material.LAPIS_BLOCK; + case PROTECTED -> Material.STRUCTURE_BLOCK; + }; + + return ItemBuilder.create(material) + .displayName("<" + (active ? "green" : "gray") + ">" + tag.getName() + " Tag") + .loreMiniMessage(Arrays.asList( + "" + tag.getDesc(), + "", + "Status: " + (active ? "ACTIVE" : "INACTIVE"), + "", + "Click to toggle" + )) + .build(); + } + + private ItemStack createMatchModeItem() { + return ItemBuilder.create(Material.COMPARATOR) + .displayName("Match Mode: " + rule.matchMode.name()) + .loreMiniMessage(Arrays.asList( + "Determines how multiple criteria", + "are evaluated together", + "", + "Current: " + rule.matchMode.name(), + "", + "• AND: All criteria must match", + "• OR: Any criteria must match", + "• NAND: Not all criteria match", + "• XOR: Exactly one criteria matches", + "", + "Click to cycle" + )) + .build(); + } + + private ItemStack createCriteriaItem(String name, Material material, boolean hasValue, String preview) { + List lore = new ArrayList<>(); + lore.add("Configure " + name.toLowerCase() + " criteria"); + lore.add(""); + + if (hasValue) { + lore.add("Current: " + preview); + } else { + lore.add("Current: Not set"); + } + + lore.add(""); + lore.add("Click to edit"); + + return ItemBuilder.create(material) + .displayName("" + name) + .loreMiniMessage(lore) + .build(); + } + + private ItemStack createMaterialModeItem() { + return ItemBuilder.create(Material.GRASS_BLOCK) + .displayName("Material Mode: " + rule.materialMode.name()) + .loreMiniMessage(Arrays.asList( + "Control which materials this", + "rule applies to", + "", + "Current: " + rule.materialMode.name(), + "", + "• IGNORE: Applies to all materials", + "• WHITELIST: Only listed materials", + "• BLACKLIST: Exclude listed materials", + "", + "Click to cycle" + )) + .build(); + } + + private ItemStack createMaterialListItem() { + if (rule.materialMode == GlobalRule.MaterialMatchMode.IGNORE) { + return ItemBuilder.create(Material.GRAY_DYE) + .displayName("Material List") + .loreMiniMessage(Arrays.asList( + "Set material mode to", + "WHITELIST or BLACKLIST", + "to configure materials" + )) + .build(); + } + + List lore = new ArrayList<>(); + lore.add("Manage materials for this rule"); + lore.add(""); + lore.add("Selected: " + rule.effectedMaterials.size() + " materials"); + + if (!rule.effectedMaterials.isEmpty()) { + lore.add(""); + List materialNames = rule.effectedMaterials.stream() + .limit(5) + .map(mat -> mat.name()) + .collect(Collectors.toList()); + + for (String mat : materialNames) { + lore.add("• " + mat); + } + + if (rule.effectedMaterials.size() > 5) { + lore.add("... and " + (rule.effectedMaterials.size() - 5) + " more"); + } + } + + lore.add(""); + lore.add("Click to manage"); + + return ItemBuilder.create(Material.CHEST) + .displayName("Material List") + .loreMiniMessage(lore) + .build(); + } + + private void toggleTag(Player player, ItemTag tag) { + if (rule.appliedTags.contains(tag)) { + rule.appliedTags.remove(tag); + } else { + rule.appliedTags.add(tag); + } + open(player); + } + + private void cycleMatchMode(Player player) { + GlobalRule.MatchMode[] modes = GlobalRule.MatchMode.values(); + int current = rule.matchMode.ordinal(); + rule.matchMode = modes[(current + 1) % modes.length]; + open(player); + } + + private void cycleMaterialMode(Player player) { + GlobalRule.MaterialMatchMode[] modes = GlobalRule.MaterialMatchMode.values(); + int current = rule.materialMode.ordinal(); + rule.materialMode = modes[(current + 1) % modes.length]; + + // Clear materials if switching to IGNORE + if (rule.materialMode == GlobalRule.MaterialMatchMode.IGNORE) { + rule.effectedMaterials.clear(); + } + + open(player); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEnchantmentEditor.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEnchantmentEditor.java new file mode 100644 index 0000000..3c7cf3c --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEnchantmentEditor.java @@ -0,0 +1,149 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.data.enums.ValidEnchantment; +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.*; + +public class GlobalRuleEnchantmentEditor extends QuickPaginatedGUI implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRuleEnchantmentEditor(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + @Override + protected String getTitle(Player player) { + return "Enchantment Criteria"; + } + + @Override + protected List getAllItems(Player player) { + return Arrays.asList(ValidEnchantment.values()); + } + + @Override + protected ItemStack createDisplayItem(ValidEnchantment enchant) { + Integer level = rule.enchantments.get(enchant); + boolean hasEnchant = level != null; + + ItemBuilder builder = ItemBuilder.create(hasEnchant ? Material.ENCHANTED_BOOK : Material.BOOK) + .displayName((hasEnchant ? "" : "") + "" + enchant.name()) + .loreMiniMessage("Enchantment: " + enchant.name()); + + if (hasEnchant) { + builder.loreMiniMessage( + "", + "Required Level: " + level, + "", + "Left-click to change level", + "Right-click to remove" + ); + } else { + builder.loreMiniMessage( + "", + "Not required", + "", + "Click to add requirement" + ); + } + + return builder.build(); + } + + @Override + protected void handleItemClick(Player player, ValidEnchantment enchant, InventoryClickEvent event) { + if (event.isRightClick() && rule.enchantments.containsKey(enchant)) { + rule.enchantments.remove(enchant); + getConfig().save(); + successAny(player, "Removed {0} requirement", enchant.name()); + createGUI(player).open(player); + } else { + QuickGui inputGui = QuickGui.create() + .titleMini("Set Enchantment Level") + .rows(3) + .item(13, ItemBuilder.create(Material.EXPERIENCE_BOTTLE) + .displayName("" + enchant.name()) + .loreMiniMessage(Arrays.asList( + "Enter the minimum level", + "required for this enchantment", + "", + "Current: " + + (rule.enchantments.containsKey(enchant) ? rule.enchantments.get(enchant) : "Not set"), + "", + "Click to set level" + )) + .build(), (g, e) -> getDupe().getGuiListener().requestChatInput(g,player, "enchantLevel","Enter minimum enchant level starting at 1.")) + .item(22, BACK(), (g, e) -> createGUI(player).open(player)) + .fillEmpty(EMPTY()) + .callback("enchantLevel", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + try { + int level = Integer.parseInt(input); + if (level < 1) { + errorAny(player, "Level must be at least 1"); + } else { + rule.enchantments.put(enchant, level); + getConfig().save(); + successAny(player, "Set {0} requirement to level {1}", enchant.name(), level); + } + } catch (NumberFormatException ex) { + errorAny(player, "Invalid number: {0}", input); + } + createGUI(player).open(player); + } + }) + .build(); + inputGui.open(player); + } + } + + @Override + protected void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters) { + filterGui.item(0, createFilterToggleItem("Selected Only", Material.LIME_DYE, filters.contains("S")), + (gui, event) -> toggleFilter(player, "S")); + filterGui.item(1, createFilterToggleItem("Weapon", Material.DIAMOND_SWORD, filters.contains("W")), + (gui, event) -> toggleFilter(player, "W")); + filterGui.item(2, createFilterToggleItem("Tool", Material.DIAMOND_PICKAXE, filters.contains("T")), + (gui, event) -> toggleFilter(player, "T")); + filterGui.item(3, createFilterToggleItem("Armor", Material.DIAMOND_CHESTPLATE, filters.contains("A")), + (gui, event) -> toggleFilter(player, "A")); + } + + @Override + protected boolean testFilter(Player player, ValidEnchantment enchant, String filterKey) { + return switch (filterKey) { + case "S" -> rule.enchantments.containsKey(enchant); + case "W" -> enchant.name().contains("SHARPNESS") || enchant.name().contains("SMITE") || + enchant.name().contains("BANE") || enchant.name().contains("KNOCKBACK") || + enchant.name().contains("FIRE_ASPECT") || enchant.name().contains("LOOTING") || + enchant.name().contains("SWEEPING"); + case "T" -> enchant.name().contains("EFFICIENCY") || enchant.name().contains("SILK_TOUCH") || + enchant.name().contains("UNBREAKING") || enchant.name().contains("FORTUNE"); + case "A" -> enchant.name().contains("PROTECTION") || enchant.name().contains("THORNS") || + enchant.name().contains("RESPIRATION") || enchant.name().contains("AQUA_AFFINITY") || + enchant.name().contains("FEATHER_FALLING") || enchant.name().contains("DEPTH_STRIDER") || + enchant.name().contains("FROST_WALKER"); + default -> false; + }; + } + + @Override + protected void openBackGUI(Player player) { + manager.openGlobalRuleEditor(player, rule); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleItemFlagEditor.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleItemFlagEditor.java new file mode 100644 index 0000000..8a5c872 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleItemFlagEditor.java @@ -0,0 +1,119 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; + +public class GlobalRuleItemFlagEditor implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRuleItemFlagEditor(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + @SuppressWarnings("deprecation") + public void open(Player player) { + QuickGui gui = QuickGui.create() + .titleMini("Item Flag Criteria") + .rows(4) + .fillBorder(EMPTY(Material.ORANGE_STAINED_GLASS_PANE)) + + // Back button + .item(0, BACK(), (g, e) -> manager.openGlobalRuleEditor(player, rule)) + + // Item flags + .item(10, createFlagItem(ItemFlag.HIDE_ENCHANTS), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_ENCHANTS)) + .item(11, createFlagItem(ItemFlag.HIDE_ATTRIBUTES), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_ATTRIBUTES)) + .item(12, createFlagItem(ItemFlag.HIDE_UNBREAKABLE), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_UNBREAKABLE)) + .item(13, createFlagItem(ItemFlag.HIDE_DESTROYS), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_DESTROYS)) + .item(14, createFlagItem(ItemFlag.HIDE_PLACED_ON), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_PLACED_ON)) + .item(15, createFlagItem(ItemFlag.HIDE_ADDITIONAL_TOOLTIP), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_ADDITIONAL_TOOLTIP)) + .item(16, createFlagItem(ItemFlag.HIDE_DYE), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_DYE)) + .item(19, createFlagItem(ItemFlag.HIDE_ARMOR_TRIM), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_ARMOR_TRIM)) + .item(20, createFlagItem(ItemFlag.HIDE_STORED_ENCHANTS), + (g, e) -> toggleFlag(player, ItemFlag.HIDE_STORED_ENCHANTS)) + + // Clear all button + .item(22, ItemBuilder.create(Material.BARRIER) + .displayName("Clear All Flags") + .loreMiniMessage(Arrays.asList( + "Remove all flag requirements", + "", + "Click to clear" + )) + .build(), (g, e) -> { + rule.itemFlags.clear(); + getConfig().save(); + successAny(player, "Cleared all item flag requirements"); + open(player); + }) + + .fillEmpty(EMPTY()) + .clickSound(Sound.UI_BUTTON_CLICK, 0.7f, 1.2f) + .build(); + + gui.open(player); + } + + private ItemStack createFlagItem(ItemFlag flag) { + boolean required = rule.itemFlags.contains(flag); + + return ItemBuilder.create(required ? Material.WHITE_BANNER : Material.LIME_BANNER) + .displayName((required ? "" : "") + "" + flag.name()) + .loreMiniMessage(Arrays.asList( + "" + getFlagDescription(flag), + "", + "Status: " + (required ? "REQUIRED" : "NOT REQUIRED"), + "", + "Click to toggle" + )) + .build(); + } + + private String getFlagDescription(ItemFlag flag) { + return switch (flag) { + case HIDE_ENCHANTS -> "Hides enchantments from item tooltip"; + case HIDE_ATTRIBUTES -> "Hides attribute modifiers"; + case HIDE_UNBREAKABLE -> "Hides the unbreakable tag"; + case HIDE_DESTROYS -> "Hides what blocks can be destroyed"; + case HIDE_PLACED_ON -> "Hides what blocks can be placed on"; + case HIDE_ADDITIONAL_TOOLTIP -> "Hides additional tooltip info"; + case HIDE_DYE -> "Hides dye color from items"; + case HIDE_ARMOR_TRIM -> "Hides armor trim information"; + case HIDE_STORED_ENCHANTS -> "Hides stored enchantments (books)"; + }; + } + + private void toggleFlag(Player player, ItemFlag flag) { + if (rule.itemFlags.contains(flag)) { + rule.itemFlags.remove(flag); + infoAny(player, "Removed {0} requirement", flag.name()); + } else { + rule.itemFlags.add(flag); + successAny(player, "Added {0} requirement", flag.name()); + } + getConfig().save(); + open(player); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleListGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleListGui.java new file mode 100644 index 0000000..1152059 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleListGui.java @@ -0,0 +1,212 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.data.enums.ValidTrimMaterial; +import me.trouper.alias.data.enums.ValidTrimPattern; +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.ItemTag; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.stream.Collectors; + +public class GlobalRuleListGui extends QuickPaginatedGUI implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + + public GlobalRuleListGui(AdminPanelManager manager) { + this.manager = manager; + } + + @Override + protected String getTitle(Player player) { + return "Global Rules Manager"; + } + + @Override + protected List getAllItems(Player player) { + return new ArrayList<>(getConfig().globalRules); + } + + @Override + protected ItemStack createDisplayItem(GlobalRule rule) { + List lore = new ArrayList<>(); + + // Display applied tags + if (!rule.appliedTags.isEmpty()) { + lore.add("Applied Tags:"); + for (ItemTag tag : rule.appliedTags) { + lore.add("• <" + manager.getTagColor(tag) + ">" + tag.getName()); + } + } else { + lore.add("No tags applied"); + } + + lore.add(""); + lore.add("Match Mode: " + rule.matchMode.name()); + lore.add("Criteria Count: " + rule.getCriteriaCount()); + + // Show material mode if applicable + if (rule.materialMode != GlobalRule.MaterialMatchMode.IGNORE) { + lore.add("Material Mode: " + rule.materialMode.name()); + if (!rule.effectedMaterials.isEmpty()) { + lore.add("Materials: " + rule.effectedMaterials.size() + " selected"); + } + } + + // Quick preview of criteria + List criteria = new ArrayList<>(); + if (!rule.nameContainsRegex.isEmpty()) criteria.add("Name"); + if (!rule.loreContainsRegex.isEmpty()) criteria.add("Lore"); + if (!rule.enchantments.isEmpty()) criteria.add("Enchants"); + if (!rule.attributes.isEmpty()) criteria.add("Attributes"); + if (!rule.itemFlags.isEmpty()) criteria.add("Flags"); + if (!rule.legacyModelData.isEmpty()) criteria.add("Model Data"); + if (!rule.potionEffects.isEmpty()) criteria.add("Potion Effects"); + if (!rule.trimPatterns.isEmpty() || !rule.trimMaterials.isEmpty()) criteria.add("Armor Trim"); + + if (!criteria.isEmpty()) { + lore.add("Criteria: " + String.join(", ", criteria)); + } + + lore.add(""); + lore.add("Left-click to edit"); + lore.add("Right-click to delete"); + lore.add("Shift-click to duplicate"); + + // Determine icon based on tags or criteria + Material icon = Material.WRITTEN_BOOK; + if (rule.appliedTags.contains(ItemTag.UNIQUE)) icon = Material.EMERALD; + else if (rule.appliedTags.contains(ItemTag.FINAL)) icon = Material.REDSTONE; + else if (rule.appliedTags.contains(ItemTag.INFINITE)) icon = Material.LAPIS_LAZULI; + else if (rule.appliedTags.contains(ItemTag.PROTECTED)) icon = Material.NETHER_STAR; + else if (!rule.enchantments.isEmpty()) icon = Material.ENCHANTED_BOOK; + else if (!rule.potionEffects.isEmpty()) icon = Material.POTION; + else if (!rule.trimPatterns.isEmpty() || !rule.trimMaterials.isEmpty()) icon = Material.NETHERITE_CHESTPLATE; + + String tagNames = rule.appliedTags.stream() + .map(ItemTag::getName) + .collect(Collectors.joining(", ")); + + return ItemBuilder.create(icon) + .displayName("Rule #" + (getConfig().globalRules.indexOf(rule) + 1) + + (tagNames.isEmpty() ? "" : " (" + tagNames + ")")) + .loreMiniMessage(lore) + .build(); + } + + @Override + protected void handleItemClick(Player player, GlobalRule rule, InventoryClickEvent event) { + if (event.isRightClick()) { + // Confirm deletion + QuickGui.create() + .titleMini("Delete Rule?") + .rows(3) + .item(12, ItemBuilder.create(Material.RED_CONCRETE) + .displayName("Delete Rule") + .loreMiniMessage("This action cannot be undone!") + .build(), (gui, e) -> { + getConfig().globalRules.remove(rule); + getConfig().save(); + successAny(player, "Deleted global rule"); + createGUI(player).open(player); + }) + .item(14, ItemBuilder.create(Material.GREEN_CONCRETE) + .displayName("Cancel") + .build(), (gui, e) -> createGUI(player).open(player)) + .fillEmpty(EMPTY()) + .build() + .open(player); + } else if (event.isShiftClick()) { + // Duplicate rule + GlobalRule duplicate = new GlobalRule(); + + // Copy all fieldsx + duplicate.matchMode = rule.matchMode; + duplicate.materialMode = rule.materialMode; + duplicate.effectedMaterials = EnumSet.copyOf(rule.effectedMaterials); + duplicate.nameContainsRegex = rule.nameContainsRegex; + duplicate.loreContainsRegex = rule.loreContainsRegex; + duplicate.legacyModelData = new HashSet<>(rule.legacyModelData); + duplicate.itemFlags = EnumSet.copyOf(rule.itemFlags); + duplicate.enchantments = new HashMap<>(rule.enchantments); + duplicate.potionEffects = new HashMap<>(rule.potionEffects); + duplicate.attributes = new HashMap<>(rule.attributes); + duplicate.trimPatterns = rule.trimPatterns.isEmpty() ? EnumSet.noneOf(ValidTrimPattern.class) : EnumSet.copyOf(rule.trimPatterns); + duplicate.trimMaterials = rule.trimMaterials.isEmpty() ? EnumSet.noneOf(ValidTrimMaterial.class) : EnumSet.copyOf(rule.trimMaterials); + duplicate.appliedTags = EnumSet.copyOf(rule.appliedTags); + + getConfig().globalRules.add(duplicate); + getConfig().save(); + + successAny(player, "Duplicated global rule"); + createGUI(player).open(player); + } else { + // Edit rule + manager.openGlobalRuleEditor(player, rule); + } + } + + @Override + protected void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters) { + filterGui.item(0, createFilterToggleItem("Has UNIQUE", Material.EMERALD_BLOCK, filters.contains("U")), + (gui, event) -> toggleFilter(player, "U")); + filterGui.item(1, createFilterToggleItem("Has FINAL", Material.REDSTONE_BLOCK, filters.contains("F")), + (gui, event) -> toggleFilter(player, "F")); + filterGui.item(2, createFilterToggleItem("Has INFINITE", Material.LAPIS_BLOCK, filters.contains("I")), + (gui, event) -> toggleFilter(player, "I")); + filterGui.item(3, createFilterToggleItem("Has PROTECTED", Material.COMMAND_BLOCK, filters.contains("P")), + (gui, event) -> toggleFilter(player, "P")); + filterGui.item(5, createFilterToggleItem("Uses Materials", Material.GRASS_BLOCK, filters.contains("M")), + (gui, event) -> toggleFilter(player, "M")); + filterGui.item(6, createFilterToggleItem("Has Criteria", Material.COMPARATOR, filters.contains("C")), + (gui, event) -> toggleFilter(player, "C")); + } + + @Override + protected boolean testFilter(Player player, GlobalRule rule, String filterKey) { + return switch (filterKey) { + case "U" -> rule.appliedTags.contains(ItemTag.UNIQUE); + case "F" -> rule.appliedTags.contains(ItemTag.FINAL); + case "I" -> rule.appliedTags.contains(ItemTag.INFINITE); + case "P" -> rule.appliedTags.contains(ItemTag.PROTECTED); + case "M" -> rule.materialMode != GlobalRule.MaterialMatchMode.IGNORE && !rule.effectedMaterials.isEmpty(); + case "C" -> rule.getCriteriaCount() > 0; + default -> false; + }; + } + + @Override + protected void openBackGUI(Player player) { + manager.openMainGui(player); + } + + @Override + public QuickGui createGUI(Player player) { + QuickGui gui = super.createGUI(player); + gui.updateItem(51, ItemBuilder.create(Material.LIME_DYE) + .displayName("+ Create New Rule") + .loreMiniMessage(Arrays.asList( + "Create a new global rule", + "to apply tags to items", + "", + "Click to create" + )) + .build(), (q, event) -> { + GlobalRule newRule = new GlobalRule(); + getConfig().globalRules.add(newRule); + getConfig().save(); + manager.openGlobalRuleEditor((Player) event.getWhoClicked(), newRule); + }); + return gui; + } + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleMaterialSelector.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleMaterialSelector.java new file mode 100644 index 0000000..e47e308 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleMaterialSelector.java @@ -0,0 +1,134 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; + +import java.util.*; + +public class GlobalRuleMaterialSelector extends QuickPaginatedGUI implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRuleMaterialSelector(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + @Override + protected String getTitle(Player player) { + return "Select Materials (" + rule.materialMode.name() + ")"; + } + + @Override + protected List getAllItems(Player player) { + return Arrays.stream(Material.values()) + .filter(mat -> !mat.isLegacy() && !mat.isAir() && mat.isItem()) + .toList(); + } + + @Override + protected ItemStack createDisplayItem(Material material) { + boolean selected = rule.effectedMaterials.contains(material); + ItemBuilder builder = ItemBuilder.create(material) + .displayName((selected ? "" : "") + "" + material.name()) + .loreMiniMessage("Material: " + material.name()); + + if (selected) { + builder.enchant(Enchantment.MENDING,1); + builder.loreMiniMessage( + "", + "✓ Selected", + "", + "Click to remove"); + } else { + builder.loreMiniMessage( + "", + "Not selected", + "", + "Click to add"); + } + + return builder.build(); + } + + @Override + protected void handleItemClick(Player player, Material material, InventoryClickEvent event) { + if (rule.effectedMaterials.contains(material)) { + rule.effectedMaterials.remove(material); + infoAny(player, "Removed {0} from material list", material.name()); + } else { + rule.effectedMaterials.add(material); + successAny(player, "Added {0} to material list", material.name()); + } + + getConfig().save(); + createGUI(player).open(player); + } + + @Override + protected void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters) { + filterGui.item(0, createFilterToggleItem("Selected Only", Material.LIME_DYE, filters.contains("S")), + (gui, event) -> toggleFilter(player, "S")); + filterGui.item(1, createFilterToggleItem("Blocks", Material.STONE, filters.contains("B")), + (gui, event) -> toggleFilter(player, "B")); + filterGui.item(2, createFilterToggleItem("Items", Material.STICK, filters.contains("I")), + (gui, event) -> toggleFilter(player, "I")); + filterGui.item(3, createFilterToggleItem("Tools", Material.DIAMOND_PICKAXE, filters.contains("T")), + (gui, event) -> toggleFilter(player, "T")); + filterGui.item(4, createFilterToggleItem("Armor", Material.DIAMOND_CHESTPLATE, filters.contains("A")), + (gui, event) -> toggleFilter(player, "A")); + } + + @Override + protected boolean testFilter(Player player, Material material, String filterKey) { + return switch (filterKey) { + case "S" -> rule.effectedMaterials.contains(material); + case "B" -> material.isBlock(); + case "I" -> !material.isBlock(); + case "T" -> material.name().contains("_AXE") || material.name().contains("_PICKAXE") || + material.name().contains("_SHOVEL") || material.name().contains("_HOE") || + material.name().contains("_SWORD"); + case "A" -> material.name().contains("_HELMET") || material.name().contains("_CHESTPLATE") || + material.name().contains("_LEGGINGS") || material.name().contains("_BOOTS"); + default -> false; + }; + } + + @Override + protected void openBackGUI(Player player) { + manager.openGlobalRuleEditor(player, rule); + } + + @Override + public QuickGui createGUI(Player player) { + QuickGui gui = super.createGUI(player); + + gui.updateItem(47, ItemBuilder.create(Material.BARRIER) + .displayName("Clear All") + .loreMiniMessage(Arrays.asList( + "Remove all selected materials", + "", + "Click to clear" + )) + .build(), (q, event) -> { + rule.effectedMaterials.clear(); + getConfig().save(); + successAny(event.getWhoClicked(), "Cleared all materials"); + createGUI((Player) event.getWhoClicked()).open((Player) event.getWhoClicked()); + }); + + return gui; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRulePotionEffectEditor.java b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRulePotionEffectEditor.java new file mode 100644 index 0000000..4d1ce72 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRulePotionEffectEditor.java @@ -0,0 +1,160 @@ +package me.trouper.dupealias.server.gui.admin.globalrule; + +import me.trouper.alias.data.enums.ValidPotionEffectType; +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.server.systems.gui.QuickPaginatedGUI; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.data.GlobalRule; +import me.trouper.dupealias.server.gui.CommonItems; +import me.trouper.dupealias.server.gui.admin.AdminPanelManager; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.*; + +public class GlobalRulePotionEffectEditor extends QuickPaginatedGUI implements DupeContext, CommonItems { + + private final AdminPanelManager manager; + private final GlobalRule rule; + + public GlobalRulePotionEffectEditor(AdminPanelManager manager, GlobalRule rule) { + this.manager = manager; + this.rule = rule; + } + + @Override + protected String getTitle(Player player) { + return "Potion Effect Criteria"; + } + + @Override + protected List getAllItems(Player player) { + return Arrays.asList(ValidPotionEffectType.values()); + } + + @Override + protected ItemStack createDisplayItem(ValidPotionEffectType effect) { + Integer amplifier = rule.potionEffects.get(effect); + boolean hasEffect = amplifier != null; + + Material icon = effect.name().contains("INSTANT") || effect.name().contains("HARM") ? + Material.SPLASH_POTION : Material.POTION; + + ItemBuilder builder = ItemBuilder.create(icon) + .displayName((hasEffect ? "" : "") + "" + effect.name()) + .loreMiniMessage("Effect: " + effect.name()); + + if (hasEffect) { + builder.loreMiniMessage( + "", + "Required Amplifier: " + amplifier, + "", + "Left-click to change amplifier", + "Right-click to remove" + ); + } else { + builder.loreMiniMessage( + "", + "Not required", + "", + "Click to add requirement" + ); + } + + return builder.build(); + } + + @Override + protected void handleItemClick(Player player, ValidPotionEffectType effect, InventoryClickEvent event) { + if (event.isRightClick() && rule.potionEffects.containsKey(effect)) { + rule.potionEffects.remove(effect); + getConfig().save(); + successAny(player, "Removed {0} requirement", effect.name()); + createGUI(player).open(player); + } else { + // Request amplifier input + QuickGui inputGui = QuickGui.create() + .titleMini("Set Effect Amplifier") + .rows(3) + .item(13, ItemBuilder.create(Material.BREWING_STAND) + .displayName("" + effect.name()) + .loreMiniMessage(Arrays.asList( + "Enter the minimum amplifier", + "required for this effect", + "(0 = Level I, 1 = Level II, etc.)", + "", + "Current: " + + (rule.potionEffects.containsKey(effect) ? rule.potionEffects.get(effect) : "Not set"), + "", + "Click to set amplifier" + )) + .build(), (g, e) -> getDupe().getGuiListener().requestChatInput(g,player, "effectAmplifier","Enter an integer to set the minimum effect amplifier. (Starting at 0)")) + .item(22, BACK(), (g, e) -> createGUI(player).open(player)) + .fillEmpty(EMPTY()) + .callback("effectAmplifier", new QuickGui.GuiCallback() { + @Override + public void onInput(QuickGui gui, Player player, String input, QuickGui.InputSource source) { + try { + int amplifier = Integer.parseInt(input); + if (amplifier < 0) { + errorAny(player, "Amplifier must be 0 or higher"); + } else { + rule.potionEffects.put(effect, amplifier); + getConfig().save(); + successAny(player, "Set {0} requirement to amplifier {1}", effect.name(), amplifier); + } + } catch (NumberFormatException ex) { + errorAny(player, "Invalid number: {0}", input); + } + createGUI(player).open(player); + } + }) + .build(); + inputGui.open(player); + } + } + + @Override + protected void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters) { + filterGui.item(0, createFilterToggleItem("Selected Only", Material.LIME_DYE, filters.contains("S")), + (gui, event) -> toggleFilter(player, "S")); + filterGui.item(1, createFilterToggleItem("Beneficial", Material.GOLDEN_APPLE, filters.contains("B")), + (gui, event) -> toggleFilter(player, "B")); + filterGui.item(2, createFilterToggleItem("Harmful", Material.POISONOUS_POTATO, filters.contains("H")), + (gui, event) -> toggleFilter(player, "H")); + filterGui.item(3, createFilterToggleItem("Neutral", Material.MILK_BUCKET, filters.contains("N")), + (gui, event) -> toggleFilter(player, "N")); + } + + @Override + protected boolean testFilter(Player player, ValidPotionEffectType effect, String filterKey) { + return switch (filterKey) { + case "S" -> rule.potionEffects.containsKey(effect); + case "B" -> effect.name().contains("SPEED") || effect.name().contains("HASTE") || + effect.name().contains("STRENGTH") || effect.name().contains("INSTANT_HEALTH") || + effect.name().contains("JUMP") || effect.name().contains("REGENERATION") || + effect.name().contains("RESISTANCE") || effect.name().contains("FIRE_RESISTANCE") || + effect.name().contains("WATER_BREATHING") || effect.name().contains("INVISIBILITY") || + effect.name().contains("NIGHT_VISION") || effect.name().contains("HEALTH_BOOST") || + effect.name().contains("ABSORPTION") || effect.name().contains("SATURATION") || + effect.name().contains("LUCK") || effect.name().contains("CONDUIT") || + effect.name().contains("DOLPHINS") || effect.name().contains("HERO"); + case "H" -> effect.name().contains("SLOWNESS") || effect.name().contains("MINING_FATIGUE") || + effect.name().contains("INSTANT_DAMAGE") || effect.name().contains("NAUSEA") || + effect.name().contains("BLINDNESS") || effect.name().contains("HUNGER") || + effect.name().contains("WEAKNESS") || effect.name().contains("POISON") || + effect.name().contains("WITHER") || effect.name().contains("UNLUCK") || + effect.name().contains("BAD_OMEN") || effect.name().contains("DARKNESS"); + case "N" -> !testFilter(player, effect, "B") && !testFilter(player, effect, "H"); + default -> false; + }; + } + + @Override + protected void openBackGUI(Player player) { + manager.openGlobalRuleEditor(player, rule); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeGui.java index e6d15a6..bbc9660 100644 --- a/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeGui.java +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeGui.java @@ -14,7 +14,6 @@ public abstract class AbstractDupeGui implements protected abstract T createSession(Player player); public T getSession(Player player) { - sessions.entrySet().removeIf(entry -> entry.getValue().isClosed()); return sessions.computeIfAbsent(player.getUniqueId(), uuid -> createSession(player)); } diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeSession.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeSession.java index dbe5895..2dbea63 100644 --- a/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeSession.java +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeSession.java @@ -25,8 +25,6 @@ public abstract class AbstractDupeSession implements DupeContext { protected abstract void tick(); - protected abstract long getTickDelay(Player player); - private void startTicking() { if (replicationTask != null && !replicationTask.isCancelled()) { replicationTask.cancel(); @@ -42,7 +40,7 @@ public abstract class AbstractDupeSession implements DupeContext { } tick(); } - }.runTaskTimer(getPlugin(), 0, getTickDelay(owner)); + }.runTaskTimer(getPlugin(), 0, 1); } public void close() { diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeChestGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeChestGui.java index 739de49..0c4f387 100644 --- a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeChestGui.java +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeChestGui.java @@ -6,6 +6,9 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import java.util.HashMap; +import java.util.Map; + public class DupeChestGui extends AbstractDupeGui { @Override @@ -13,10 +16,21 @@ public class DupeChestGui extends AbstractDupeGui { return new ChestSession(player); } + @Override + public ChestSession getSession(Player player) { + ChestSession session = super.getSession(player); + session.setDelayTicks(getDupe().getPermissionValue(player, "dupealias.gui.chest.refresh.", getConfig().chest.baseRefreshDelayTicks)); + session.open(); + return session; + } + public class ChestSession extends AbstractDupeSession { + private Map itemDelays = new HashMap<>(); + private int delayTicks; public ChestSession(Player owner) { super(owner, "DUPE CHEST", 6); + this.delayTicks = getDupe().getPermissionValue(owner, "dupealias.gui.chest.refresh.", getConfig().chest.baseRefreshDelayTicks); } @Override @@ -24,12 +38,19 @@ public class DupeChestGui extends AbstractDupeGui { return QuickGui.create() .titleMini(title) .rows(rows) - .onGlobalClick((g, e) -> e.setCancelled( - shouldBlockClick(e.getCursor()) || - shouldBlockClick(e.getCurrentItem()) || - (e.getClickedInventory() != null && shouldBlockClick(e.getClickedInventory().getItem(e.getSlot()))) - )) + .onGlobalClick((g, e) -> { + boolean shouldCancel = shouldBlockClick(e.getCursor()) || + shouldBlockClick(e.getCurrentItem()) || + (e.getClickedInventory() != null && shouldBlockClick(e.getClickedInventory().getItem(e.getSlot()))); + + if (!shouldCancel && e.getClickedInventory() != null) { + resetItemDelay(e.getSlot()); + } + + e.setCancelled(shouldCancel); + }) .allowDrag() + .clickSound(null,0,0) .onCreate((g, i) -> populateInventory(i)) .onClose((g, e) -> close()) .build(); @@ -40,22 +61,67 @@ public class DupeChestGui extends AbstractDupeGui { populateInventory(getGui().getInventory()); } - @Override - protected long getTickDelay(Player player) { - return 1; + private void resetItemDelay(int slot) { + ItemDelayInfo info = itemDelays.get(slot); + if (info != null) { + info.currentTicks = 0; + info.ready = false; + } } - } + private ItemStack getDelayedItem(int slot, ItemStack sourceItem) { + if (sourceItem == null) { + itemDelays.remove(slot); + return createPopulatedItem(null,1); + } - private void populateInventory(Inventory inv) { - for (int row = 0; row < 6; row++) { - int rowStart = row * 9; - inv.setItem(rowStart + 4, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - for (int col = 0; col < 4; col++) { - int leftIndex = rowStart + col; - int rightIndex = rowStart + 8 - col; - ItemStack leftItem = inv.getItem(leftIndex); - inv.setItem(rightIndex, createPopulatedItem(leftItem)); + ItemDelayInfo info = itemDelays.get(slot); + if (info == null) { + info = new ItemDelayInfo(sourceItem.clone()); + itemDelays.put(slot, info); + } + + if (!sourceItem.isSimilar(info.originalItem)) { + info = new ItemDelayInfo(sourceItem.clone()); + itemDelays.put(slot, info); + } + + if (!info.ready) { + info.currentTicks++; + double progress = Math.min(1.0, (double) info.currentTicks / delayTicks); + if (info.currentTicks >= delayTicks) { + info.ready = true; + } + return createPopulatedItem(sourceItem, progress); + } + + return createPopulatedItem(sourceItem, 1.0); + } + + private void populateInventory(Inventory inv) { + for (int row = 0; row < 6; row++) { + int rowStart = row * 9; + inv.setItem(rowStart + 4, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); + for (int col = 0; col < 4; col++) { + int leftIndex = rowStart + col; + int rightIndex = rowStart + 8 - col; + ItemStack leftItem = inv.getItem(leftIndex); + inv.setItem(rightIndex, getDelayedItem(rightIndex, leftItem)); + } + } + } + + public void setDelayTicks(int delayTicks) { + this.delayTicks = delayTicks; + } + + private static class ItemDelayInfo { + ItemStack originalItem; + int currentTicks = 0; + boolean ready = false; + + ItemDelayInfo(ItemStack item) { + this.originalItem = item; } } } diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeGui.java index 1dda1d3..5c391e5 100644 --- a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeGui.java +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeGui.java @@ -13,7 +13,7 @@ import org.bukkit.inventory.ItemStack; public class DupeGui implements DupeContext, CommonItems { - public final ReplicatorGui replicatorGui = new ReplicatorGui(); + public final DupeReplicatorGui replicatorGui = new DupeReplicatorGui(); public final DupeInventoryGui inventoryGui = new DupeInventoryGui(); public final DupeChestGui chestGui = new DupeChestGui(); @@ -81,7 +81,7 @@ public class DupeGui implements DupeContext, CommonItems { abstractDupeGui.getSession(player).getGui().open(player); } else { getVerbose().send("Creating new session for {0}",player.getName()); - player.openInventory(abstractDupeGui.createSession(player).open()); + player.openInventory(abstractDupeGui.getSession(player).open()); } } else { player.closeInventory(); diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeInventoryGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeInventoryGui.java index 55a4c2f..5485db5 100644 --- a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeInventoryGui.java +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeInventoryGui.java @@ -4,18 +4,33 @@ import me.trouper.alias.server.systems.gui.QuickGui; import org.bukkit.entity.Player; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.Map; public class DupeInventoryGui extends AbstractDupeGui { - + @Override protected InventorySession createSession(Player player) { return new InventorySession(player); } + @Override + public InventorySession getSession(Player player) { + InventorySession session = super.getSession(player); + session.setDelayTicks(getDupe().getPermissionValue(player, "dupealias.gui.inventory.refresh.", getConfig().inventory.baseRefreshDelayTicks)); + session.open(); + return session; + } + public class InventorySession extends AbstractDupeSession { + private Map itemDelays = new HashMap<>(); + private int delayTicks; public InventorySession(Player owner) { super(owner, "YOUR INVENTORY", 6); + this.delayTicks = getDupe().getPermissionValue(owner, "dupealias.gui.inventory.refresh.", getConfig().inventory.baseRefreshDelayTicks); } @Override @@ -23,12 +38,19 @@ public class DupeInventoryGui extends AbstractDupeGui e.setCancelled( - shouldBlockClick(e.getCursor()) || - shouldBlockClick(e.getCurrentItem()) || - (e.getClickedInventory() != null && shouldBlockClick(e.getClickedInventory().getItem(e.getSlot()))) - )) + .onGlobalClick((g, e) -> { + boolean shouldCancel = shouldBlockClick(e.getCursor()) || + shouldBlockClick(e.getCurrentItem()) || + (e.getClickedInventory() != null && shouldBlockClick(e.getClickedInventory().getItem(e.getSlot()))); + + if (!shouldCancel && e.getClickedInventory() != null) { + resetItemDelay(e.getSlot()); + } + + e.setCancelled(shouldCancel); + }) .allowDrag() + .clickSound(null,0,0) .onCreate((g, i) -> populateInventory(getOwner(), i)) .onClose((g, e) -> close()) .build(); @@ -39,35 +61,80 @@ public class DupeInventoryGui extends AbstractDupeGui= delayTicks) { + info.ready = true; + } + return createPopulatedItem(sourceItem, progress); + } + + return createPopulatedItem(sourceItem, 1.0); } - for (int i = 27; i < 36; i++) { - inv.setItem(i, createPopulatedItem(player.getInventory().getItem(i))); + + private void populateInventory(Player player, Inventory inv) { + for (int i = 0; i < 18; i++) { + inv.setItem(i, EMPTY()); + } + + inv.setItem(0, getDelayedItem(0, player.getInventory().getHelmet())); + inv.setItem(1, getDelayedItem(1, player.getInventory().getChestplate())); + inv.setItem(2, getDelayedItem(2, player.getInventory().getLeggings())); + inv.setItem(3, getDelayedItem(3, player.getInventory().getBoots())); + inv.setItem(6, getDelayedItem(6, player.getInventory().getItemInOffHand())); + + for (int i = 0; i < 9; i++) { + inv.setItem(i + 18, getDelayedItem(i + 18, player.getInventory().getItem(i))); + } + for (int i = 27; i < 36; i++) { + inv.setItem(i, getDelayedItem(i, player.getInventory().getItem(i))); + } + for (int i = 36; i < 45; i++) { + inv.setItem(i, getDelayedItem(i, player.getInventory().getItem(i - 18))); + } + for (int i = 45; i < 54; i++) { + inv.setItem(i, getDelayedItem(i, player.getInventory().getItem(i - 36))); + } } - for (int i = 36; i < 45; i++) { - inv.setItem(i, createPopulatedItem(player.getInventory().getItem(i - 18))); + + public void setDelayTicks(int delayTicks) { + this.delayTicks = delayTicks; } - for (int i = 45; i < 54; i++) { - inv.setItem(i, createPopulatedItem(player.getInventory().getItem(i - 36))); + + private static class ItemDelayInfo { + ItemStack originalItem; + int currentTicks = 0; + boolean ready = false; + + ItemDelayInfo(ItemStack item) { + this.originalItem = item; + } } } } \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeReplicatorGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeReplicatorGui.java new file mode 100644 index 0000000..9a9473d --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeReplicatorGui.java @@ -0,0 +1,227 @@ +package me.trouper.dupealias.server.gui.dupe; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.alias.utils.FormatUtils; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.alias.utils.SoundPlayer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class DupeReplicatorGui extends AbstractDupeGui { + + private final int[] inputRing = {1, 2, 3, 12, 21, 20, 19, 10}; + private final int[] outputRing = {5, 6, 7, 16, 25, 24, 23, 14}; + private final ItemStack emptyLightBlue = EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE); + private final ItemStack emptyCyan = EMPTY(Material.CYAN_STAINED_GLASS_PANE); + private final ItemStack emptyWhite = EMPTY(Material.WHITE_STAINED_GLASS_PANE); + private final ItemStack emptyBlue = EMPTY(Material.BLUE_STAINED_GLASS_PANE); + + @Override + protected ReplicatorSession createSession(Player player) { + return new ReplicatorSession(player, player.getInventory().getItemInMainHand()); + } + + @Override + public ReplicatorSession getSession(Player player) { + ReplicatorSession session = super.getSession(player); + session.setDelayTicks(getDupe().getPermissionValue(player, "dupealias.gui.replicator.refresh.", getConfig().replicator.baseRefreshDelayTicks)); + session.setCooldownTicks(getDupe().getPermissionValue(player, "dupealias.gui.replicator.cooldown.", getConfig().replicator.baseInputCooldownTicks)); + session.open(); + return session; + } + + public class ReplicatorSession extends AbstractDupeSession { + private ItemStack input; + private int timer = 0; + private int delayTicks; + private int currentDelayTicks = 0; + private int cooldownTicks; + private int currentCooldownTicks = 0; + private boolean ready = false; + + public ReplicatorSession(Player owner, ItemStack input) { + super(owner, "REPLICATOR", 3); + getVerbose().send("Creating a new replicator with input of {0}", input.getType().name()); + setInput(input); + this.delayTicks = getDupe().getPermissionValue(owner, "dupealias.gui.replicator.refresh.", getConfig().replicator.baseRefreshDelayTicks); + this.cooldownTicks = getDupe().getPermissionValue(owner, "dupealias.gui.replicator.cooldown.", getConfig().replicator.baseInputCooldownTicks); + } + + @Override + protected QuickGui buildGui(String title, int rows) { + return QuickGui.create() + .titleMini(title) + .rows(rows) + .fillSlots(EMPTY(Material.BLACK_STAINED_GLASS_PANE), null, 0, 9, 18, 4, 13, 22, 8, 17, 26) // Background + .fillSlots(emptyLightBlue, null, inputRing) + .fillSlots(EMPTY(Material.BLUE_STAINED_GLASS_PANE), null, outputRing) + .item(15, createPopulatedItem(null,1)) + .onGlobalClick((g, e) -> { + if (e.getClickedInventory() != null && e.getSlot() == 15) { + ItemStack clicked = e.getCurrentItem(); + ItemStack cursor = e.getCursor(); + ItemStack slot = e.getClickedInventory().getItem(e.getSlot()); + if (shouldBlockClick(clicked) || shouldBlockClick(cursor) || shouldBlockClick(slot)) { + e.setCancelled(true); + return; + } + + ready = false; + currentDelayTicks = 0; + SoundPlayer.play(getOwner(), Sound.ENTITY_ITEM_PICKUP, 0.8F, 1.2F); + + e.setCancelled(false); + return; + } + if (e.getSlot() == 11) { + if (currentCooldownTicks > 0) { + SoundPlayer.play(getOwner(),Sound.BLOCK_NOTE_BLOCK_BASS); + return; + } else { + currentCooldownTicks = cooldownTicks; + } + + Inventory inv = getGui().getInventory(); + ItemStack cursor = e.getCursor(); + if (cursor == null || cursor.getType() == Material.AIR) { + setInput(new ItemStack(Material.AIR)); + SoundPlayer.play(getOwner(),Sound.ITEM_BUNDLE_REMOVE_ONE); + getOwner().stopSound(Sound.BLOCK_BEACON_AMBIENT); + deactivateRings(inv); + } else { + if (setInput(cursor)) { + SoundPlayer.play(getOwner(),Sound.ITEM_BUNDLE_INSERT); + activateRings(inv); + } + } + } + }) + .clickSound(null,0,0) + .onClose((g, e) -> close()) + .build(); + } + + @Override + protected void tick() { + timer++; + + Inventory inv = getGui().getInventory(); + + if (timer % 2 == 0) for (int i = 0; i < outputRing.length; i++) { + int currentSlot = outputRing[i]; + int nextSlot = outputRing[(i+1) % outputRing.length]; + ItemStack currentItem = inv.getItem(currentSlot); + if (currentItem != null && currentItem.isSimilar(emptyLightBlue)) { + inv.setItem(currentSlot,emptyCyan); + inv.setItem(nextSlot,emptyLightBlue); + break; + } + } + + ItemStack output = inv.getItem(15); + + if (currentCooldownTicks > 1) { + currentCooldownTicks--; + inv.setItem(11,createInputItem(null, (double) currentCooldownTicks / (double) cooldownTicks)); + } else if (currentCooldownTicks == 1) { + currentCooldownTicks--; + getGui().getInventory().setItem(11, createInputItem(input, 1)); + SoundPlayer.play(getOwner(), Sound.BLOCK_AMETHYST_BLOCK_RESONATE, 1, 0.8F); + } + + if (input == null || input.getType() == Material.AIR) { + if (output != null && !output.isSimilar(createPopulatedItem(null,1))) { + inv.setItem(15, createPopulatedItem(null,1)); + } + return; + } + + if (timer % 20 == 0) { + SoundPlayer.play(getOwner(), Sound.BLOCK_BEACON_AMBIENT, 0.5F, 1.2F); + } + + if (!ready) { + currentDelayTicks++; + double progress = Math.min(1.0, (double) currentDelayTicks / delayTicks); + inv.setItem(15, createPopulatedItem(input, progress)); + if (currentDelayTicks >= delayTicks) { + ready = true; + SoundPlayer.play(getOwner(), Sound.BLOCK_AMETHYST_BLOCK_RESONATE, 1, 0.8F); + } + return; + } + + if (input.isSimilar(output)) return; + + inv.setItem(15, createPopulatedItem(input, 1)); + } + + public boolean setInput(ItemStack newInput) { + if (getDupe().isUnique(newInput)) { + SoundPlayer.play(getOwner(), Sound.ENTITY_VILLAGER_NO, 1, 0.8F); + warningAny(getOwner(), "Your {0} is or contains a unique item!", FormatUtils.formatEnum(newInput.getType())); + getGui().getInventory().setItem(11, createInputItem(this.input, 1)); + return false; + } + this.input = newInput.clone(); + getGui().getInventory().setItem(11, createInputItem(this.input, 1)); + return true; + } + + public void setDelayTicks(int delayTicks) { + this.delayTicks = delayTicks; + } + + public void setCooldownTicks(int cooldownTicks) { + this.cooldownTicks = cooldownTicks; + } + + private void activateRings(Inventory inv) { + for (int i : inputRing) { + inv.setItem(i,emptyWhite); + } + for (int i : outputRing) { + inv.setItem(i,emptyCyan); + } + inv.setItem(outputRing[0], emptyLightBlue); + } + + private void deactivateRings(Inventory inv) { + for (int i : inputRing) { + inv.setItem(i, emptyLightBlue); + } + for (int i : outputRing) { + inv.setItem(i,emptyBlue); + } + } + } + + private ItemStack createInputItem(ItemStack input, double cooldownProgress) { + if (cooldownProgress < 1) { + return ItemBuilder.of(EMPTY(Material.BARRIER)) + .displayName("Replicator Input") + .loreComponent(getTextSystem().createProgressBar(cooldownProgress, '|',30, TextColor.color(0xFF895A),TextColor.color(0x6F6F6F))) + .loreComponent(Component.text("Replicator input on cooldown.", NamedTextColor.DARK_RED)) + .build(); + } + if (input == null || input.getType() == Material.AIR) { + return ItemBuilder.headOfTexture("http://textures.minecraft.net/texture/86bd920b402815ad89018df82977be9f7ea19e799ecf016f7f0da4ab47ca23c5") + .displayName("Replicator Input") + .loreMiniMessage("No item selected.") + .loreMiniMessage("Drag an item into this slot.") + .build(); + } else { + return ItemBuilder.headOfTexture("http://textures.minecraft.net/texture/32d250f5336449b32bfe990bdfd307a1b39ae5ca07e9a1593b1bb6ed33ec14ba") + .displayName("Replicator Input") + .loreMiniMessage("Set Item: " + FormatUtils.formatEnum(input.getType())) + .loreMiniMessage("Replication Ready!") + .build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/dupealias/server/gui/dupe/ReplicatorGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/ReplicatorGui.java deleted file mode 100644 index aeed0f6..0000000 --- a/src/main/java/me/trouper/dupealias/server/gui/dupe/ReplicatorGui.java +++ /dev/null @@ -1,173 +0,0 @@ -package me.trouper.dupealias.server.gui.dupe; - -import me.trouper.alias.server.systems.gui.QuickGui; -import me.trouper.alias.utils.FormatUtils; -import me.trouper.alias.utils.ItemBuilder; -import me.trouper.alias.utils.SoundPlayer; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; - -import java.util.HashMap; -import java.util.Map; - -public class ReplicatorGui extends AbstractDupeGui { - - @Override - protected ReplicatorSession createSession(Player player) { - return new ReplicatorSession(player, player.getInventory().getItemInMainHand()); - } - - public class ReplicatorSession extends AbstractDupeSession { - private ItemStack input; - private int ticks = 0; - - public ReplicatorSession(Player owner, ItemStack input) { - super(owner, "REPLICATOR", 3); - getVerbose().send("Creating a new replicator with input of {0}", input.getType().name()); - setInput(input); - } - - @Override - protected QuickGui buildGui(String title, int rows) { - return QuickGui.create() - .titleMini(title) - .rows(rows) - .fillSlots(EMPTY(Material.BLACK_STAINED_GLASS_PANE), null, 0, 9, 18, 4, 13, 22, 8, 17, 26) // Background - .fillSlots(EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE), null, 1, 2, 3, 10, 12, 19, 20, 21) // Input ring - .fillSlots(EMPTY(Material.BLUE_STAINED_GLASS_PANE), null, 5, 6, 7, 14, 16, 23, 24, 25) // Replication ring - .item(15, new ItemStack(Material.AIR)) - .onGlobalClick((g, e) -> { - if (e.getSlot() == 15) { - e.setCancelled(false); - return; - } - if (e.getSlot() == 11) { - Inventory inv = getGui().getInventory(); - ItemStack cursor = e.getCursor(); - if (cursor == null || cursor.getType() == Material.AIR) { - setInput(new ItemStack(Material.AIR)); - SoundPlayer.play(getOwner(),Sound.ITEM_BUNDLE_REMOVE_ONE); - getOwner().stopSound(Sound.BLOCK_BEACON_AMBIENT); - deactivateRings(inv); - } else { - if (setInput(cursor)) { - SoundPlayer.play(getOwner(),Sound.ITEM_BUNDLE_INSERT); - activateRings(inv); - } - } - } - }) - .onClose((g, e) -> close()) - .build(); - } - - @Override - protected long getTickDelay(Player player) { - return 1; - } - - @Override - protected void tick() { - ticks++; - Inventory inv = getGui().getInventory(); - ItemStack output = inv.getItem(15); - - if (input == null || input.getType() == Material.AIR) { - if (output != null && output.getType() != Material.AIR) { - inv.setItem(15, new ItemStack(Material.AIR)); - } - return; - } - - if (ticks % 20 == 0) { - SoundPlayer.play(getOwner(), Sound.BLOCK_BEACON_AMBIENT, 0.5F, 1.2F); - } - - if (input.isSimilar(output)) return; - - inv.setItem(15, createPopulatedItem(input)); - SoundPlayer.play(getOwner(), Sound.BLOCK_AMETHYST_BLOCK_RESONATE, 1, 0.8F); - } - - public boolean setInput(ItemStack newInput) { - if (getDupe().isUnique(newInput)) { - SoundPlayer.play(getOwner(), Sound.ENTITY_VILLAGER_NO, 1, 0.8F); - warningAny(getOwner(), "Your {0} is or contains a unique item!", FormatUtils.formatEnum(newInput.getType())); - return false; - } - this.input = newInput.clone(); - getGui().getInventory().setItem(11, createInputItem(this.input)); - return true; - } - - public ItemStack getInput() { - return input; - } - } - - private ItemStack createInputItem(ItemStack input) { - if (input == null || input.getType() == Material.AIR) { - return ItemBuilder.headOfTexture("http://textures.minecraft.net/texture/86bd920b402815ad89018df82977be9f7ea19e799ecf016f7f0da4ab47ca23c5") - .displayName("Replicator Input") - .loreMiniMessage("No item selected.") - .loreMiniMessage("Drag an item into this slot.") - .build(); - } else { - return ItemBuilder.headOfTexture("http://textures.minecraft.net/texture/32d250f5336449b32bfe990bdfd307a1b39ae5ca07e9a1593b1bb6ed33ec14ba") - .displayName("Replicator Input") - .loreMiniMessage("Item: " + FormatUtils.formatEnum(input.getType())) - .loreMiniMessage("Replication Ready!") - .build(); - } - } - - private void activateRings(Inventory inv) { - inv.setItem(1, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(2, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(3, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(10, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(12, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(19, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(20, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - inv.setItem(21, EMPTY(Material.WHITE_STAINED_GLASS_PANE)); - - inv.setItem(5, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(6, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(7, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(14, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(16, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(23, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(24, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(25, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - } - - private void deactivateRings(Inventory inv) { - inv.setItem(1, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(2, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(3, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(10, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(12, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(19, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(20, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - inv.setItem(21, EMPTY(Material.LIGHT_BLUE_STAINED_GLASS_PANE)); - - inv.setItem(5, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(6, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(7, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(14, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(16, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(23, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(24, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - inv.setItem(25, EMPTY(Material.BLUE_STAINED_GLASS_PANE)); - } - - public class ReplicatorConfig { - final int baseRefreshDelayTicks = 1; - final int baseInputCooldownTicks = 20; - final Map permissionRefreshDelayTicks = new HashMap<>(); - final Map permissionInputCooldownTicks = new HashMap<>(); - } -} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 398f39c..6049f2f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -22,15 +22,18 @@ permissions: default: op dupealias.final.bypass: description: Allows the bypassing of final item restrictions + default: op dupealias.protected.bypass: description: Allows the bypassing of protected item restrictions + default: op dupealias.unique.bypass: description: Allows the duping of unique items + default: op dupealias.dupe: description: Allows duplication of items through the command. default: true children: - dupealias.dupe.cooldownbypass: op + dupealias.dupe.cooldownbypass: false dupealias.dupe.cooldownbypass: description: Bypass the cooldown for /dupe default: op @@ -48,14 +51,16 @@ permissions: description: The gui which lets you shift click copies of a single item. default: true children: - dupalias.gui.replicator.keep: op + dupealias.gui.replicator.keep: false # Controls if a player should keep their previous replicator session with items in it. This does not persist across reboots. + dupealias.gui.replicator.refresh.integerhere: false # Controls the time it will take a duplicated item to refill or refresh in the GUI. Always takes the lowest number on a permission holder. dupealias.gui.inventory: description: The gui which shows your inventory and armor on top. default: true children: - dupalias.gui.inventory.keep: op + dupealias.gui.inventory.refresh.integerhere: false # Controls the time it will take a duplicated item to refill or refresh in the GUI. Always takes the lowest number on a permission holder. dupealias.gui.chest: description: A gui which items can be put in and taken out as copies. default: true children: - dupalias.gui.chest.keep: op \ No newline at end of file + dupealias.gui.chest.keep: false # Controls if a player should keep their previous chest session with items in it. This does not persist across reboots. + dupealias.gui.chest.refresh.integerhere: false # Controls the time it will take a duplicated item to refill or refresh in the GUI. Always takes the lowest number on a permission holder. \ No newline at end of file