From 2817161c11f4eb2fc109327a0ab4ca9df37af0fb Mon Sep 17 00:00:00 2001 From: wolf Date: Thu, 24 Jul 2025 19:57:54 -0500 Subject: [PATCH] Replaced global material with Global Rule --- Ideas.md | 38 +- build.gradle | 61 -- build.gradle.kts | 41 ++ gradle.properties | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 10 +- gradlew.bat | 6 +- settings.gradle | 1 - settings.gradle.kts | 5 + .../java/me/trouper/dupealias/DupeAlias.java | 10 +- .../me/trouper/dupealias/DupeContext.java | 10 +- .../me/trouper/dupealias/data/GlobalRule.java | 168 +++++ .../trouper/dupealias/data/ItemCapture.java | 81 +++ .../me/trouper/dupealias/data/PlayerData.java | 13 - .../data/{ => files}/CommonConfig.java | 2 +- .../data/{ => files}/DupeConfig.java | 32 +- .../dupealias/data/files/NBTStorage.java | 41 ++ .../trouper/dupealias/server/DupeManager.java | 252 +++++-- .../server/commands/AdminCommand.java | 303 +++++--- .../server/commands/DupeCommand.java | 6 +- .../server/functions/UniqueCheck.java | 2 +- .../dupealias/server/gui/CommonItems.java | 17 +- .../dupealias/server/gui/admin/AdminGui.java | 681 ------------------ .../server/gui/admin/AdminPanelManager.java | 358 +++++++++ .../dupealias/server/gui/admin/ConfigGui.java | 57 -- .../server/gui/admin/HeldItemGui.java | 160 ++++ .../dupealias/server/gui/admin/HelpGui.java | 224 ++++++ .../server/gui/admin/MainAdminGui.java | 90 +++ .../server/gui/admin/MaterialBrowserGui.java | 70 -- .../gui/admin/config/CommandRegexGui.java | 108 +++ .../gui/admin/config/CommonConfigGui.java | 172 +++++ .../server/gui/admin/config/ConfigGui.java | 389 ++++++++++ .../globalrule/GlobalRuleArmorTrimEditor.java | 190 +++++ .../globalrule/GlobalRuleAttributeEditor.java | 136 ++++ .../admin/globalrule/GlobalRuleEditorGui.java | 269 +++++++ .../GlobalRuleEnchantmentEditor.java | 149 ++++ .../globalrule/GlobalRuleItemFlagEditor.java | 119 +++ .../admin/globalrule/GlobalRuleListGui.java | 212 ++++++ .../GlobalRuleMaterialSelector.java | 134 ++++ .../GlobalRulePotionEffectEditor.java | 160 ++++ .../server/gui/dupe/AbstractDupeGui.java | 1 - .../server/gui/dupe/AbstractDupeSession.java | 4 +- .../server/gui/dupe/DupeChestGui.java | 102 ++- .../dupealias/server/gui/dupe/DupeGui.java | 4 +- .../server/gui/dupe/DupeInventoryGui.java | 125 +++- .../server/gui/dupe/DupeReplicatorGui.java | 227 ++++++ .../server/gui/dupe/ReplicatorGui.java | 173 ----- src/main/resources/plugin.yml | 13 +- 49 files changed, 4120 insertions(+), 1311 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts mode change 100755 => 100644 gradlew delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts create mode 100644 src/main/java/me/trouper/dupealias/data/GlobalRule.java create mode 100644 src/main/java/me/trouper/dupealias/data/ItemCapture.java delete mode 100644 src/main/java/me/trouper/dupealias/data/PlayerData.java rename src/main/java/me/trouper/dupealias/data/{ => files}/CommonConfig.java (96%) rename src/main/java/me/trouper/dupealias/data/{ => files}/DupeConfig.java (50%) create mode 100644 src/main/java/me/trouper/dupealias/data/files/NBTStorage.java delete mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/AdminGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/AdminPanelManager.java delete mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/ConfigGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/HeldItemGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/HelpGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/MainAdminGui.java delete mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/MaterialBrowserGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/config/CommandRegexGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/config/CommonConfigGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/config/ConfigGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleArmorTrimEditor.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleAttributeEditor.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEditorGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleEnchantmentEditor.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleItemFlagEditor.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleListGui.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRuleMaterialSelector.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/admin/globalrule/GlobalRulePotionEffectEditor.java create mode 100644 src/main/java/me/trouper/dupealias/server/gui/dupe/DupeReplicatorGui.java delete mode 100644 src/main/java/me/trouper/dupealias/server/gui/dupe/ReplicatorGui.java 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 e6441136f3d4ba8a0da8d277868979cfbc8ad796..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch delta 35073 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 delta 34912 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

Gn4F+B~IivciU9^t0j-Mgao3 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 dDuazZG^u*-) zCV{{p$D*4m=Z;!QJ0C3EBfaUv#@3CmRgS-#a4+Uk9LtR(mzdSt-?*B-Q{Jgs!F;+_ zT&Jc~{N951OskjVtnWOLnfUacmSEkQ#fQojqa=HN?aN#ubFA){K&TrfoSnBphJ<43S zS5?OUrqPYpEqf;(zc;IIty0d`-n-Ke%PlArJt^DN>ZYW)ZS5z$Rnx=cxbM1#zPRNS1N0u>c z*gpBeTvyKAjdr#Uz%$J3C+p2~l9`-P6`=_1Y83#nAp`Kd5jfb*H#u;=%w#=&g~|Q% z1!N9S7n+#|tj#t98*^$X3S9)i3I*n?N+YghXJ9Z!QN%wvV7|W$;;Lx|1{)Ol%c4L- zlz|txtAK4ozLL`xMPZR7Sm6grRj@+Db&BXF9#sG|gMywm=IO1O01imHv3 zFjX9ri>su;HX}wK85k5$R0A&?fdtA5_Q_vX$jc(elo%M~QB>A-K~#2igOjH$d^80Z zC8#NjzZarXrcVZJJEF73z+ix4=7c`5N>TJt7>~rfl$^E}UAMlP2$P)vS26{cE?ONf_&Vd@-E2?PMU+S~pB 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