commit e40981518816255d2e56f2d141633d3cea40491d Author: wolf Date: Wed Jul 9 21:34:08 2025 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f737e --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ +runs/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/Ideas.md b/Ideas.md new file mode 100644 index 0000000..8749e7f --- /dev/null +++ b/Ideas.md @@ -0,0 +1,36 @@ +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. | diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d936c4f --- /dev/null +++ b/build.gradle @@ -0,0 +1,61 @@ +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/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e69de29 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..b740cf1 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# 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 + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +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, +# 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. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9f982d3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'DupeAlias' diff --git a/src/main/java/me/trouper/dupealias/DupeAlias.java b/src/main/java/me/trouper/dupealias/DupeAlias.java new file mode 100644 index 0000000..f85c0cd --- /dev/null +++ b/src/main/java/me/trouper/dupealias/DupeAlias.java @@ -0,0 +1,69 @@ +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.server.DupeManager; +import org.bukkit.plugin.java.JavaPlugin; + +public final class DupeAlias extends JavaPlugin { + + private static DupeAlias instance; + private AliasContext alias; + private Common common; + private DupeManager dupe; + + @Override + public void onLoad() { + instance = this; + common = new CommonConfig().generateCommon(); + alias = new AliasContext(this, common); + AliasContextProvider.registerContext(this,alias); + } + + @Override + public void onEnable() { + alias.initialize(); + alias.getDataManager().load(CommonConfig.class).save(); + alias.getDataManager().load(DupeConfig.class).save(); + alias.getDataManager().load(PlayerData.class).save(); + updateCommon(); + + dupe = new DupeManager(); + } + + @Override + public void onDisable() { + alias.getDataManager().save(CommonConfig.class); + alias.getDataManager().save(DupeConfig.class); + alias.getDataManager().save(PlayerData.class); + alias.shutdown(); + } + + public static DupeAlias getDupeAlias() { + return instance; + } + + public void updateCommon() { + common.update(alias.getDataManager().get(CommonConfig.class).generateCommon()); + } + + public Common getCommon() { + return common; + } + + public CommonConfig getCommonConfig() { + return alias.getDataManager().get(CommonConfig.class); + } + + public DupeConfig getDupeConfig() { + return alias.getDataManager().get(DupeConfig.class); + } + + public DupeManager getDupe() { + return dupe; + } +} diff --git a/src/main/java/me/trouper/dupealias/DupeContext.java b/src/main/java/me/trouper/dupealias/DupeContext.java new file mode 100644 index 0000000..11b84aa --- /dev/null +++ b/src/main/java/me/trouper/dupealias/DupeContext.java @@ -0,0 +1,34 @@ +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.server.DupeManager; +import org.bukkit.plugin.java.JavaPlugin; + +public interface DupeContext extends ContextAware { + @Override + default Class getPluginClass() { + return DupeAlias.class; + } + + default DupeAlias getInstance() { + return DupeAlias.getDupeAlias(); + } + + default CommonConfig getCommonConfig() { + return getDataManager().get(CommonConfig.class); + } + + default DupeConfig getConfig() { + return getDataManager().get(DupeConfig.class); + } + default GuiInputListener getGuiListener() { + return getContext().getGuiInputListener(); + } + + default DupeManager getDupe() { + return getInstance().getDupe(); + } +} diff --git a/src/main/java/me/trouper/dupealias/data/CommonConfig.java b/src/main/java/me/trouper/dupealias/data/CommonConfig.java new file mode 100644 index 0000000..bcdfc87 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/data/CommonConfig.java @@ -0,0 +1,40 @@ +package me.trouper.dupealias.data; + +import me.trouper.alias.data.Common; +import me.trouper.alias.data.JsonSerializable; +import me.trouper.dupealias.DupeContext; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public class CommonConfig implements JsonSerializable, DupeContext { + + @Override + public File getFile() { + return new File(getInstance().getDataFolder(), "common.json"); + } + + public int mainColor = 0xAAAAFF; + public int secondaryColor = 0x00DDFF; + public String pluginName = "DupeAlias"; + public String flatPrefix = "&9DupeAlias> &7"; + public boolean flat = false; + public boolean debugMode = false; + public Set debuggerExclusions = new HashSet<>(); + + public Common generateCommon() { + Common common = new Common( + "me.trouper.dupealias", + mainColor, + secondaryColor, + pluginName, + flatPrefix, + flat, + "http://api.trouper.me:9090/download/plugins/DupeAlias/DupeAlias-LATEST.jar" + ); + common.setDebugMode(debugMode); + common.setDebuggerExclusions(debuggerExclusions); + return common; + } +} diff --git a/src/main/java/me/trouper/dupealias/data/DupeConfig.java b/src/main/java/me/trouper/dupealias/data/DupeConfig.java new file mode 100644 index 0000000..1e463b2 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/data/DupeConfig.java @@ -0,0 +1,35 @@ +package me.trouper.dupealias.data; + +import me.trouper.alias.data.JsonSerializable; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.server.ItemTag; +import org.bukkit.Material; + +import java.io.File; +import java.util.*; + +public class DupeConfig implements JsonSerializable, DupeContext { + @Override + public File getFile() { + return new File(getInstance().getDataFolder(),"config.json"); + } + + public long dupeCooldownMillis = 1000; + + public String defaultDupeGui = "REPLICATOR"; + + public List finalCommandRegex = new ArrayList<>(List.of( + "\"(?:itemname|iname|einame|eitemname|itemrename|irename|eitemrename|eirename)\"gmi", + "\"(?:itemlore|lore|elore|ilore|eilore|eitemlore)\"gmi" + )); + + public Map tagLore = new HashMap<>(Map.of( + ItemTag.PROTECTED, "| Protected", + ItemTag.FINAL, "| Final", + ItemTag.UNIQUE, "| Unique", + ItemTag.INFINITE, "| Infinite" + )); + + public Map> globalMaterials = new HashMap<>(); + +} diff --git a/src/main/java/me/trouper/dupealias/data/PlayerData.java b/src/main/java/me/trouper/dupealias/data/PlayerData.java new file mode 100644 index 0000000..99e162e --- /dev/null +++ b/src/main/java/me/trouper/dupealias/data/PlayerData.java @@ -0,0 +1,13 @@ +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/server/DupeManager.java b/src/main/java/me/trouper/dupealias/server/DupeManager.java new file mode 100644 index 0000000..5d289f7 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/DupeManager.java @@ -0,0 +1,136 @@ +package me.trouper.dupealias.server; + +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeContext; +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.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DupeManager implements DupeContext { + + public boolean isUnique(ItemStack item) { + return !new UniqueCheck().passes(item); + } + + public boolean hasIndividualTag(ItemStack input, ItemTag tag) { + return input.hasItemMeta() && input.getPersistentDataContainer().has(tag.getKey()); + } + + public boolean checkIndividualTag(ItemStack input, ItemTag tag) { + boolean set = hasIndividualTag(input,tag); + if (!set) throw new IllegalArgumentException("Tried to check a tag which was not set, this may produce unexpected behavior!"); + 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)); + + if (set) return individual; + return global; + } + + 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; + } + + 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); + getConfig().save(); + return result; + } + + public boolean addTag(ItemStack item, ItemTag tag) { + if (hasIndividualTag(item,tag) && getDupe().checkIndividualTag(item,tag)) return false; + ItemBuilder builder = ItemBuilder.of(item); + builder.loreMiniMessage(getConfig().tagLore.get(tag)); + builder.modifyMeta(itemMeta -> { + 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; + 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(); + + 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); + return itemMeta; + }); + + ItemStack result = builder.buildAndGet(); + + item.setItemMeta(result.getItemMeta()); + } + + public ItemTag getTag(NamespacedKey key) { + for (ItemTag value : ItemTag.values()) { + if (value.getKey().equals(key)) return value; + } + throw new IllegalArgumentException("Invalid NameSpacedKey '%s'".formatted(key.value())); + } + +} diff --git a/src/main/java/me/trouper/dupealias/server/ItemTag.java b/src/main/java/me/trouper/dupealias/server/ItemTag.java new file mode 100644 index 0000000..14529ac --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/ItemTag.java @@ -0,0 +1,33 @@ +package me.trouper.dupealias.server; + +import me.trouper.dupealias.DupeAlias; +import org.bukkit.NamespacedKey; + +public enum ItemTag { + UNIQUE(new NamespacedKey(DupeAlias.getDupeAlias(),"unique"), "Unique", "Cannot be duplicated"), + FINAL(new NamespacedKey(DupeAlias.getDupeAlias(),"final"), "Final", "Cannot be modified"), + PROTECTED(new NamespacedKey(DupeAlias.getDupeAlias(),"protected"), "Protected", "Cannot be used or crafted with"), + INFINITE(new NamespacedKey(DupeAlias.getDupeAlias(),"infinite"), "Infinite", "Will always have max stack size"); + + + private final NamespacedKey key; + private final String name; + private final String desc; + + ItemTag(NamespacedKey key, String name, String desc) { + this.key = key; + this.name = name; + this.desc = desc; + } + + public NamespacedKey getKey() { + return key; + } + public String getName() { + return name; + } + public String getDesc() { + return desc; + } + +} diff --git a/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java b/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java new file mode 100644 index 0000000..555742c --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/commands/AdminCommand.java @@ -0,0 +1,284 @@ +package me.trouper.dupealias.server.commands; + +import me.trouper.alias.server.commands.Args; +import me.trouper.alias.server.commands.CommandRegistry; +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.server.ItemTag; +import me.trouper.dupealias.server.gui.admin.AdminGui; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +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]", + blocksAllowed = false, + printStackTrace = true +) +public class AdminCommand implements QuickCommand, DupeContext { + + @Override + public void handleCommand(CommandSender sender, Command command, String label, Args args) { + if (args.isEmpty()) { + openBaseGui(sender); + return; + } + + switch (args.get(0).toString()) { + case "debug" -> { + handleDebug(sender,args); + } + + case "gui" -> { + openBaseGui(sender); + } + + case "tag" -> { + handleTag(sender,args); + } + + default -> { + errorAny(sender,"Invalid subcommand!"); + } + } + } + + @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("gui") + ); + } + + + private void handleDebug(CommandSender sender, Args args) { + if (args.getSize() < 2) { + errorAny(sender, "Usage: debug "); + return; + } + + final String sub = args.get(1).toString(); + + switch (sub) { + case "toggle" -> { + boolean result = false; + getCommonConfig().debugMode = result = !getCommonConfig().debugMode; + getCommonConfig().save(); + + 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(); + + 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(); + + getInstance().updateCommon(); + + successAny(sender, "Removed exclusion for {0} on the debugger.", exclusion); + } + } + } + private void handleTag(CommandSender sender, Args args) { + if (args.getSize() < 2) { + errorAny(sender, "You must specify an item tag. Usage: /gui tag ..."); + return; + } + + // Argument 1 + final ItemTag tag; + try { + tag = args.get(1).toEnum(ItemTag.class); + } catch (IllegalArgumentException e) { + errorAny(sender, "Argument '{0}' is not a valid item tag.", args.get(1).toString()); + return; + } + + // gui 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'."); + return; + } + ItemStack heldItem = player.getInventory().getItemInMainHand(); + if (heldItem.getType().isAir()) { + errorAny(sender, "You must be holding an item to tag it."); + return; + } + if (getDupe().addTag(heldItem, tag)) { + successAny(sender, "Your {0} is now tagged as {1} and {2}.", heldItem.getType(), tag.getName(), tag.getDesc()); + return; + } else { + infoAny(sender,"Your {0} already has the {1} tag.",heldItem.getType(),tag.getName()); + } + + } + + // Argument 2 + String subCommand = args.get(2).toString().toLowerCase(); + + // gui tag remove|false + switch (subCommand) { + case "remove" -> { + if (args.getSize() != 3) { + errorAny(sender, "Invalid arguments. Usage: /gui tag remove"); + return; + } + if (!(sender instanceof Player player)) { + errorAny(sender, "This command can only be run by a player to remove a tag from a held item."); + return; + } + ItemStack heldItem = player.getInventory().getItemInMainHand(); + if (heldItem.getType().isAir()) { + errorAny(sender, "You must be holding an item to remove its tag."); + return; + } + if (getDupe().removeTag(heldItem, tag)) { + successAny(sender, "Removed tag {0} from your {1}.", tag.getName(), heldItem.getType()); + return; + } else { + infoAny(sender,"Your {0} does not have the {1} tag.",heldItem.getType(),tag.getName()); + } + } + case "false" -> { + if (args.getSize() != 3) { + errorAny(sender, "Invalid arguments. Usage: /gui tag remove"); + 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."); + return; + } + ItemStack heldItem = player.getInventory().getItemInMainHand(); + if (heldItem.getType().isAir()) { + errorAny(sender, "You must be holding an item to set its tag."); + return; + } + getDupe().setTag(heldItem, tag, false); + successAny(sender, "Set tag {0} from your {1} to {2}.", tag.getName(), heldItem.getType(), "false"); + return; + } + + + // gui tag global [remove] + case "global" -> { + if (!(sender instanceof Player player)) { + errorAny(sender, "The 'global' subcommand must be run by a player."); + return; + } + Material heldMaterial = player.getInventory().getItemInMainHand().getType(); + if (heldMaterial.isAir()) { + errorAny(sender, "You must be holding an item to use the 'global' subcommand."); + return; + } + + 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"); + return; + } + if (getDupe().removeGlobalTag(heldMaterial, tag)) { + successAny(sender, "Removed global tag {0} from all {1} items.", tag.getName(), heldMaterial); + } else { + infoAny(sender, "{0} is not globally tagged as {1}.", heldMaterial, tag.getName()); + } + } else { + if (args.getSize() != 3) { + errorAny(sender, "Invalid arguments. Usage: /gui 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()); + } else { + infoAny(sender, "All {0} items are already tagged as {1} and {2}.", heldMaterial, tag.getName(), tag.getDesc()); + } + } + return; + } + } + + // gui 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"); + return; + } + if (getDupe().removeGlobalTag(material, tag)) { + successAny(sender, "Removed global tag {0} from {1}.", tag.getName(), material); + } else { + infoAny(sender, "{0} is not tagged as {1} globally.", material, tag.getName()); + } + } else { + if (args.getSize() != 3) { + errorAny(sender, "Invalid arguments. Usage: /gui 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()); + } else { + infoAny(sender, "All {0} items are already tagged as {1} and {2}.", material, tag.getName(), tag.getDesc()); + } + } + } catch (IllegalArgumentException e) { + errorAny(sender, "Invalid subcommand '{0}'. Expected 'remove', 'global', or a valid material name.", subCommand); + } + } + + + public void openBaseGui(CommandSender sender) { + if (sender instanceof Player player) { + new AdminGui().openMainGui(player); + } else { + errorAny(sender, "Console may not open a GUI."); + } + } + +} diff --git a/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java b/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java new file mode 100644 index 0000000..840bf63 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/commands/DupeCommand.java @@ -0,0 +1,118 @@ +package me.trouper.dupealias.server.commands; + +import me.trouper.alias.server.commands.Args; +import me.trouper.alias.server.commands.CommandRegistry; +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.alias.utils.misc.Cooldown; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.server.gui.dupe.DupeGui; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.UUID; + +@CommandRegistry( + value = "dupe", + permission = @Permission(value = "dupealias.dupe", message = "You do not have permission to duplicate items."), + usage = "/dupe [integer|gui]", + printStackTrace = true, + blocksAllowed = false, + consoleAllowed = false +) +public class DupeCommand implements QuickCommand, DupeContext { + + private final DupeGui dupeGui = new DupeGui(); + private final Cooldown dupeCooldown = new Cooldown<>(); + + @Override + public void handleCommand(CommandSender sender, Command command, String label, Args args) { + Player player = (Player) sender; + if (!player.hasPermission("dupealias.dupe.cooldownbypass") && dupeCooldown.isOnCooldown(player.getUniqueId())) { + warningAny(player,"You can run /dupe again in {0}.", dupeCooldown.formatLong(player.getUniqueId())); + return; + } + + if (args.isEmpty()) { + if (dupeHeld(player,0)) { + dupeCooldown.setCooldown(player.getUniqueId(), getConfig().dupeCooldownMillis); + } else { + dupeGui.openDefaultGui(player); + } + return; + } + + switch (args.get(0).toString()) { + case "gui" -> { + dupeGui.openMainGui(player); + return; + } + case "replicator" -> { + dupeGui.openIfPermission(player,dupeGui.replicatorGui,"dupealias.gui.replicator"); + return; + } + case "inventory" -> { + dupeGui.openIfPermission(player,dupeGui.inventoryGui,"dupealias.gui.inventory"); + return; + } + case "chest" -> { + dupeGui.openIfPermission(player,dupeGui.chestGui,"dupealias.gui.chest"); + return; + } + } + + try { + int amount = args.get(0).toInt(); + if (dupeHeld(player,amount)) { + dupeCooldown.setCooldown(player.getUniqueId(), getConfig().dupeCooldownMillis); + } else { + dupeGui.openDefaultGui(player); + } + } catch (NumberFormatException e) { + warningAny(player,"{0} is not a valid number.", args.get(0).toString()); + } + } + + @Override + public void handleCompletion(CommandSender sender, Command command, String label, Args args, CompletionBuilder b) { + b.then( + b.arg("gui","number","replicator","inventory","chest") + ); + } + + private boolean dupeHeld(Player player, int amount) { + ItemStack inHand = player.getInventory().getItemInMainHand(); + if (getDupe().isUnique(inHand)) { + warningAny(player,"Your {0} is or contains a unique item that cannot be duped!", inHand.getType()); + return false; + } + if (inHand.isEmpty()) return false; + + int baseCount = inHand.getAmount(); + int maxPerStack = inHand.getMaxStackSize(); + + for (int i = 0; i <= amount; i++) { + int remaining = baseCount * (1 << i); + + while (remaining > 0) { + int stackAmt = Math.min(remaining, maxPerStack); + remaining -= stackAmt; + + ItemStack batch = inHand.clone(); + batch.setAmount(stackAmt); + + if (!player.getInventory().addItem(batch).isEmpty()) { + infoAny(player,"Your inventory is now full."); + return true; + } + } + } + + int totalGiven = baseCount * ((1 << (amount + 1)) - 1); + successAny(player,"You have duplicated {0} items!", totalGiven); + return true; + } +} diff --git a/src/main/java/me/trouper/dupealias/server/events/InfiniteItemEvents.java b/src/main/java/me/trouper/dupealias/server/events/InfiniteItemEvents.java new file mode 100644 index 0000000..6f18032 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/events/InfiniteItemEvents.java @@ -0,0 +1,111 @@ +package me.trouper.dupealias.server.events; + +import me.trouper.alias.server.events.QuickListener; +import me.trouper.alias.utils.FormatUtils; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.server.ItemTag; +import org.bukkit.block.Container; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockDispenseEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.ProjectileLaunchEvent; +import org.bukkit.event.player.*; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +public class InfiniteItemEvents implements QuickListener, DupeContext { + @EventHandler + public void onPlace(BlockPlaceEvent e) { + resetBothHands(e.getPlayer()); + } + + @EventHandler + public void onThrow(ProjectileLaunchEvent e) { + if (!(e.getEntity().getShooter() instanceof Player player)) return; + resetBothHands(player); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + Player player = e.getPlayer(); + ItemStack stack = e.getItemDrop().getItemStack(); + if (!getDupe().checkEffectiveTag(stack,ItemTag.INFINITE)) return; + if (getDupe().checkEffectiveTag(stack,ItemTag.PROTECTED)) { + e.setCancelled(true); + return; + } + if (stack.getAmount() < 99) { + resetBothHands(player); + + getDupe().removeTag(stack,ItemTag.INFINITE); + ItemMeta stackMeta = stack.getItemMeta(); + stackMeta.setMaxStackSize(stack.getType().getMaxStackSize()); + stack.setItemMeta(stackMeta); + e.getItemDrop().setItemStack(stack); + } else { + infoAny(player,"You have dropped your infinite {0}!", FormatUtils.formatEnum(stack.getType())); + } + + } + + @EventHandler + public void onDispense(BlockDispenseEvent e) { + ItemStack stack = e.getItem(); + + if (!getDupe().checkEffectiveTag(stack, ItemTag.INFINITE)) return; + if (getDupe().checkEffectiveTag(stack, ItemTag.PROTECTED)) { + e.setCancelled(true); + return; + } + + if (stack.getAmount() < 99) { + getDupe().removeTag(stack,ItemTag.INFINITE); + ItemMeta stackMeta = stack.getItemMeta(); + stackMeta.setMaxStackSize(stack.getType().getMaxStackSize()); + stack.setItemMeta(stackMeta); + e.setItem(stack); + } + + Container container = (Container) e.getBlock().getState(); + + for (ItemStack itemStack : container.getInventory()) { + if (!getDupe().checkEffectiveTag(itemStack, ItemTag.INFINITE)) continue; + itemStack.setAmount(99); + } + } + + @EventHandler + public void onInteract(PlayerInteractEvent e) { + resetBothHands(e.getPlayer()); + } + + @EventHandler + public void onInteractAtEntity(PlayerInteractAtEntityEvent e) { + resetBothHands(e.getPlayer()); + } + + @EventHandler + public void onInteractEntity(PlayerInteractEntityEvent e) { + resetBothHands(e.getPlayer()); + } + + @EventHandler + public void onItemChange(PlayerItemHeldEvent e) { + Player player = e.getPlayer(); + ItemStack held = player.getInventory().getItem(e.getNewSlot()); + if (held == null || held.isEmpty()) return; + if (!getDupe().checkEffectiveTag(held,ItemTag.INFINITE)) return; + ItemMeta meta = held.getItemMeta(); + meta.setMaxStackSize(99); + held.setItemMeta(meta); + held.setAmount(meta.getMaxStackSize()); + } + + private void resetBothHands(Player player) { + ItemStack main = player.getInventory().getItemInMainHand(); + ItemStack off = player.getInventory().getItemInOffHand(); + if (!main.isEmpty() && getDupe().checkEffectiveTag(main,ItemTag.INFINITE)) main.setAmount(main.getMaxStackSize()); + if (!off.isEmpty() && getDupe().checkEffectiveTag(off,ItemTag.INFINITE)) off.setAmount(off.getMaxStackSize()); + } +} diff --git a/src/main/java/me/trouper/dupealias/server/events/ItemModificationEvents.java b/src/main/java/me/trouper/dupealias/server/events/ItemModificationEvents.java new file mode 100644 index 0000000..fbf46fd --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/events/ItemModificationEvents.java @@ -0,0 +1,680 @@ +package me.trouper.dupealias.server.events; + +import com.destroystokyo.paper.event.inventory.PrepareResultEvent; +import me.trouper.alias.server.events.QuickListener; +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.server.ItemTag; +import net.kyori.adventure.audience.Audience; +import org.bukkit.FluidCollisionMode; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.*; +import org.bukkit.entity.Player; +import org.bukkit.entity.ThrowableProjectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.*; +import org.bukkit.event.enchantment.EnchantItemEvent; +import org.bukkit.event.enchantment.PrepareItemEnchantEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.entity.EntityShootBowEvent; +import org.bukkit.event.entity.ProjectileLaunchEvent; +import org.bukkit.event.inventory.*; +import org.bukkit.event.player.*; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.*; +import org.bukkit.inventory.view.MerchantView; +import org.bukkit.persistence.PersistentDataType; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class ItemModificationEvents implements QuickListener, DupeContext { + + + @EventHandler(priority = EventPriority.HIGHEST) + public void prepareCraftEvent(PrepareItemCraftEvent event) { + getVerbose().send("Checking crafting matrix"); + ItemStack result = event.getInventory().getResult(); + + if (getDupe().checkEffectiveTag(result, ItemTag.PROTECTED)) { + event.getInventory().setResult(null); + warningAny(Audience.audience(event.getViewers()), "You cannot craft protected items!"); + return; + } + + for (ItemStack ingredient : event.getInventory().getMatrix()) { + if (ingredient == null || ingredient.isEmpty()) continue; + if (getDupe().checkEffectiveTag(ingredient, ItemTag.FINAL)) { + if (isModifyingCraft(ingredient, result)) { + event.getInventory().setResult(null); + warningAny(Audience.audience(event.getViewers()), "You cannot modify final items!"); + return; + } + } + if (getDupe().checkEffectiveTag(ingredient, ItemTag.PROTECTED)) { + event.getInventory().setResult(null); + warningAny(Audience.audience(event.getViewers()), "You cannot use protected items!"); + return; + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onSmithingTableUse(PrepareSmithingEvent event) { + getVerbose().send("Smithing Event"); + ItemStack result = event.getResult(); + ItemStack base = event.getInventory().getItem(1); + ItemStack addition = event.getInventory().getItem(2); + + // Prevent creating PROTECTED items + if (getDupe().checkEffectiveTag(result, ItemTag.PROTECTED)) { + event.setResult(null); + return; + } + + // Prevent modifying FINAL items (base or addition) + if (getDupe().checkEffectiveTag(base, ItemTag.FINAL) || getDupe().checkEffectiveTag(addition, ItemTag.FINAL)) { + // A smithing recipe always modifies the base item. + event.setResult(null); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onEnchantItem(EnchantItemEvent event) { + getVerbose().send("Enchant Event"); + ItemStack item = event.getItem(); + + if (getDupe().checkEffectiveTag(item, ItemTag.FINAL)) { + event.setCancelled(true); + warningAny(event.getEnchanter(), "You cannot modify final items!"); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPrepareEnchant(PrepareItemEnchantEvent event) { + getVerbose().send("Enchant Prepare Event"); + ItemStack item = event.getItem(); + if (getDupe().checkEffectiveTag(item, ItemTag.FINAL)) { + event.setCancelled(true); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onAnvilUse(PrepareAnvilEvent event) { + getVerbose().send("Anvil Prepare Event"); + AnvilInventory inventory = event.getInventory(); + ItemStack first = inventory.getItem(0); + ItemStack second = inventory.getItem(1); + ItemStack result = event.getResult(); + + // Prevent modifying a FINAL item + if (getDupe().checkEffectiveTag(first, ItemTag.FINAL) || getDupe().checkEffectiveTag(second, ItemTag.FINAL)) { + if (isModifyingCraft(first, result) || isModifyingCraft(second, result)) { + event.setResult(null); + return; + } + } + + // Prevent creating PROTECTED items + if (getDupe().checkEffectiveTag(result, ItemTag.PROTECTED)) { + event.setResult(null); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void furnaceBurnEvent(FurnaceBurnEvent event) { + Block block = event.getBlock(); + BlockState state = block.getState(); + BlockInventoryHolder holder = (BlockInventoryHolder) state; + Furnace furnace = (Furnace) holder; + if (dropIllegalSlots(furnace)) { + event.setCancelled(true); + event.setBurning(false); + } + } + + + @EventHandler + public void brewingStandFuel(BrewingStandFuelEvent event) { + Block block = event.getBlock(); + BlockState state = block.getState(); + BlockInventoryHolder holder = (BlockInventoryHolder) state; + BrewingStand stand = (BrewingStand) holder; + if (dropIllegalSlots(stand)) { + event.setCancelled(true); + event.setFuelPower(0); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onFurnace(InventoryBlockStartEvent event) { + Block block = event.getBlock(); + BlockState state = block.getState(); + if (state instanceof Campfire campfire) { + ItemStack source = event.getSource().clone(); + if (getDupe().checkEffectiveTag(source, ItemTag.FINAL) || getDupe().checkEffectiveTag(source, ItemTag.PROTECTED)) { + block.setType(Material.AIR); + block.setType(campfire.getType()); + block.setBlockData(campfire.getBlockData()); + source.setAmount(1); + block.getWorld().dropItem(block.getLocation(), source); + + } + return; + } + BlockInventoryHolder holder = (BlockInventoryHolder) state; + switch (holder) { + case Furnace furnace -> dropIllegalSlots(furnace); + case BrewingStand stand -> dropIllegalSlots(stand); + default -> { + } + } + } + + + private boolean dropIllegalSlots(BlockInventoryHolder holder) { + int[] dropProtected; + int[] dropFinal; + boolean result = false; + switch (holder) { + case BrewingStand ignored -> { + dropProtected = new int[]{4, 3}; + dropFinal = new int[]{0, 1, 2}; + } + case Furnace ignored -> { + dropProtected = new int[]{0, 1}; + dropFinal = new int[]{0}; + } + default -> { + return true; + } + } + for (int i : dropProtected) { + if (dropIfTag(holder, i, ItemTag.PROTECTED) && !result) result = true; + } + for (int i : dropFinal) { + if (dropIfTag(holder, i, ItemTag.FINAL) && !result) result = true; + } + return result; + } + + private boolean dropIfTag(BlockInventoryHolder holder, int slot, ItemTag tag) { + ItemStack item = holder.getInventory().getItem(slot); + if (item != null && getDupe().checkEffectiveTag(item, tag)) { + holder.getInventory().setItem(slot, new ItemStack(Material.AIR)); + holder.getBlock().getWorld().dropItem(holder.getBlock().getLocation(), item); + return true; + } + return false; + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onSpecialCraft(PrepareItemCraftEvent event) { + getVerbose().send("Special Craft"); + + CraftingInventory inv = event.getInventory(); + ItemStack result = inv.getResult(); + + // Prevent creating PROTECTED items in any of these custom inventories + if (getDupe().checkEffectiveTag(result, ItemTag.PROTECTED)) { + inv.setResult(null); + return; + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void resultEvent(PrepareResultEvent event) { + getVerbose().send("Result Event"); + ItemStack result = event.getResult(); + if (getDupe().checkEffectiveTag(result, ItemTag.PROTECTED)) { + event.setResult(null); + return; + } + + Inventory inv = event.getInventory(); + getVerbose().send("Inv: {0}", inv.getType()); + // Handle FINAL item modification logic for each type + switch (inv) { + case LoomInventory loom -> { + if (getDupe().checkEffectiveTag(loom.getItem(1), ItemTag.PROTECTED) || getDupe().checkEffectiveTag(loom.getItem(2), ItemTag.PROTECTED)) { + warningAny(Audience.audience(event.getViewers()), "You cannot use a protected item!"); + event.getInventory().close(); + } + if (getDupe().checkEffectiveTag(loom.getItem(0), ItemTag.FINAL)) { + warningAny(Audience.audience(event.getViewers()), "You cannot modify a final item!"); + event.getInventory().close(); + } + } + case CartographyInventory carto -> { + if (getDupe().checkEffectiveTag(carto.getResult(), ItemTag.PROTECTED)) { + warningAny(Audience.audience(event.getViewers()), "You cannot use a protected item!"); + event.getInventory().close(); + } + if (getDupe().checkEffectiveTag(carto.getItem(0), ItemTag.FINAL) || getDupe().checkEffectiveTag(carto.getItem(1), ItemTag.FINAL)) { + warningAny(Audience.audience(event.getViewers()), "You cannot modify a final item!"); + event.getInventory().close(); + } + } + case GrindstoneInventory grind -> { + if (getDupe().checkEffectiveTag(grind.getResult(), ItemTag.PROTECTED)) { + warningAny(Audience.audience(event.getViewers()), "You cannot use a protected item!"); + event.getInventory().close(); + } + if (getDupe().checkEffectiveTag(grind.getItem(0), ItemTag.FINAL) || getDupe().checkEffectiveTag(grind.getItem(1), ItemTag.FINAL)) { + warningAny(Audience.audience(event.getViewers()), "You cannot modify a final item!"); + event.getInventory().close(); + } + } + case StonecutterInventory stone -> { + if (getDupe().checkEffectiveTag(stone.getResult(), ItemTag.PROTECTED)) { + warningAny(Audience.audience(event.getViewers()), "You cannot use a protected item!"); + event.getInventory().close(); + } + if (getDupe().checkEffectiveTag(stone.getItem(0), ItemTag.FINAL)) { + warningAny(Audience.audience(event.getViewers()), "You cannot modify a final item!"); + event.getInventory().close(); + } + } + default -> { + } + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onGrindStone(PrepareGrindstoneEvent event) { + getVerbose().send("Grindstone Event"); + GrindstoneInventory grind = event.getInventory(); + if (getDupe().checkEffectiveTag(grind.getResult(), ItemTag.PROTECTED)) { + grind.setResult(null); + } + if (getDupe().checkEffectiveTag(grind.getItem(0), ItemTag.FINAL) || getDupe().checkEffectiveTag(grind.getItem(1), ItemTag.FINAL)) { + grind.setResult(null); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onCauldron(CauldronLevelChangeEvent event) { + getVerbose().send("Cauldron Event"); + if (event.getEntity() instanceof Player player) { + ItemStack main = player.getInventory().getItemInMainHand(); + ItemStack off = player.getInventory().getItemInOffHand(); + if (main.getType().equals(Material.BUCKET) || main.getType().equals(Material.GLASS_BOTTLE) || main.getType().name().contains("BANNER") || main.getType().name().contains("ARMOR")) { + if (getDupe().checkEffectiveTag(main, ItemTag.FINAL) || getDupe().checkEffectiveTag(main, ItemTag.FINAL)) { + event.setCancelled(true); + warningAny(player, "That item is final and cannot be modified!"); + } + } else if (off.getType().equals(Material.BUCKET) || off.getType().equals(Material.GLASS_BOTTLE) || off.getType().name().contains("BANNER") || off.getType().name().contains("ARMOR")) { + if (getDupe().checkEffectiveTag(off, ItemTag.FINAL) || getDupe().checkEffectiveTag(off, ItemTag.FINAL)) { + event.setCancelled(true); + warningAny(player, "That item is final and cannot be modified!"); + } + } + + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onCommand(PlayerCommandPreprocessEvent e) { + ItemStack main = e.getPlayer().getInventory().getItemInMainHand(); + ItemStack off = e.getPlayer().getInventory().getItemInOffHand(); + if (!getDupe().checkEffectiveTag(main, ItemTag.FINAL) && !getDupe().checkEffectiveTag(off, ItemTag.FINAL)) + return; + + String command = e.getMessage(); + for (String finalCommandRegex : getConfig().finalCommandRegex) { + if (command.matches(finalCommandRegex)) { + e.setCancelled(true); + warningAny(e.getPlayer(), "That item is final and cannot be modified!"); + return; + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPickUp(EntityPickupItemEvent event) { + ItemStack item = event.getItem().getItemStack(); + + if (!(event.getEntity() instanceof Player)) { + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onBlockPlace(BlockPlaceEvent event) { + ItemStack item = event.getItemInHand(); + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + warningAny(event.getPlayer(), "You cannot place protected items!"); + } + if (item.getItemMeta() instanceof BannerMeta && getDupe().checkEffectiveTag(item, ItemTag.FINAL)) { + event.setCancelled(true); + warningAny(event.getPlayer(), "You cannot place final banners!"); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerInteract(PlayerInteractEvent event) { + ItemStack item = event.getItem(); + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + warningAny(event.getPlayer(), "You cannot use protected items!"); + } + + Block targetBlock = event.getPlayer().getTargetBlockExact(4, FluidCollisionMode.ALWAYS); + if (targetBlock != null && targetBlock.getType() == Material.WATER) { + if (getDupe().checkEffectiveTag(item, ItemTag.FINAL) && item != null && (item.getType().equals(Material.GLASS_BOTTLE) || item.getType().equals(Material.BUCKET))) { + event.setCancelled(true); + warningAny(event.getPlayer(), "You cannot fill final items!"); + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onBucketEmpty(PlayerBucketEmptyEvent event) { + ItemStack item = event.getPlayer().getInventory().getItem(event.getHand()); + boolean isFinal = getDupe().checkEffectiveTag(item, ItemTag.FINAL); + boolean isProtected = getDupe().checkEffectiveTag(item, ItemTag.PROTECTED); + if (isProtected || isFinal) { + String message = isFinal ? "You cannot drain final buckets!" : "You cannot drain protected buckets!"; + warningAny(event.getPlayer(), message); + event.setCancelled(true); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onBucketFill(PlayerBucketFillEvent event) { + ItemStack item = event.getPlayer().getInventory().getItem(event.getHand()); + boolean isFinal = getDupe().checkEffectiveTag(item, ItemTag.FINAL); + boolean isProtected = getDupe().checkEffectiveTag(item, ItemTag.PROTECTED); + if (isProtected || isFinal) { + String message = isFinal ? "You cannot fill final buckets!" : "You cannot fill protected buckets!"; + warningAny(event.getPlayer(), message); + event.setCancelled(true); + } + } + + + @EventHandler + public void onBucketFish(PlayerBucketEntityEvent event) { + ItemStack item = event.getOriginalBucket(); + boolean isFinal = getDupe().checkEffectiveTag(item, ItemTag.FINAL); + boolean isProtected = getDupe().checkEffectiveTag(item, ItemTag.PROTECTED); + if (isProtected || isFinal) { + String message = isFinal ? "You cannot fish with final buckets!" : "You cannot fish with protected buckets!"; + warningAny(event.getPlayer(), message); + event.setCancelled(true); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + ItemStack item = event.getPlayer().getInventory().getItem(event.getHand()); + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onItemConsume(PlayerItemConsumeEvent event) { + ItemStack item = event.getItem(); + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + warningAny(event.getPlayer(), "You cannot consume protected items!"); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onBowShoot(EntityShootBowEvent event) { + ItemStack bow = event.getBow(); + ItemStack consumable = event.getConsumable(); + boolean isFinal = getDupe().checkEffectiveTag(bow, ItemTag.FINAL); + boolean isProtected = getDupe().checkEffectiveTag(bow, ItemTag.PROTECTED) || getDupe().checkEffectiveTag(consumable, ItemTag.PROTECTED); + + if (isFinal || isProtected) { + event.getProjectile().remove(); + event.setCancelled(true); + if (event.getEntity() instanceof Player player) { + if (consumable != null) player.getInventory().addItem(consumable); + String message = isFinal ? "You cannot use final bows!" : "You cannot shoot protected items!"; + warningAny(player, message); + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onProjectileLaunch(ProjectileLaunchEvent event) { + if (event.getEntity().getShooter() instanceof Player player) { + // This covers tridents, etc. held in hand + ItemStack item = player.getInventory().getItemInMainHand(); + if (event.getEntity() instanceof ThrowableProjectile t) { + item = t.getItem(); + } + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + warningAny(player, "You cannot use protected items!"); + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onAttack(EntityDamageByEntityEvent event) { + if (event.getDamager() instanceof Player player) { + ItemStack item = player.getInventory().getItemInMainHand(); + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + warningAny(player, "You cannot use protected items!"); + } + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onBreakBlock(BlockBreakEvent event) { + Player player = event.getPlayer(); + ItemStack item = player.getInventory().getItemInMainHand(); + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + warningAny(player, "You cannot use protected items!"); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onDispense(BlockDispenseEvent event) { + ItemStack item = event.getItem(); + + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onItemDamage(PlayerItemDamageEvent event) { + ItemStack item = event.getItem(); + if (getDupe().checkEffectiveTag(item, ItemTag.FINAL)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onItemMend(PlayerItemMendEvent event) { + ItemStack item = event.getItem(); + if (getDupe().checkEffectiveTag(item, ItemTag.FINAL)) { + event.setCancelled(true); + } + } + + + @EventHandler + public void onVillagerClick(TradeSelectEvent event) { + MerchantView view = event.getView(); + ItemStack trade1 = view.getItem(0); + ItemStack trade2 = view.getItem(1); + ItemStack result = view.getItem(2); + + if (getDupe().checkEffectiveTag(trade1, ItemTag.PROTECTED) || getDupe().checkEffectiveTag(trade2, ItemTag.PROTECTED) || getDupe().checkEffectiveTag(result, ItemTag.PROTECTED)) { + event.setCancelled(true); + event.getInventory().close(); + } + } + + + @EventHandler(priority = EventPriority.HIGHEST) + public void onInventoryMove(InventoryMoveItemEvent event) { + ItemStack item = event.getItem(); + if (getDupe().checkEffectiveTag(item, ItemTag.PROTECTED)) { + event.setCancelled(true); + } + } + + + private boolean isModifyingCraft(ItemStack ingredient, ItemStack result) { + if (ingredient == null || result == null) return false; + getVerbose().send("Performing Heuristic check on {0} and {1}", ingredient.getType(), result.getType()); + + if (ingredient.getType() != result.getType()) { + return false; + } + + if (ingredient.isSimilar(result) && ingredient.getAmount() == result.getAmount()) { + return false; + } + + if (result.getAmount() < ingredient.getAmount()) { + return true; + } + + ItemMeta inMeta = ingredient.getItemMeta(); + ItemMeta outMeta = result.getItemMeta(); + if (inMeta == null && outMeta == null) { + return false; + } + if (inMeta == null || outMeta == null) { + return true; + } + + Map inMap = new HashMap<>(inMeta.serialize()); + Map outMap = new HashMap<>(outMeta.serialize()); + inMap.remove("material"); + inMap.remove("amount"); + outMap.remove("material"); + outMap.remove("amount"); + if (!inMap.equals(outMap)) { + return true; + } + + if (!Objects.equals(inMeta.getCustomModelData(), outMeta.getCustomModelData())) { + return true; + } + if (!inMeta.getItemFlags().equals(outMeta.getItemFlags())) { + return true; + } + + if (inMeta instanceof Damageable inD && outMeta instanceof Damageable outD) { + if (inD.getDamage() != outD.getDamage()) { + return true; + } + } + + if (!inMeta.getEnchants().equals(outMeta.getEnchants())) { + return true; + } + + if (!Objects.equals(inMeta.getDisplayName(), outMeta.getDisplayName()) || + !Objects.equals(inMeta.getLore(), outMeta.getLore())) { + return true; + } + + Set keysIn = inMeta.getPersistentDataContainer().getKeys(); + Set keysOut = outMeta.getPersistentDataContainer().getKeys(); + if (!keysIn.equals(keysOut)) { + return true; + } + for (NamespacedKey key : keysIn) { + Object valIn = inMeta.getPersistentDataContainer().get(key, PersistentDataType.TAG_CONTAINER); + Object valOut = outMeta.getPersistentDataContainer().get(key, PersistentDataType.TAG_CONTAINER); + if (!Objects.equals(valIn, valOut)) { + return true; + } + } + + if (inMeta instanceof BannerMeta inB && outMeta instanceof BannerMeta outB) { + if (inB.getPatterns().size() != outB.getPatterns().size()) { + return true; + } + } + if (inMeta instanceof MapMeta inM && outMeta instanceof MapMeta outM) { + boolean iLocked = inM.hasMapView() && inM.getMapView().isLocked(); + boolean oLocked = outM.hasMapView() && outM.getMapView().isLocked(); + if (iLocked != oLocked) return true; + byte iScale = inM.hasMapView() ? inM.getMapView().getScale().getValue() : -1; + byte oScale = outM.hasMapView() ? outM.getMapView().getScale().getValue() : -1; + if (iScale != oScale) return true; + } + if (inMeta instanceof LeatherArmorMeta inL && outMeta instanceof LeatherArmorMeta outL) { + if (!inL.getColor().equals(outL.getColor())) { + return true; + } + } + if (inMeta instanceof PotionMeta inP && outMeta instanceof PotionMeta outP) { + if (!Objects.equals(inP.getBasePotionType(), outP.getBasePotionType()) + || !inP.getCustomEffects().equals(outP.getCustomEffects()) + || !Objects.equals(inP.getColor(), outP.getColor())) { + return true; + } + } + if (inMeta instanceof FireworkMeta inF && outMeta instanceof FireworkMeta outF) { + if (inF.getEffects().size() != outF.getEffects().size()) { + return true; + } + } + if (inMeta instanceof BookMeta inBk && outMeta instanceof BookMeta outBk) { + if (!Objects.equals(inBk.getTitle(), outBk.getTitle()) + || !Objects.equals(inBk.getAuthor(), outBk.getAuthor()) + || !inBk.pages().equals(outBk.pages())) { + return true; + } + } + if (inMeta instanceof CrossbowMeta inC && outMeta instanceof CrossbowMeta outC) { + if (!inC.getChargedProjectiles().equals(outC.getChargedProjectiles()) + || inC.hasChargedProjectiles() != outC.hasChargedProjectiles()) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/me/trouper/dupealias/server/functions/Check.java b/src/main/java/me/trouper/dupealias/server/functions/Check.java new file mode 100644 index 0000000..a082af7 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/functions/Check.java @@ -0,0 +1,7 @@ +package me.trouper.dupealias.server.functions; + +import me.trouper.dupealias.DupeContext; + +public interface Check extends DupeContext { + boolean passes(T input); +} diff --git a/src/main/java/me/trouper/dupealias/server/functions/InventoryCheck.java b/src/main/java/me/trouper/dupealias/server/functions/InventoryCheck.java new file mode 100644 index 0000000..abcfb38 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/functions/InventoryCheck.java @@ -0,0 +1,27 @@ +package me.trouper.dupealias.server.functions; + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class InventoryCheck implements Check { + + private final Check nestedCheck; + + public InventoryCheck(Check nestedCheck) { + this.nestedCheck = nestedCheck; + } + + @Override + public boolean passes(Inventory inventory) { + if (inventory == null) return true; + + for (ItemStack item : inventory.getContents()) { + if (item == null) continue; + if (!nestedCheck.passes(item)) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/me/trouper/dupealias/server/functions/ItemInventoryCheck.java b/src/main/java/me/trouper/dupealias/server/functions/ItemInventoryCheck.java new file mode 100644 index 0000000..8d61bdd --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/functions/ItemInventoryCheck.java @@ -0,0 +1,31 @@ +package me.trouper.dupealias.server.functions; + +import me.trouper.alias.utils.InventoryUtils; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BundleMeta; + +public class ItemInventoryCheck implements Check { + + private final Check nestedCheck; + + public ItemInventoryCheck(Check nestedCheck) { + this.nestedCheck = nestedCheck; + } + + @Override + public boolean passes(ItemStack input) { + if (input == null || !input.hasItemMeta()) return true; + + if (input.getItemMeta() instanceof BundleMeta bundle) { + for (ItemStack item : bundle.getItems()) { + if (!nestedCheck.passes(item)) return false; + } + } + + Inventory subInventory = InventoryUtils.getInventory(input); + if (subInventory == null) return true; + + return new InventoryCheck(nestedCheck).passes(subInventory); + } +} diff --git a/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java b/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java new file mode 100644 index 0000000..a23d1f5 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/functions/UniqueCheck.java @@ -0,0 +1,19 @@ +package me.trouper.dupealias.server.functions; + +import me.trouper.dupealias.server.ItemTag; +import org.bukkit.inventory.ItemStack; +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 set = input.hasItemMeta() && input.getPersistentDataContainer().has(ItemTag.UNIQUE.getKey()); + boolean individuallyUnique = Boolean.TRUE.equals(input.getPersistentDataContainer().get(ItemTag.UNIQUE.getKey(), PersistentDataType.BOOLEAN)); + + if (set && individuallyUnique) return false; + if (!set && globallyUnique) return false; + + return new ItemInventoryCheck(this).passes(input); + } +} diff --git a/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java b/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java new file mode 100644 index 0000000..dd4b246 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/CommonItems.java @@ -0,0 +1,58 @@ +package me.trouper.dupealias.server.gui; + +import me.trouper.alias.utils.FormatUtils; +import me.trouper.alias.utils.ItemBuilder; +import me.trouper.dupealias.DupeAlias; +import me.trouper.dupealias.DupeContext; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +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("") + .modifyMeta(meta->{ + meta.getPersistentDataContainer().set(CANCEL_CLICK(), PersistentDataType.BOOLEAN,Boolean.TRUE); + return meta; + }) + .build(); + } + + default ItemStack EMPTY(Material display) { + return EMPTY().withType(display); + } + + default ItemStack BACK() { + return ItemBuilder.of(Material.ARROW) + .displayName("← Back") + .modifyMeta(meta->{ + meta.getPersistentDataContainer().set(CANCEL_CLICK(), PersistentDataType.BOOLEAN,Boolean.TRUE); + return meta; + }) + .build(); + } + + default boolean shouldBlockClick(ItemStack item) { + if (item == null || item.isEmpty()) return false; + return (item.hasItemMeta() + && item.getItemMeta().getPersistentDataContainer().has(CANCEL_CLICK(),PersistentDataType.BOOLEAN) + && Boolean.TRUE.equals(item.getItemMeta().getPersistentDataContainer().get(CANCEL_CLICK(), PersistentDataType.BOOLEAN))); + } + + default ItemStack createPopulatedItem(ItemStack item) { + 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)) + .displayName("UNIQUE ITEM") + .loreMiniMessage("You are unable to dupe " + FormatUtils.formatEnum(clone.getType())) + .build(); + return clone; + } +} 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 new file mode 100644 index 0000000..b5507f8 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/AdminGui.java @@ -0,0 +1,681 @@ +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/ConfigGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/ConfigGui.java new file mode 100644 index 0000000..bd97898 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/ConfigGui.java @@ -0,0 +1,57 @@ +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/MaterialBrowserGui.java b/src/main/java/me/trouper/dupealias/server/gui/admin/MaterialBrowserGui.java new file mode 100644 index 0000000..6a3903c --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/admin/MaterialBrowserGui.java @@ -0,0 +1,70 @@ +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/dupe/AbstractDupeGui.java b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeGui.java new file mode 100644 index 0000000..e6d15a6 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeGui.java @@ -0,0 +1,24 @@ +package me.trouper.dupealias.server.gui.dupe; + +import me.trouper.dupealias.DupeContext; +import me.trouper.dupealias.server.gui.CommonItems; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public abstract class AbstractDupeGui implements DupeContext, CommonItems { + protected final Map sessions = new HashMap<>(); + + 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)); + } + + public void removeSession(Player player) { + sessions.remove(player.getUniqueId()); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..dbe5895 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/AbstractDupeSession.java @@ -0,0 +1,75 @@ +package me.trouper.dupealias.server.gui.dupe; + +import me.trouper.alias.server.systems.gui.QuickGui; +import me.trouper.dupealias.DupeContext; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +public abstract class AbstractDupeSession implements DupeContext { + protected final Player owner; + protected final QuickGui gui; + protected BukkitTask replicationTask; + protected boolean closed; + + public AbstractDupeSession(Player owner, String title, int rows) { + this.owner = owner; + this.closed = false; + this.gui = buildGui(title, rows); + startTicking(); + } + + protected abstract QuickGui buildGui(String title, int rows); + + protected abstract void tick(); + + protected abstract long getTickDelay(Player player); + + private void startTicking() { + if (replicationTask != null && !replicationTask.isCancelled()) { + replicationTask.cancel(); + } + replicationTask = new BukkitRunnable() { + @Override + public void run() { + if (closed || !owner.isOnline()) { + owner.stopSound(Sound.BLOCK_BEACON_AMBIENT); + cancel(); + replicationTask = null; + return; + } + tick(); + } + }.runTaskTimer(getPlugin(), 0, getTickDelay(owner)); + } + + public void close() { + this.closed = true; + } + + public Inventory open() { + this.closed = false; + if (replicationTask == null || replicationTask.isCancelled()) { + startTicking(); + } + return gui.getInventory(); + } + + public boolean isClosed() { + return closed; + } + + public QuickGui getGui() { + return gui; + } + + public Player getOwner() { + return owner; + } + + public BukkitTask getReplicationTask() { + return replicationTask; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..739de49 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeChestGui.java @@ -0,0 +1,62 @@ +package me.trouper.dupealias.server.gui.dupe; + +import me.trouper.alias.server.systems.gui.QuickGui; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class DupeChestGui extends AbstractDupeGui { + + @Override + protected ChestSession createSession(Player player) { + return new ChestSession(player); + } + + public class ChestSession extends AbstractDupeSession { + + public ChestSession(Player owner) { + super(owner, "DUPE CHEST", 6); + } + + @Override + protected QuickGui buildGui(String title, int rows) { + 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()))) + )) + .allowDrag() + .onCreate((g, i) -> populateInventory(i)) + .onClose((g, e) -> close()) + .build(); + } + + @Override + protected void tick() { + populateInventory(getGui().getInventory()); + } + + @Override + protected long getTickDelay(Player player) { + return 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)); + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..1dda1d3 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeGui.java @@ -0,0 +1,103 @@ +package me.trouper.dupealias.server.gui.dupe; + +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 net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class DupeGui implements DupeContext, CommonItems { + + public final ReplicatorGui replicatorGui = new ReplicatorGui(); + public final DupeInventoryGui inventoryGui = new DupeInventoryGui(); + public final DupeChestGui chestGui = new DupeChestGui(); + + public void openMainGui(Player player) { + QuickGui gui = QuickGui.create() + .rows(5) + .titleMini("Available GUIs") + .item(20, + permissionItem( + player, + "dupealias.gui.replicator", + ItemBuilder.of(Material.DISPENSER) + .displayName("Replicator GUI") + .loreMiniMessage("Open the single-item dupe GUI.") + ), + openSession(replicatorGui,"dupealias.gui.replicator")) + + .item(22,permissionItem( + player, + "dupealias.gui.inventory", + ItemBuilder.of(Material.NETHERITE_CHESTPLATE) + .displayName("Inventory GUI") + .loreMiniMessage("Open a mirror of your own inventory.") + ), + openSession(inventoryGui,"dupealias.gui.inventory")) + + .item(24,permissionItem( + player, + "dupealias.gui.chest", + ItemBuilder.of(Material.ENDER_CHEST) + .displayName("Chest GUI") + .loreMiniMessage("Open the multi-item dupe GUI.") + ), + openSession(chestGui,"dupealias.gui.chest")) + + .fillBorder(EMPTY(Material.PURPLE_STAINED_GLASS_PANE)) + .fillEmpty(EMPTY(Material.WHITE_STAINED_GLASS_PANE)) + .build(); + + gui.open(player); + } + + private ItemStack permissionItem(Player player, String permission, ItemBuilder builder) { + if (player.hasPermission(permission)) { + return builder.build(); + } else { + Component name = builder.build().effectiveName(); + return builder.displayName("Unavailable GUI") + .loreMiniMessage() + .loreComponent(Component.text("You lack the permission to",NamedTextColor.RED),Component.text("use the ", NamedTextColor.RED).decoration(TextDecoration.ITALIC,false).append(name).append(Component.text("."))) + .build().withType(Material.BARRIER); + } + } + + private QuickGui.GuiAction openSession(AbstractDupeGui abstractDupeGui, String guiPermission) { + return (gui, event) -> { + openIfPermission((Player) event.getWhoClicked(),abstractDupeGui,guiPermission); + }; + } + + public void openIfPermission(Player player, AbstractDupeGui abstractDupeGui, String guiPermission) { + if (player.hasPermission(guiPermission)) { + if (player.hasPermission(guiPermission + ".keep")) { + getVerbose().send("Opening existing session for {0}",player.getName()); + abstractDupeGui.getSession(player).getGui().open(player); + } else { + getVerbose().send("Creating new session for {0}",player.getName()); + player.openInventory(abstractDupeGui.createSession(player).open()); + } + } else { + player.closeInventory(); + warningAny(player,"You do not have permission to use that GUI!"); + } + } + + public void openDefaultGui(Player player) { + switch (getConfig().defaultDupeGui) { + case "REPLICATOR" -> openIfPermission(player,replicatorGui,"dupealias.gui.replicator"); + case "INVENTORY" -> openIfPermission(player,inventoryGui,"dupealias.gui.inventory"); + case "CHEST" -> openIfPermission(player,chestGui,"dupealias.gui.chest"); + case "MENU" -> openMainGui(player); + default -> { + infoAny(player,"There is currently no default Dupe GUI."); + } + } + } +} 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 new file mode 100644 index 0000000..55a4c2f --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/DupeInventoryGui.java @@ -0,0 +1,73 @@ +package me.trouper.dupealias.server.gui.dupe; + +import me.trouper.alias.server.systems.gui.QuickGui; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.Inventory; + +public class DupeInventoryGui extends AbstractDupeGui { + + @Override + protected InventorySession createSession(Player player) { + return new InventorySession(player); + } + + public class InventorySession extends AbstractDupeSession { + + public InventorySession(Player owner) { + super(owner, "YOUR INVENTORY", 6); + } + + @Override + protected QuickGui buildGui(String title, int rows) { + 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()))) + )) + .allowDrag() + .onCreate((g, i) -> populateInventory(getOwner(), i)) + .onClose((g, e) -> close()) + .build(); + } + + @Override + protected void tick() { + populateInventory(getOwner(), getGui().getInventory()); + } + + @Override + protected long getTickDelay(Player player) { + return 1; + } + } + + private void populateInventory(Player player, Inventory inv) { + EntityEquipment equipment = player.getEquipment(); + for (int i = 0; i < 18; i++) { + inv.setItem(i, EMPTY()); + } + + inv.setItem(0, createPopulatedItem(equipment.getHelmet())); + inv.setItem(1, createPopulatedItem(equipment.getChestplate())); + inv.setItem(2, createPopulatedItem(equipment.getLeggings())); + inv.setItem(3, createPopulatedItem(equipment.getBoots())); + inv.setItem(6, createPopulatedItem(equipment.getItemInOffHand())); + + for (int i = 0; i < 9; i++) { + inv.setItem(i + 18, createPopulatedItem(player.getInventory().getItem(i))); + } + for (int i = 27; i < 36; i++) { + inv.setItem(i, createPopulatedItem(player.getInventory().getItem(i))); + } + for (int i = 36; i < 45; i++) { + inv.setItem(i, createPopulatedItem(player.getInventory().getItem(i - 18))); + } + for (int i = 45; i < 54; i++) { + inv.setItem(i, createPopulatedItem(player.getInventory().getItem(i - 36))); + } + } +} \ 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 new file mode 100644 index 0000000..aeed0f6 --- /dev/null +++ b/src/main/java/me/trouper/dupealias/server/gui/dupe/ReplicatorGui.java @@ -0,0 +1,173 @@ +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 new file mode 100644 index 0000000..398f39c --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,61 @@ +name: DupeAlias +version: '0.0.1' +main: me.trouper.dupealias.DupeAlias +api-version: '1.21' +prefix: DupeAlias +authors: [ obvWolf ] +description: A powerful dupe plugin with niche features for creating unique servers. +commands: + dupealias: + description: A command to manage the plugin. + permission: dupealias.admin + usage: none + aliases: + - da + dupe: + description: A command to duplicate items. + permission: dupealias.dupe + usage: /dupe [gui|] +permissions: + dupealias.admin: + description: Allows access to the /dupealias admin command. + default: op + dupealias.final.bypass: + description: Allows the bypassing of final item restrictions + dupealias.protected.bypass: + description: Allows the bypassing of protected item restrictions + dupealias.unique.bypass: + description: Allows the duping of unique items + dupealias.dupe: + description: Allows duplication of items through the command. + default: true + children: + dupealias.dupe.cooldownbypass: op + dupealias.dupe.cooldownbypass: + description: Bypass the cooldown for /dupe + default: op + dupealias.infinite: + description: Allows the use of items tagged as "infinite". + default: true + dupealias.gui: + description: Allows access to the full dupe GUI. + default: true + children: + dupealias.gui.replicator: true + dupealias.gui.inventory: true + dupealias.gui.chest: true + dupealias.gui.replicator: + description: The gui which lets you shift click copies of a single item. + default: true + children: + dupalias.gui.replicator.keep: op + dupealias.gui.inventory: + description: The gui which shows your inventory and armor on top. + default: true + children: + dupalias.gui.inventory.keep: op + 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