From 58b7d44d8fded9cddc204dc15a75275f3752e81f Mon Sep 17 00:00:00 2001 From: cybsec Date: Sun, 8 Feb 2026 10:59:02 -0500 Subject: [PATCH] v1.3.1: NBT preservation, GUI previews, and global placement toggle --- build.gradle.kts | 2 +- oyeOwner/INTEGRATION_GUIDE.md | 57 ++++ oyeOwner/pom.xml | 73 +++++ .../java/party/cybsec/CoreProtectHook.java | 56 ++++ .../src/main/java/party/cybsec/OyeOwner.java | 50 +++ .../main/java/party/cybsec/OyeOwnerAPI.java | 59 ++++ .../java/party/cybsec/command/WhoCommand.java | 47 +++ oyeOwner/src/main/resources/plugin.yml | 14 + .../party/cybsec/CoreProtectHook.class | Bin 0 -> 2498 bytes .../classes/party/cybsec/OyeOwner.class | Bin 0 -> 2018 bytes .../classes/party/cybsec/OyeOwnerAPI.class | Bin 0 -> 2857 bytes .../party/cybsec/command/WhoCommand.class | Bin 0 -> 2714 bytes oyeOwner/target/classes/plugin.yml | 14 + oyeOwner/target/maven-archiver/pom.properties | 3 + .../compile/default-compile/createdFiles.lst | 4 + .../compile/default-compile/inputFiles.lst | 4 + .../oyeshops/command/AdminCommands.java | 284 ++++++++++++++---- .../cybsec/oyeshops/config/ConfigManager.java | 9 + .../oyeshops/database/DatabaseManager.java | 10 +- .../oyeshops/database/ShopRepository.java | 45 ++- .../cybsec/oyeshops/gui/ConfigDialog.java | 238 +++++++++++---- .../cybsec/oyeshops/gui/ConfirmationGui.java | 116 ------- .../cybsec/oyeshops/gui/SetupDialog.java | 72 +++-- .../listener/ChestInteractionListener.java | 249 +++++++++------ .../oyeshops/listener/LoginListener.java | 31 +- .../listener/ShopActivationListener.java | 67 ++--- .../party/cybsec/oyeshops/model/Shop.java | 25 +- .../oyeshops/registry/ShopRegistry.java | 28 ++ .../transaction/TransactionManager.java | 83 ++++- src/main/resources/config.yml | 3 + src/main/resources/plugin.yml | 4 +- 31 files changed, 1204 insertions(+), 443 deletions(-) create mode 100644 oyeOwner/INTEGRATION_GUIDE.md create mode 100644 oyeOwner/pom.xml create mode 100644 oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java create mode 100644 oyeOwner/src/main/java/party/cybsec/OyeOwner.java create mode 100644 oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java create mode 100644 oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java create mode 100644 oyeOwner/src/main/resources/plugin.yml create mode 100644 oyeOwner/target/classes/party/cybsec/CoreProtectHook.class create mode 100644 oyeOwner/target/classes/party/cybsec/OyeOwner.class create mode 100644 oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class create mode 100644 oyeOwner/target/classes/party/cybsec/command/WhoCommand.class create mode 100644 oyeOwner/target/classes/plugin.yml create mode 100644 oyeOwner/target/maven-archiver/pom.properties create mode 100644 oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst delete mode 100644 src/main/java/party/cybsec/oyeshops/gui/ConfirmationGui.java diff --git a/build.gradle.kts b/build.gradle.kts index 6ca7c02..1d356fd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } group = "party.cybsec" -version = "1.3.0" +version = "1.3.1" description = "deterministic item-for-item chest barter" repositories { diff --git a/oyeOwner/INTEGRATION_GUIDE.md b/oyeOwner/INTEGRATION_GUIDE.md new file mode 100644 index 0000000..452f6da --- /dev/null +++ b/oyeOwner/INTEGRATION_GUIDE.md @@ -0,0 +1,57 @@ +# oyeOwner Integration Guide for AI Assistants + +This document provides concise instructions for integrating the **oyeOwner** API into other Bukkit/Spigot plugins. + +## 1. Dependency Configuration (Maven) +Add the `oyeOwner` project as a dependency in your `pom.xml`. + +```xml + + party.cybsec + oyeOwner + 1.0-SNAPSHOT + provided + +``` + +## 2. Plugin Configuration (plugin.yml) +Add `oyeOwner` as a dependency to ensure it loads before your plugin. + +```yaml +depend: [oyeOwner] +``` + +## 3. Java API Usage + +### Accessing the API +The API is accessible via a static getter in the main class: `party.cybsec.OyeOwner.getAPI()`. + +### Sync Lookup (Blocking) +Use this if you are already in an asynchronous task or if a tiny delay is acceptable. +```java +import org.bukkit.block.Block; +import party.cybsec.OyeOwner; + +// Returns String username or null +String owner = OyeOwner.getAPI().getBlockOwner(block); +``` + +### Async Lookup (Non-blocking) +Recommended for use on the main thread to avoid lag. +```java +import org.bukkit.block.Block; +import party.cybsec.OyeOwner; + +OyeOwner.getAPI().getBlockOwnerAsync(block).thenAccept(owner -> { + if (owner != null) { + // Player name found: owner + } else { + // No ownership data found + } +}); +``` + +## 4. Summary of Capabilities +- **Lookback Period**: 60 days. +- **Action Tracked**: Block Placement (Action ID 1). +- **Core Engine**: Powered by CoreProtect with a reflection-based safe hook. diff --git a/oyeOwner/pom.xml b/oyeOwner/pom.xml new file mode 100644 index 0000000..8575d8d --- /dev/null +++ b/oyeOwner/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + party.cybsec + oyeOwner + 1.0-SNAPSHOT + jar + + oyeOwner + + + 21 + UTF-8 + + + + + spigotmc-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + sonatype + https://oss.sonatype.org/content/groups/public/ + + + playpro + https://maven.playpro.com + + + + + + org.spigotmc + spigot-api + 1.21.1-R0.1-SNAPSHOT + provided + + + net.coreprotect + coreprotect + 23.1 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + + src/main/resources + true + + + + diff --git a/oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java b/oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java new file mode 100644 index 0000000..2f61634 --- /dev/null +++ b/oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java @@ -0,0 +1,56 @@ +package party.cybsec; + +import net.coreprotect.CoreProtectAPI; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; + +public class CoreProtectHook { + + private final OyeOwner plugin; + + public CoreProtectHook(OyeOwner plugin) { + this.plugin = plugin; + } + + public CoreProtectAPI getCoreProtect() { + Plugin cpPlugin = Bukkit.getPluginManager().getPlugin("CoreProtect"); + + // Check that CoreProtect is loaded and enabled + if (cpPlugin == null || !cpPlugin.isEnabled()) { + return null; + } + + // Use reflection to get the API to avoid NoClassDefFoundError at load time + try { + // Verify it's the right class name + if (!cpPlugin.getClass().getName().equals("net.coreprotect.CoreProtect")) { + return null; + } + + Method getAPIMethod = cpPlugin.getClass().getMethod("getAPI"); + Object apiObject = getAPIMethod.invoke(cpPlugin); + + if (apiObject instanceof CoreProtectAPI) { + CoreProtectAPI api = (CoreProtectAPI) apiObject; + + // Check that the API is enabled + if (!api.isEnabled()) { + return null; + } + + // Check that a compatible version of the API is loaded + if (api.APIVersion() < 11) { + return null; + } + + return api; + } + } catch (Exception e) { + plugin.getLogger().warning("Failed to hook into CoreProtect API via reflection: " + e.getMessage()); + } + + return null; + } +} diff --git a/oyeOwner/src/main/java/party/cybsec/OyeOwner.java b/oyeOwner/src/main/java/party/cybsec/OyeOwner.java new file mode 100644 index 0000000..09cba8c --- /dev/null +++ b/oyeOwner/src/main/java/party/cybsec/OyeOwner.java @@ -0,0 +1,50 @@ +package party.cybsec; + +import party.cybsec.command.WhoCommand; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.logging.Logger; + +public class OyeOwner extends JavaPlugin { + + private static OyeOwner instance; + private CoreProtectHook coreProtectHook; + private OyeOwnerAPI api; + + @Override + public void onEnable() { + instance = this; + Logger logger = getLogger(); + logger.info("oyeOwner is enabling..."); + + this.coreProtectHook = new CoreProtectHook(this); + this.api = new OyeOwnerAPI(this); + + if (coreProtectHook.getCoreProtect() == null) { + logger.severe("CoreProtect not found or incompatible! Disabling oyeOwner."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + getCommand("who").setExecutor(new WhoCommand(this)); + + logger.info("oyeOwner enabled successfully."); + } + + @Override + public void onDisable() { + getLogger().info("oyeOwner disabled."); + } + + public CoreProtectHook getCoreProtectHook() { + return coreProtectHook; + } + + public OyeOwnerAPI getOyeAPI() { + return api; + } + + public static OyeOwnerAPI getAPI() { + return instance.api; + } +} diff --git a/oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java b/oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java new file mode 100644 index 0000000..f599e4d --- /dev/null +++ b/oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java @@ -0,0 +1,59 @@ +package party.cybsec; + +import net.coreprotect.CoreProtectAPI; +import org.bukkit.block.Block; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class OyeOwnerAPI { + + private final OyeOwner plugin; + + public OyeOwnerAPI(OyeOwner plugin) { + this.plugin = plugin; + } + + /** + * Get the owner (player who placed) of a block. + * Searches back 60 days. + * + * @param block The block to check. + * @return The username of the player who placed the block, or null if not + * found/error. + */ + public String getBlockOwner(Block block) { + CoreProtectAPI api = plugin.getCoreProtectHook().getCoreProtect(); + if (api == null || block == null) { + return null; + } + + // 60 days in seconds + int time = 60 * 24 * 60 * 60; + + List lookup = api.blockLookup(block, time); + if (lookup == null || lookup.isEmpty()) { + return null; + } + + for (String[] result : lookup) { + CoreProtectAPI.ParseResult parsed = api.parseResult(result); + // Action ID 1 is "Placement" + if (parsed.getActionId() == 1) { + return parsed.getPlayer(); + } + } + + return null; + } + + /** + * Get the owner of a block asynchronously. + * + * @param block The block to check. + * @return A CompletableFuture containing the username or null. + */ + public CompletableFuture getBlockOwnerAsync(Block block) { + return CompletableFuture.supplyAsync(() -> getBlockOwner(block)); + } +} diff --git a/oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java b/oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java new file mode 100644 index 0000000..3655f49 --- /dev/null +++ b/oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java @@ -0,0 +1,47 @@ +package party.cybsec.command; + +import party.cybsec.OyeOwner; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class WhoCommand implements CommandExecutor { + + private final OyeOwner plugin; + + public WhoCommand(OyeOwner plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(ChatColor.RED + "Only players can use this command."); + return true; + } + + Player player = (Player) sender; + Block targetBlock = player.getTargetBlockExact(5); + + if (targetBlock == null || targetBlock.getType() == Material.AIR) { + player.sendMessage(ChatColor.RED + "You must be looking at a block."); + return true; + } + + String owner = plugin.getOyeAPI().getBlockOwner(targetBlock); + + if (owner != null) { + player.sendMessage(ChatColor.DARK_AQUA + "--- Block Owner Info ---"); + player.sendMessage(ChatColor.GOLD + "user: " + ChatColor.WHITE + owner); + player.sendMessage(ChatColor.DARK_AQUA + "------------------------"); + return true; + } + + player.sendMessage(ChatColor.GRAY + "No placement records found for this block."); + return true; + } +} diff --git a/oyeOwner/src/main/resources/plugin.yml b/oyeOwner/src/main/resources/plugin.yml new file mode 100644 index 0000000..e4914f5 --- /dev/null +++ b/oyeOwner/src/main/resources/plugin.yml @@ -0,0 +1,14 @@ +name: oyeOwner +version: '1.0' +main: party.cybsec.OyeOwner +api-version: '1.21' +depend: [CoreProtect] +commands: + who: + description: check who placed the block you are looking at + permission: oyeowner.use + usage: / +permissions: + oyeowner.use: + description: Allows player to use the /who command + default: op diff --git a/oyeOwner/target/classes/party/cybsec/CoreProtectHook.class b/oyeOwner/target/classes/party/cybsec/CoreProtectHook.class new file mode 100644 index 0000000000000000000000000000000000000000..7185bf044482fe45f2bc63e425bbb3df64f77b61 GIT binary patch literal 2498 zcmah~TXz#x6#h<2ph~M$r>!y=dB|cUd4gFu2%;G};uX8ZyuX`YO`(HuU1gQq?Hx zla6c5yN*Y=aNnfg!%ciGK z1Ro+Q8Qy#lrXX!uGF*Y91B3aPWq63*WRy!|k3c4dD1*J&r=VLy4|)X-y&`}>+p~iO z#s=~YVir8tv`e||G2}An!vO^cH5@`#pnJQsc!*iOYRe_dSPuBT>3bg0a1{LlX#$wE zWVI^L$&4ESf<Kt10t)?S<(eU|r^!9SpT>y5e%tWIioCZ-6@lk^YJqy$ni5b zvxvcnmK0nOI7s637;$m&2bqrJfQoUvrQs^31XM0A8gA8eSPrbD83osPuMMDA?-z}V z$88mF;~fQg4F$}S+AS+5%mPudKI z3`KcQx;D{0r$s=jzAwQIf&NCSVTJ0UBtlhA$)e}DH2XHL$H*|mb4}XImQn4`6Gp9)i!l^wE4I?) zczvT{Y+?S2UJ!+tl$Nz%dIm|=W^6ubL2ovy3O*M&_CiiuK~?Z2CHitx_N_249OeU@ zVChmF{u9hmg5wr^#Q%>uo8sKxSBHLu7#jWs%1?pKZT{~>lK(r9;;0Sn_=K~)p*22* zvBKXVFgqo*;h%^clelcUN%|YdFJ5r*Idi@eR%eTwO*hZF{f`gBUK+>JCce zZ<^2A3K1k}-AV6q!0E$?(+nOE)f&09iRKW}|Djt!pXhjslL~SQ5(>sM{~|G=V7ik* b9GWF~_+5U}bo2Og@fl|-M-T7?zCzFcb*`U$ literal 0 HcmV?d00001 diff --git a/oyeOwner/target/classes/party/cybsec/OyeOwner.class b/oyeOwner/target/classes/party/cybsec/OyeOwner.class new file mode 100644 index 0000000000000000000000000000000000000000..d23a7554b9450016b4de0b519f0dbc9eab3323b7 GIT binary patch literal 2018 zcmZ`)Yje{^6g}%0%T^SU7(#H!LkXnV4z(z4DaDkMhLAuVl42T4A5dA|IBFcNN0OoW zSEU7afSG>nOlSI!I-TB?WRFChFM6!bJ@?$F{`&XL9{^UtRirSYAg$p9G7O8tZ5fU3 z!GY}?op!fnJH}7uGt;<7m!069A#=laZ2u<1NU^k=Ll!v&s)kW$3{xG`^^XkesNr$T z*goRh&mHbEWNpXuO~>L4)3u?Ps!B{JHH>3|A=l!5O|)9lbhcQk1sb}(-8R~EYCA0> z{94T-PmCh+quZX&9kbCUM5R(uaT-$!&S;p%S%$O6u`r}2(V zt6+vyBt)+Xm+!d3=a#=I#DR(eW@QHF7-ovc#24mjL4%G(hH)!N!7!h+d&fmp!FdWH zp`xGO)t$R4mM|-8@t%f@C{k_Aj!j~coG}ARcL8M$E4ajPlF}XX$FM{>cermzk)1G3 zBL*Ocl=%w7_1K~A2w&e5U8kuFSGOHY9Cl3KZnXKLe%toKs_6Yv1;x3n;R9KjjK`mG zmwGXa4>f#*kHZeAbC-@~SxU87UxvrkQG9}H3O?0v9aVwRf40FY}-h>VHQxiU8zaxoEL-z4(ThXtWNpvLakJe*D5)V;1{anIZ9w%bRQEFLoC z2bd_8P3o@nnrPC(7_Zq5zu!GtrfbXh=rHZ?+a7Hi;jGIQK(LHl z7P5h6j$a^Dyt;2%2U}(*;>igoY**yL z&7eD@`-$@JU@rpLZUh2RJ)&nV1mK&1j^XjZ_-~{GXHUz&K`HkzR$IwW_He5F7KO(| zc3K;$UwDP(@=MJ3Q2G;vmHhiX7-VwgHLfz0U*kp!FUt8Zdbs@-o6_K34_~h2 zzaly*YXnl(l|aG-xt&A~dHMyM#yL#UBAb@`f#$qGLo$O6%#qv$JfYZS2xmf>Q3yHW z`WD|&Sl`pPEZ9HTP>6})he2r9$U^2tm0{)Uk&0BHLjDPX3#4EL zP!Ft literal 0 HcmV?d00001 diff --git a/oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class b/oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class new file mode 100644 index 0000000000000000000000000000000000000000..37bd04a43cb90c4a1c39234de50f72ed979ea824 GIT binary patch literal 2857 zcmbtWTXz#x6#foPGHo(#W6Py!0SgpJlRy9gC8=OrDUGJI5UdK`Ch3q4nasqQ2@vmp z!#Cf2t!rr&9$Y@*S}x!GH!i$_U%% zkcu!O0`WWAT`g;BRyDg=zN1$>fyk6$8Qw*Kp5)|mA9~TJAgUq;RbZr{IbJJUX_Z~Q zl3i@+i}x(unO-W;tYJ2*h9$7CxT#V;iUABNh^u%3Ljrp?)e;!0>fWsF=u3|6F`jwb zt_h4MC%aLf(;)b6>`|~+#XgJ(?C%19PFY}}+lRnm;#<0xt8guWyIav0*c{Yt~b1vU#6Q<`?!iECIxy8Pj@uWb_Dj4)-%IGM^5ZC zG73(pI4QA*HmVSRRdcWE_dTh5O2uiM5eQin2<(1>8fcrmg0lj{x7zOfjgsdWR+W_* z!+8}i;}xcZWpVZEy4y5~Go9Q-eWyB1EIorDb`JDBrQ%hvVN=_eBWTK4*aOoQ&#K-i5?WX02 zlPxtH4b#va(wj#?!5b>xL{Z?_vpQ95tI~8F-SXI`^@gc?TG`a+nqJeQv>uBuqXl0ZKh`c}3@8V1j)9UKrey*D zTC$r?MV~WdpTq-`cT&y_MKiYTxt^mn7IbgbUUB0%GZ=;~d%B^*z)~DDgAuq(G3~e zjI4q$cw26-%6VvKhId&n|55;sy!`FpSBT$XT;uaPS99Ez`_a^o5UKPJP`=}87fO5% zpohE5c%J zC~+~9KCq6_TqqHGg#JY60mcQsmV5I1m_RO^2tPnd3Mb`PR^S)Bl#65%k#(HQDG8!p z2z`S-`EhCO+kgHLYxEcLqdw1wflio}QN%}-hft=C zO0Bk}LdgmxV^AJrSizY13kjj%ka&z<1?kM6gb~njn{Qc`OMJ_+#8T;pm<4{q70Iw& zmk3pX3xRsXo<4ElbJX(p^eu2 z^~L3*F5g_P<&!UV*<$d9ig zV2fh1XjUrRSWB*Kni=nrq^o*uqhv7j=UTAybuoX_5LQaT6AXt;Ya>~xmCGe3DGaAX zIu~?a7ghxOuwOw;MK2C89Pr^Xo7~Bmx@nPR`ROT!zFfQAR1|&aS8zy090M&LE;?4p z*kDk$xLykPw*+-?F=Bfce1A{8U<230(XBji_wmqo@jiriT#Ih*ukJKS*WUCJ7g zZIpFE!;pewDvsj>gXVLhNoa%3MPaN7a<|K-hgo6Ud_yoC^xND#X1NR97*_EV5)8c? z!dc=Ljgz`rEKlF!GOmXc*%3cu1-X}W5veYWVobp)72`Nf%=&Ed!H7Ajxn3nJ`V%e6 zX1OD*64z4^JdI}*Jgee4JWr~WFmGGPDW29ZO$P}JZ zF)i~qGL_C>yq>;vIUS?A8;M{y}N-kVfk&_F-mGjx9=@>dOCsS>n;X<28 z?@i9VD{Yc4hJbf`Ud5t!d_KJzqb4w@V42~VFHXC%aQc=g)*M-O1Bv$Jrbb9ZAY-mk zUi9QjhM22W3c_0A1)Wx6IkU+1Wlrfq^TuMp*(_1X47Dn3p1jjgQpBNYH0pt2y(JJ$ zcw^fumy|zLyAY|pTwUF}G;)R7m@GM2d&eH5u5lohMdKVwz{kMJm!QrILYJKEw4Fu? z6N6N;#7S=(lia>4`y&Oxe?m*|B>i2^PT5n2`=S19bPQY@@gY4(M7Xn z6~#& z7%8_VG{(ZHH_)`V_Y=l@3}bB0l$I4mg~-w@Q8cYJTU$44#u|;5XQg*~PYCZv@c}+m z@R2ORj~Pzv#HnO#nPrjm!sT56Gt^j3i;VcrU#ShC$IyAYAa!A%$dR+PYMTV~WkQRf zY_o0P{^am(?3NzXP?6!fzF2YurG%O_DAF07+qSS3e8zBck5BDPRqzGFk;g+REj_<5 zMDGb5)VVLyFDBm==*E=uAXaF6gJv@{@1Xza=r-8s*j*^UxR_NMpP;4(YJ~ypM}*|B z(5%lx<0{^y8Oga?8EJb9*F2r<@HL z1mnT*eVpvTL?|A5h<)+UeT*>NLGmZ`$Ae9&7o^mSQfl;9DEIJEJaiAQPAGBZAu)UwX7grguUTFiFl!mlgEm5Pb*80}X?8 zD<8ob9K{^X7U+H98cw2!VbtiN{{{*CfD!zNQ@Bg_wj?d@(ea41ZCuCOL>5s5j)F_+ zHfdOM3EzgmI#H0Sf|{*iAj-3mr~|3+RRV>?ly otqvG4>3^7nZqrd!ceLEmamOupyycGX;uFF~3HK>J$Crrz2bAK_pa1{> literal 0 HcmV?d00001 diff --git a/oyeOwner/target/classes/plugin.yml b/oyeOwner/target/classes/plugin.yml new file mode 100644 index 0000000..e4914f5 --- /dev/null +++ b/oyeOwner/target/classes/plugin.yml @@ -0,0 +1,14 @@ +name: oyeOwner +version: '1.0' +main: party.cybsec.OyeOwner +api-version: '1.21' +depend: [CoreProtect] +commands: + who: + description: check who placed the block you are looking at + permission: oyeowner.use + usage: / +permissions: + oyeowner.use: + description: Allows player to use the /who command + default: op diff --git a/oyeOwner/target/maven-archiver/pom.properties b/oyeOwner/target/maven-archiver/pom.properties new file mode 100644 index 0000000..583cbc8 --- /dev/null +++ b/oyeOwner/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=oyeOwner +groupId=party.cybsec +version=1.0-SNAPSHOT diff --git a/oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..d8477ff --- /dev/null +++ b/oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,4 @@ +party/cybsec/command/WhoCommand.class +party/cybsec/OyeOwner.class +party/cybsec/CoreProtectHook.class +party/cybsec/OyeOwnerAPI.class diff --git a/oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..2a3268b --- /dev/null +++ b/oyeOwner/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,4 @@ +/Users/jacktotonchi/oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java +/Users/jacktotonchi/oyeOwner/src/main/java/party/cybsec/OyeOwner.java +/Users/jacktotonchi/oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java +/Users/jacktotonchi/oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java diff --git a/src/main/java/party/cybsec/oyeshops/command/AdminCommands.java b/src/main/java/party/cybsec/oyeshops/command/AdminCommands.java index 6ab96e8..b874685 100644 --- a/src/main/java/party/cybsec/oyeshops/command/AdminCommands.java +++ b/src/main/java/party/cybsec/oyeshops/command/AdminCommands.java @@ -48,13 +48,15 @@ public class AdminCommands implements CommandExecutor, TabCompleter { case "notify" -> handleNotifyToggle(sender); case "info" -> handleInfo(sender); case "help" -> handleHelp(sender); - case "setup" -> handleSetup(sender); - case "config" -> handleConfig(sender); + case "setup" -> handleSetup(sender, args); + case "config" -> handleConfig(sender, args); case "reload" -> handleReload(sender); case "inspect", "i" -> handleInspect(sender); case "spoof", "s" -> handleSpoof(sender); case "unregister", "delete", "remove" -> handleUnregister(sender, args); + case "tpshop" -> handleTpShop(sender, args); case "_activate" -> handleActivate(sender, args); + case "toggleplacement", "toggle-placement" -> handleTogglePlacement(sender); default -> handleHelp(sender); } @@ -93,6 +95,10 @@ public class AdminCommands implements CommandExecutor, TabCompleter { .append(Component.text(" - disable a shop", NamedTextColor.GRAY))); sender.sendMessage(Component.text("/oyeshops unregister ", NamedTextColor.YELLOW) .append(Component.text(" - delete a shop", NamedTextColor.GRAY))); + sender.sendMessage(Component.text("/oyeshops tpshop ", NamedTextColor.YELLOW) + .append(Component.text(" - teleport to a shop", NamedTextColor.GRAY))); + sender.sendMessage(Component.text("/oyeshops toggle-placement", NamedTextColor.YELLOW) + .append(Component.text(" - toggle container placement requirement", NamedTextColor.GRAY))); } } @@ -222,10 +228,13 @@ public class AdminCommands implements CommandExecutor, TabCompleter { private void handleActivate(CommandSender sender, String[] args) { if (!(sender instanceof Player player)) return; - if (args.length < 2) + if (args.length < 3) { + player.sendMessage(Component.text("usage: /oyes _activate ", NamedTextColor.RED)); return; + } String action = args[1].toLowerCase(); + String name = args[2]; PendingActivation activation = plugin.getActivationManager().getAndRemove(player.getUniqueId()); if (activation == null) { @@ -235,10 +244,10 @@ public class AdminCommands implements CommandExecutor, TabCompleter { switch (action) { case "accept" -> { - finalizeShop(player, activation); + finalizeShop(player, activation, name); } case "invert" -> { - finalizeShop(player, activation.invert()); + finalizeShop(player, activation.invert(), name); } case "cancel" -> { player.sendMessage( @@ -247,8 +256,8 @@ public class AdminCommands implements CommandExecutor, TabCompleter { } } - private void finalizeShop(Player player, PendingActivation activation) { - plugin.getShopActivationListener().finalizeShop(player, activation); + private void finalizeShop(Player player, PendingActivation activation, String shopName) { + plugin.getShopActivationListener().finalizeShop(player, activation, shopName); } private void handleEnable(CommandSender sender, String[] args) { @@ -275,7 +284,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter { e.printStackTrace(); } } else { - sender.sendMessage(Component.text("usage: /oyeshops enable ", NamedTextColor.RED)); + sender.sendMessage(Component.text("usage: /oyeshops enable ", NamedTextColor.RED)); } return; } @@ -285,18 +294,25 @@ public class AdminCommands implements CommandExecutor, TabCompleter { return; } - try { - int shopId = Integer.parseInt(args[1]); - Shop shop = plugin.getShopRegistry().getShopById(shopId); - if (shop == null) { - sender.sendMessage(Component.text("shop not found: " + shopId, NamedTextColor.RED)); - return; + String target = args[1]; + Shop shop = plugin.getShopRegistry().getShopByName(target); + if (shop == null) { + try { + int id = Integer.parseInt(target); + shop = plugin.getShopRegistry().getShopById(id); + } catch (NumberFormatException ignored) { } + } + + if (shop == null) { + sender.sendMessage(Component.text("shop not found: " + target, NamedTextColor.RED)); + return; + } + + try { shop.setEnabled(true); - plugin.getShopRepository().setEnabled(shopId, true); - sender.sendMessage(Component.text("shop #" + shopId + " enabled", NamedTextColor.GREEN)); - } catch (NumberFormatException e) { - sender.sendMessage(Component.text("invalid shop id", NamedTextColor.RED)); + plugin.getShopRepository().setEnabled(shop.getId(), true); + sender.sendMessage(Component.text("shop '" + shop.getName() + "' enabled", NamedTextColor.GREEN)); } catch (SQLException e) { sender.sendMessage(Component.text("database error", NamedTextColor.RED)); e.printStackTrace(); @@ -315,7 +331,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter { e.printStackTrace(); } } else { - sender.sendMessage(Component.text("usage: /oyeshops disable ", NamedTextColor.RED)); + sender.sendMessage(Component.text("usage: /oyeshops disable ", NamedTextColor.RED)); } return; } @@ -325,18 +341,25 @@ public class AdminCommands implements CommandExecutor, TabCompleter { return; } - try { - int shopId = Integer.parseInt(args[1]); - Shop shop = plugin.getShopRegistry().getShopById(shopId); - if (shop == null) { - sender.sendMessage(Component.text("shop not found: " + shopId, NamedTextColor.RED)); - return; + String target = args[1]; + Shop shop = plugin.getShopRegistry().getShopByName(target); + if (shop == null) { + try { + int id = Integer.parseInt(target); + shop = plugin.getShopRegistry().getShopById(id); + } catch (NumberFormatException ignored) { } + } + + if (shop == null) { + sender.sendMessage(Component.text("shop not found: " + target, NamedTextColor.RED)); + return; + } + + try { shop.setEnabled(false); - plugin.getShopRepository().setEnabled(shopId, false); - sender.sendMessage(Component.text("shop #" + shopId + " disabled", NamedTextColor.YELLOW)); - } catch (NumberFormatException e) { - sender.sendMessage(Component.text("invalid shop id", NamedTextColor.RED)); + plugin.getShopRepository().setEnabled(shop.getId(), false); + sender.sendMessage(Component.text("shop '" + shop.getName() + "' disabled", NamedTextColor.YELLOW)); } catch (SQLException e) { sender.sendMessage(Component.text("database error", NamedTextColor.RED)); e.printStackTrace(); @@ -350,22 +373,29 @@ public class AdminCommands implements CommandExecutor, TabCompleter { } if (args.length < 2) { - sender.sendMessage(Component.text("usage: /oyeshops unregister ", NamedTextColor.RED)); + sender.sendMessage(Component.text("usage: /oyeshops unregister ", NamedTextColor.RED)); + return; + } + + String target = args[1]; + Shop shop = plugin.getShopRegistry().getShopByName(target); + if (shop == null) { + try { + int id = Integer.parseInt(target); + shop = plugin.getShopRegistry().getShopById(id); + } catch (NumberFormatException ignored) { + } + } + + if (shop == null) { + sender.sendMessage(Component.text("shop not found: " + target, NamedTextColor.RED)); return; } try { - int shopId = Integer.parseInt(args[1]); - Shop shop = plugin.getShopRegistry().getShopById(shopId); - if (shop == null) { - sender.sendMessage(Component.text("shop not found: " + shopId, NamedTextColor.RED)); - return; - } plugin.getShopRegistry().unregister(shop); - plugin.getShopRepository().deleteShop(shopId); - sender.sendMessage(Component.text("shop #" + shopId + " deleted", NamedTextColor.RED)); - } catch (NumberFormatException e) { - sender.sendMessage(Component.text("invalid shop id", NamedTextColor.RED)); + plugin.getShopRepository().deleteShop(shop.getId()); + sender.sendMessage(Component.text("shop '" + shop.getName() + "' deleted", NamedTextColor.RED)); } catch (SQLException e) { sender.sendMessage(Component.text("database error", NamedTextColor.RED)); e.printStackTrace(); @@ -379,7 +409,8 @@ public class AdminCommands implements CommandExecutor, TabCompleter { List subCommands = new ArrayList<>( List.of("help", "setup", "config", "on", "off", "toggle", "notify", "info", "enable", "disable")); if (PermissionManager.isAdmin(sender)) { - subCommands.addAll(List.of("reload", "inspect", "spoof", "unregister")); + subCommands.addAll(List.of("reload", "inspect", "spoof", "unregister", "tpshop", "toggle-placement", + "toggleplacement")); } String partial = args[0].toLowerCase(); for (String sub : subCommands) { @@ -388,20 +419,61 @@ public class AdminCommands implements CommandExecutor, TabCompleter { } } else if (args.length == 2) { String subCommand = args[0].toLowerCase(); - if (PermissionManager.isAdmin(sender) && (subCommand.equals("enable") || subCommand.equals("disable") - || subCommand.equals("unregister"))) { - String partial = args[1]; + if (subCommand.equals("config") && sender instanceof Player player) { + String partial = args[1].toLowerCase(); for (Shop shop : plugin.getShopRegistry().getAllShops()) { + if (shop.getOwner().equals(player.getUniqueId()) + || shop.getContributors().contains(player.getUniqueId())) { + if (shop.getName().toLowerCase().startsWith(partial)) { + completions.add(shop.getName()); + } + } + } + } else if (PermissionManager.isAdmin(sender) && (subCommand.equals("enable") || subCommand.equals("disable") + || subCommand.equals("unregister"))) { + String partial = args[1].toLowerCase(); + for (Shop shop : plugin.getShopRegistry().getAllShops()) { + if (shop.getName().toLowerCase().startsWith(partial)) { + completions.add(shop.getName()); + } String id = String.valueOf(shop.getId()); if (id.startsWith(partial)) completions.add(id); } + } else if (subCommand.equals("tpshop") && PermissionManager.isAdmin(sender)) { + String partial = args[1].toLowerCase(); + java.util.Set owners = new java.util.HashSet<>(); + for (Shop shop : plugin.getShopRegistry().getAllShops()) { + org.bukkit.OfflinePlayer owner = org.bukkit.Bukkit.getOfflinePlayer(shop.getOwner()); + if (owner.getName() != null) { + owners.add(owner.getName()); + } + } + for (String name : owners) { + if (name.toLowerCase().startsWith(partial)) { + completions.add(name); + } + } + } + } else if (args.length == 3) { + String subCommand = args[0].toLowerCase(); + if (subCommand.equals("tpshop") && PermissionManager.isAdmin(sender)) { + String ownerName = args[1]; + String partial = args[2].toLowerCase(); + for (Shop shop : plugin.getShopRegistry().getAllShops()) { + org.bukkit.OfflinePlayer owner = org.bukkit.Bukkit.getOfflinePlayer(shop.getOwner()); + if (ownerName.equalsIgnoreCase(owner.getName())) { + if (shop.getName().toLowerCase().startsWith(partial)) { + completions.add(shop.getName()); + } + } + } } } return completions; } - private void handleSetup(CommandSender sender) { + private void handleSetup(CommandSender sender, String[] args) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("players only", NamedTextColor.RED)); return; @@ -412,6 +484,25 @@ public class AdminCommands implements CommandExecutor, TabCompleter { return; } + if (args.length < 2) { + player.sendMessage(Component.text("usage: /oyes setup ", NamedTextColor.RED)); + player.sendMessage(Component.text("example: /oyes setup myEpicBambooShop", NamedTextColor.GREEN)); + return; + } + + String shopName = args[1]; + + if (shopName.equalsIgnoreCase("myEpicBambooShop")) { + player.sendMessage( + Component.text("hey! that's cybsec's shop! choose a different name", NamedTextColor.RED)); + return; + } + + if (plugin.getShopRegistry().getShopByOwnerAndName(player.getUniqueId(), shopName) != null) { + player.sendMessage(Component.text("you already have a shop with that name!", NamedTextColor.RED)); + return; + } + Block block = player.getTargetBlockExact(5); if (block == null || !(block.getBlockData() instanceof WallSign wallSign)) { player.sendMessage(Component.text("you must look at a wall sign to use the wizard", NamedTextColor.RED)); @@ -427,7 +518,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter { return; } - if (!PermissionManager.isAdmin(player)) { + if (plugin.getConfigManager().isRequirePlacement() && !PermissionManager.isAdmin(player)) { java.util.UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation()); if (sessionOwner == null || !sessionOwner.equals(player.getUniqueId())) { player.sendMessage( @@ -437,27 +528,59 @@ public class AdminCommands implements CommandExecutor, TabCompleter { } } - SetupDialog.open(player, block, plugin); + SetupDialog.open(player, block, plugin, shopName); } - private void handleConfig(CommandSender sender) { + private void handleTogglePlacement(CommandSender sender) { + if (!PermissionManager.isAdmin(sender)) { + sender.sendMessage(Component.text("you don't have permission to do this", NamedTextColor.RED)); + return; + } + + boolean current = plugin.getConfigManager().isRequirePlacement(); + boolean newValue = !current; + plugin.getConfigManager().setRequirePlacement(newValue); + + sender.sendMessage(Component.text("global container placement requirement is now ", NamedTextColor.GREEN) + .append(Component.text(newValue ? "ENABLED" : "DISABLED", + newValue ? NamedTextColor.YELLOW : NamedTextColor.RED, + net.kyori.adventure.text.format.TextDecoration.BOLD))); + } + + private void handleConfig(CommandSender sender, String[] args) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("players only", NamedTextColor.RED)); return; } - Block block = player.getTargetBlockExact(5); - if (block == null) { - player.sendMessage(Component.text("you must look at a shop sign or its container", NamedTextColor.RED)); - return; - } - Shop shop = null; - if (block.getBlockData() instanceof WallSign) { - shop = plugin.getShopRegistry().getShop(block.getLocation()); - } else if (party.cybsec.oyeshops.listener.ShopActivationListener.isContainer(block.getType())) { - // try to find shop on any of the adjacent wall signs - shop = plugin.getShopRegistry().getShopByContainer(block); + if (args.length > 1) { + // remote config by name (lookup by owner) + shop = plugin.getShopRegistry().getShopByOwnerAndName(player.getUniqueId(), args[1]); + if (shop == null && PermissionManager.isAdmin(player)) { + // admins can lookup any shop by name if not found in their own + shop = plugin.getShopRegistry().getShopByName(args[1]); + } + + if (shop == null) { + player.sendMessage(Component.text("shop not found: " + args[1], NamedTextColor.RED)); + return; + } + } else { + // target block config + Block block = player.getTargetBlockExact(5); + if (block == null) { + player.sendMessage( + Component.text("you must look at a shop sign or its container, or use /oyes config ", + NamedTextColor.RED)); + return; + } + + if (block.getBlockData() instanceof WallSign) { + shop = plugin.getShopRegistry().getShop(block.getLocation()); + } else if (party.cybsec.oyeshops.listener.ShopActivationListener.isContainer(block.getType())) { + shop = plugin.getShopRegistry().getShopByContainer(block); + } } if (shop == null) { @@ -465,11 +588,50 @@ public class AdminCommands implements CommandExecutor, TabCompleter { return; } - if (!shop.getOwner().equals(player.getUniqueId()) && !PermissionManager.isAdmin(player)) { - player.sendMessage(Component.text("you do not own this shop", NamedTextColor.RED)); + if (!shop.getOwner().equals(player.getUniqueId()) && !shop.getContributors().contains(player.getUniqueId()) + && !PermissionManager.isAdmin(player)) { + player.sendMessage(Component.text("you do not own or contribute to this shop", NamedTextColor.RED)); return; } ConfigDialog.open(player, shop, plugin); } + + private void handleTpShop(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("players only", NamedTextColor.RED)); + return; + } + + if (!PermissionManager.isAdmin(player)) { + player.sendMessage(Component.text("no permission", NamedTextColor.RED)); + return; + } + + if (args.length < 3) { + player.sendMessage(Component.text("usage: /oyes tpshop ", NamedTextColor.RED)); + return; + } + + String ownerName = args[1]; + String shopName = args[2]; + Shop targetShop = null; + + for (Shop shop : plugin.getShopRegistry().getAllShops()) { + org.bukkit.OfflinePlayer owner = org.bukkit.Bukkit.getOfflinePlayer(shop.getOwner()); + if (ownerName.equalsIgnoreCase(owner.getName()) && shopName.equalsIgnoreCase(shop.getName())) { + targetShop = shop; + break; + } + } + + if (targetShop == null) { + player.sendMessage(Component.text("shop not found for that owner and name", NamedTextColor.RED)); + return; + } + + player.teleport(targetShop.getSignLocation().clone().add(0.5, 0, 0.5)); + player.sendMessage( + Component.text("teleporting to shop '" + shopName + "' by " + ownerName, NamedTextColor.GREEN)); + } } diff --git a/src/main/java/party/cybsec/oyeshops/config/ConfigManager.java b/src/main/java/party/cybsec/oyeshops/config/ConfigManager.java index beca5ea..c937009 100644 --- a/src/main/java/party/cybsec/oyeshops/config/ConfigManager.java +++ b/src/main/java/party/cybsec/oyeshops/config/ConfigManager.java @@ -37,4 +37,13 @@ public class ConfigManager { public int getMaxTransactionsPerShop() { return config.getInt("transactions.max-per-shop", 100); } + + public boolean isRequirePlacement() { + return config.getBoolean("require-placement", true); + } + + public void setRequirePlacement(boolean require) { + config.set("require-placement", require); + save(); + } } diff --git a/src/main/java/party/cybsec/oyeshops/database/DatabaseManager.java b/src/main/java/party/cybsec/oyeshops/database/DatabaseManager.java index 2de95ed..06cc5d8 100644 --- a/src/main/java/party/cybsec/oyeshops/database/DatabaseManager.java +++ b/src/main/java/party/cybsec/oyeshops/database/DatabaseManager.java @@ -109,10 +109,18 @@ public class DatabaseManager { } try { - stmt.execute("alter table shops add column disc text"); + stmt.execute("alter table shops add column name text"); } catch (SQLException ignored) { } + try { + stmt.execute("alter table shops add column contributors text"); + } catch (SQLException ignored) { + } + + // migration: set name to shop_id for existing shops where name is null + stmt.execute("update shops set name = cast(shop_id as text) where name is null"); + // indexes stmt.execute(""" create index if not exists idx_shop_location diff --git a/src/main/java/party/cybsec/oyeshops/database/ShopRepository.java b/src/main/java/party/cybsec/oyeshops/database/ShopRepository.java index 73b4523..34b43e8 100644 --- a/src/main/java/party/cybsec/oyeshops/database/ShopRepository.java +++ b/src/main/java/party/cybsec/oyeshops/database/ShopRepository.java @@ -30,8 +30,8 @@ public class ShopRepository { insert into shops (world_uuid, sign_x, sign_y, sign_z, owner_uuid, price_item, price_quantity, product_item, product_quantity, owed_amount, enabled, created_at, custom_title, - cosmetic_sign, disc) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + cosmetic_sign, disc, name, contributors) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """; try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql, @@ -52,6 +52,8 @@ public class ShopRepository { stmt.setString(13, shop.getCustomTitle()); stmt.setBoolean(14, shop.isCosmeticSign()); stmt.setString(15, shop.getDisc()); + stmt.setString(16, shop.getName()); + stmt.setString(17, serializeContributors(shop.getContributors())); stmt.executeUpdate(); @@ -175,14 +177,16 @@ public class ShopRepository { */ public void updateShopConfig(Shop shop) throws SQLException { String sql = """ - update shops set custom_title = ?, disc = ? + update shops set custom_title = ?, disc = ?, name = ?, contributors = ? where shop_id = ? """; try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) { stmt.setString(1, shop.getCustomTitle()); stmt.setString(2, shop.getDisc()); - stmt.setInt(3, shop.getId()); + stmt.setString(3, shop.getName()); + stmt.setString(4, serializeContributors(shop.getContributors())); + stmt.setInt(5, shop.getId()); stmt.executeUpdate(); } } @@ -251,16 +255,47 @@ public class ShopRepository { return shops; } + /** + * serialize list of UUIDs to comma-separated string + */ + private String serializeContributors(List contributors) { + if (contributors == null || contributors.isEmpty()) + return ""; + List uuids = new ArrayList<>(); + for (UUID uuid : contributors) { + uuids.add(uuid.toString()); + } + return String.join(",", uuids); + } + + /** + * deserialize comma-separated string to list of UUIDs + */ + private List deserializeContributors(String data) { + List list = new ArrayList<>(); + if (data == null || data.isEmpty()) + return list; + for (String s : data.split(",")) { + try { + list.add(UUID.fromString(s)); + } catch (IllegalArgumentException ignored) { + } + } + return list; + } + /** * convert result set to shop object */ private Shop shopFromResultSet(ResultSet rs) throws SQLException { int id = rs.getInt("shop_id"); + String name = rs.getString("name"); UUID worldUuid = UUID.fromString(rs.getString("world_uuid")); int x = rs.getInt("sign_x"); int y = rs.getInt("sign_y"); int z = rs.getInt("sign_z"); UUID ownerUuid = UUID.fromString(rs.getString("owner_uuid")); + List contributors = deserializeContributors(rs.getString("contributors")); Material priceItem = Material.valueOf(rs.getString("price_item")); int priceQty = rs.getInt("price_quantity"); Material productItem = Material.valueOf(rs.getString("product_item")); @@ -280,7 +315,7 @@ public class ShopRepository { Location location = new Location(world, x, y, z); Trade trade = new Trade(priceItem, priceQty, productItem, productQty); - return new Shop(id, location, ownerUuid, trade, owedAmount, enabled, createdAt, + return new Shop(id, name, location, ownerUuid, contributors, trade, owedAmount, enabled, createdAt, customTitle, cosmeticSign, disc); } diff --git a/src/main/java/party/cybsec/oyeshops/gui/ConfigDialog.java b/src/main/java/party/cybsec/oyeshops/gui/ConfigDialog.java index 5fe4b83..47de09f 100644 --- a/src/main/java/party/cybsec/oyeshops/gui/ConfigDialog.java +++ b/src/main/java/party/cybsec/oyeshops/gui/ConfigDialog.java @@ -15,81 +15,191 @@ import party.cybsec.oyeshops.OyeShopsPlugin; import party.cybsec.oyeshops.model.Shop; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.UUID; /** * config dialog for shop settings */ public class ConfigDialog { - // valid disc names - private static final Set VALID_DISCS = Set.of( - "none", "blocks", "chirp", "far", "mall", "mellohi", "stal", "strad", "ward", "wait"); + // valid disc names + private static final Set VALID_DISCS = Set.of( + "none", "blocks", "chirp", "far", "mall", "mellohi", "stal", "strad", "ward", "wait"); - /** - * open config dialog for a specific shop - */ - public static void open(Player player, Shop shop, OyeShopsPlugin plugin) { - Dialog dialog = Dialog.create(builder -> builder.empty() - .base(DialogBase.builder(Component.text("shop #" + shop.getId() + " config", NamedTextColor.GOLD)) - .inputs(List.of( - DialogInput.text("custom_title", - Component.text("custom title (optional)", NamedTextColor.YELLOW)) - .initial(shop.getCustomTitle() != null ? shop.getCustomTitle() : "") - .build(), - DialogInput.text("disc", - Component.text( - "music disc: none/blocks/chirp/far/mall/mellohi/stal/strad/ward/wait", - NamedTextColor.AQUA)) - .initial(shop.getDisc() != null ? shop.getDisc() : "none") - .build())) - .build()) - .type(DialogType.confirmation( - ActionButton.builder(Component.text("save", TextColor.color(0xB0FFA0))) - .tooltip(Component.text("save configuration")) - .action(DialogAction.customClick((view, audience) -> { - Player p = (Player) audience; - String title = view.getText("custom_title"); - String disc = view.getText("disc"); + /** + * open config dialog for a specific shop + */ + public static void open(Player player, Shop shop, OyeShopsPlugin plugin) { + Dialog dialog = Dialog.create(builder -> builder.empty() + .base(DialogBase.builder( + Component.text("config: " + shop.getName(), NamedTextColor.GOLD)) + .inputs(List.of( + DialogInput.text("name", + Component.text("rename shop", + NamedTextColor.YELLOW)) + .initial(shop.getName()) + .build(), + DialogInput.text("custom_title", + Component.text("custom title (optional)", + NamedTextColor.YELLOW)) + .initial(shop.getCustomTitle() != null + ? shop.getCustomTitle() + : "") + .build(), + DialogInput.text("disc", + Component.text( + "music disc: none/blocks/chirp/far/mall/mellohi/stal/strad/ward/wait", + NamedTextColor.AQUA)) + .initial(shop.getDisc() != null + ? shop.getDisc() + : "none") + .build(), + DialogInput.text("contributors", + Component.text("contributors (comma separated names)", + NamedTextColor.LIGHT_PURPLE)) + .initial(getContributorNames( + shop.getContributors())) + .build())) + .build()) + .type(DialogType.confirmation( + ActionButton.builder(Component.text("save", TextColor.color(0xB0FFA0))) + .tooltip(Component.text("save configuration")) + .action(DialogAction.customClick((view, audience) -> { + Player p = (Player) audience; + String newName = view.getText("name"); + String title = view.getText("custom_title"); + String disc = view.getText("disc"); + String contributorsStr = view + .getText("contributors"); - // validate disc - disc = disc.toLowerCase().trim(); - if (!disc.isEmpty() && !VALID_DISCS.contains(disc)) { - p.sendMessage(Component.text("invalid disc: " + disc - + ". valid options: none, blocks, chirp, far, mall, mellohi, stal, strad, ward, wait", - NamedTextColor.RED)); - open(p, shop, plugin); // reopen dialog - return; - } + if (newName == null || newName.isEmpty()) { + p.sendMessage(Component.text( + "shop name cannot be empty", + NamedTextColor.RED)); + open(p, shop, plugin); + return; + } - shop.setCustomTitle(title.isEmpty() ? null : title); - shop.setDisc(disc.isEmpty() || disc.equals("none") ? null : disc); + if (newName.equalsIgnoreCase( + "myEpicBambooShop")) { + p.sendMessage(Component.text( + "hey! that's cybsec's shop! choose a different name", + NamedTextColor.RED)); + open(p, shop, plugin); + return; + } - plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - try { - plugin.getShopRepository().updateShopConfig(shop); - plugin.getServer().getScheduler().runTask(plugin, () -> { - p.sendMessage( - Component.text("shop config saved", NamedTextColor.GREEN)); - }); - } catch (SQLException e) { - plugin.getServer().getScheduler().runTask(plugin, () -> { - p.sendMessage(Component.text("database error", NamedTextColor.RED)); - }); - e.printStackTrace(); - } - }); - }, ClickCallback.Options.builder().uses(1).build())) - .build(), - ActionButton.builder(Component.text("cancel", TextColor.color(0xFFA0B1))) - .tooltip(Component.text("discard changes")) - .action(DialogAction.customClick((view, audience) -> { - ((Player) audience) - .sendMessage(Component.text("config cancelled", NamedTextColor.YELLOW)); - }, ClickCallback.Options.builder().build())) - .build()))); + // validate name uniqueness if changed (player + // scoped) + if (!newName.equalsIgnoreCase(shop.getName())) { + if (plugin.getShopRegistry() + .getShopByOwnerAndName( + shop.getOwner(), + newName) != null) { + p.sendMessage(Component.text( + "you already have a shop with that name!", + NamedTextColor.RED)); + open(p, shop, plugin); + return; + } + } - player.showDialog(dialog); - } + // validate disc + disc = disc.toLowerCase().trim(); + if (!disc.isEmpty() && !VALID_DISCS + .contains(disc)) { + p.sendMessage(Component.text( + "invalid disc: " + disc + + ". valid options: none, blocks, chirp, far, mall, mellohi, stal, strad, ward, wait", + NamedTextColor.RED)); + open(p, shop, plugin); + return; + } + + shop.setName(newName); + shop.setCustomTitle( + title.isEmpty() ? null : title); + shop.setDisc(disc.isEmpty() + || disc.equals("none") ? null + : disc); + + // resolve contributors + List contributorUuids = resolveContributors( + contributorsStr); + shop.getContributors().clear(); + shop.getContributors().addAll(contributorUuids); + + plugin.getServer().getScheduler() + .runTaskAsynchronously(plugin, + () -> { + try { + plugin.getShopRepository() + .updateShopConfig( + shop); + plugin.getServer() + .getScheduler() + .runTask(plugin, () -> { + p.sendMessage( + Component.text("shop config saved", + NamedTextColor.GREEN)); + }); + } catch (SQLException e) { + plugin.getServer() + .getScheduler() + .runTask(plugin, () -> { + p.sendMessage(Component + .text("database error", + NamedTextColor.RED)); + }); + e.printStackTrace(); + } + }); + }, ClickCallback.Options.builder().uses(1).build())) + .build(), + ActionButton.builder( + Component.text("cancel", TextColor.color(0xFFA0B1))) + .tooltip(Component.text("discard changes")) + .action(DialogAction.customClick((view, audience) -> { + ((Player) audience) + .sendMessage(Component.text( + "config cancelled", + NamedTextColor.YELLOW)); + }, ClickCallback.Options.builder().build())) + .build()))); + + player.showDialog(dialog); + } + + private static String getContributorNames(List uuids) { + if (uuids == null || uuids.isEmpty()) + return ""; + List names = new ArrayList<>(); + for (UUID uuid : uuids) { + org.bukkit.OfflinePlayer op = org.bukkit.Bukkit.getOfflinePlayer(uuid); + if (op.getName() != null) { + names.add(op.getName()); + } else { + names.add(uuid.toString()); + } + } + return String.join(", ", names); + } + + private static List resolveContributors(String data) { + List list = new ArrayList<>(); + if (data == null || data.isEmpty()) + return list; + for (String part : data.split(",")) { + String name = part.trim(); + if (name.isEmpty()) + continue; + // try to resolve by name + org.bukkit.OfflinePlayer op = org.bukkit.Bukkit.getOfflinePlayer(name); + list.add(op.getUniqueId()); + } + return list; + } } diff --git a/src/main/java/party/cybsec/oyeshops/gui/ConfirmationGui.java b/src/main/java/party/cybsec/oyeshops/gui/ConfirmationGui.java deleted file mode 100644 index 9ecf4cb..0000000 --- a/src/main/java/party/cybsec/oyeshops/gui/ConfirmationGui.java +++ /dev/null @@ -1,116 +0,0 @@ -package party.cybsec.oyeshops.gui; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import party.cybsec.oyeshops.model.Shop; -import party.cybsec.oyeshops.model.Trade; - -import java.util.ArrayList; -import java.util.List; - -/** - * confirmation GUI for shop purchases - * supports batch purchases with units - */ -public class ConfirmationGui implements InventoryHolder { - private final Shop shop; - private final int units; - private final Inventory inventory; - - public ConfirmationGui(Shop shop, int units) { - this.shop = shop; - this.units = Math.max(1, units); - this.inventory = createInventory(); - } - - private Inventory createInventory() { - Trade trade = shop.getTrade(); - int totalPrice = trade.priceQuantity() * units; - int totalProduct = trade.productQuantity() * units; - - String title = units > 1 - ? "confirm: " + units + " units" - : "confirm purchase"; - - Inventory inv = Bukkit.createInventory(this, 27, Component.text(title)); - - // fill with gray glass - ItemStack filler = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta fillerMeta = filler.getItemMeta(); - fillerMeta.displayName(Component.text(" ")); - filler.setItemMeta(fillerMeta); - for (int i = 0; i < 27; i++) { - inv.setItem(i, filler.clone()); - } - - // price item display (left side - slot 3) - ItemStack priceDisplay = new ItemStack(trade.priceItem(), Math.min(totalPrice, 64)); - ItemMeta priceMeta = priceDisplay.getItemMeta(); - priceMeta.displayName(Component.text("you pay:", NamedTextColor.RED)); - List priceLore = new ArrayList<>(); - priceLore.add(Component.text(totalPrice + " " + formatMaterial(trade.priceItem()), NamedTextColor.WHITE)); - if (units > 1) { - priceLore.add(Component.text("(" + trade.priceQuantity() + " × " + units + " units)", NamedTextColor.GRAY)); - } - priceMeta.lore(priceLore); - priceDisplay.setItemMeta(priceMeta); - inv.setItem(3, priceDisplay); - - // product item display (right side - slot 5) - ItemStack productDisplay = new ItemStack(trade.productItem(), Math.min(totalProduct, 64)); - ItemMeta productMeta = productDisplay.getItemMeta(); - productMeta.displayName(Component.text("you get:", NamedTextColor.GREEN)); - List productLore = new ArrayList<>(); - productLore.add(Component.text(totalProduct + " " + formatMaterial(trade.productItem()), NamedTextColor.WHITE)); - if (units > 1) { - productLore.add( - Component.text("(" + trade.productQuantity() + " × " + units + " units)", NamedTextColor.GRAY)); - } - productMeta.lore(productLore); - productDisplay.setItemMeta(productMeta); - inv.setItem(5, productDisplay); - - // confirm button (green - bottom left) - ItemStack confirm = new ItemStack(Material.LIME_STAINED_GLASS_PANE); - ItemMeta confirmMeta = confirm.getItemMeta(); - confirmMeta.displayName(Component.text("CONFIRM", NamedTextColor.GREEN)); - confirmMeta.lore(List.of(Component.text("click to complete purchase", NamedTextColor.GRAY))); - confirm.setItemMeta(confirmMeta); - inv.setItem(11, confirm); - inv.setItem(12, confirm.clone()); - - // cancel button (red - bottom right) - ItemStack cancel = new ItemStack(Material.RED_STAINED_GLASS_PANE); - ItemMeta cancelMeta = cancel.getItemMeta(); - cancelMeta.displayName(Component.text("CANCEL", NamedTextColor.RED)); - cancelMeta.lore(List.of(Component.text("click to cancel", NamedTextColor.GRAY))); - cancel.setItemMeta(cancelMeta); - inv.setItem(14, cancel); - inv.setItem(15, cancel.clone()); - - return inv; - } - - private String formatMaterial(Material material) { - return material.name().toLowerCase().replace("_", " "); - } - - public Shop getShop() { - return shop; - } - - public int getUnits() { - return units; - } - - @Override - public Inventory getInventory() { - return inventory; - } -} diff --git a/src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java b/src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java index cb0d624..26b7673 100644 --- a/src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java +++ b/src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java @@ -24,10 +24,16 @@ import java.util.List; */ public class SetupDialog { - public static void open(Player player, Block signBlock, OyeShopsPlugin plugin) { + public static void open(Player player, Block signBlock, OyeShopsPlugin plugin, String shopName) { Dialog dialog = Dialog.create(builder -> builder.empty() .base(DialogBase.builder(Component.text("shop setup wizard", NamedTextColor.GOLD)) .inputs(List.of( + // shop name + DialogInput.text("shop_name", + Component.text("shop name (unique)", + NamedTextColor.YELLOW)) + .initial(shopName) + .build(), // product (selling) DialogInput.text("product_item", Component.text("selling what? (e.g. dirt)", @@ -41,22 +47,13 @@ public class SetupDialog { // price (buying) DialogInput.text("price_item", - Component.text("what do you want? (e.g. diamond)", + Component.text("what do you want? (prop: diamond)", NamedTextColor.GREEN)) .build(), DialogInput.text("price_qty", Component.text("how many? (e.g. 10)", NamedTextColor.GREEN)) .initial("1") - .build(), - - // cosmetic sign toggle - DialogInput.bool("cosmetic_sign", - Component.text("cosmetic sign? (don't rewrite text)", - NamedTextColor.AQUA)) - .initial(false) - .onTrue("enabled") - .onFalse("disabled") .build())) .build()) .type(DialogType.confirmation( @@ -65,6 +62,7 @@ public class SetupDialog { .tooltip(Component .text("click to confirm trade details")) .action(DialogAction.customClick((view, audience) -> { + String newShopName = view.getText("shop_name"); String productStr = view .getText("product_item"); String productQtyStr = view @@ -73,6 +71,36 @@ public class SetupDialog { String priceQtyStr = view.getText("price_qty"); Player p = (Player) audience; + // shop name validation + if (newShopName == null + || newShopName.isEmpty()) { + p.sendMessage(Component.text( + "shop name cannot be empty", + NamedTextColor.RED)); + open(p, signBlock, plugin, shopName); + return; + } + + if (newShopName.equalsIgnoreCase( + "myEpicBambooShop")) { + p.sendMessage(Component.text( + "hey! that's cybsec's shop! choose a different name", + NamedTextColor.RED)); + open(p, signBlock, plugin, newShopName); + return; + } + + if (plugin.getShopRegistry() + .getShopByOwnerAndName( + p.getUniqueId(), + newShopName) != null) { + p.sendMessage(Component.text( + "you already have a shop with that name!", + NamedTextColor.RED)); + open(p, signBlock, plugin, newShopName); + return; + } + // 1. parse price qty int priceQty; try { @@ -82,7 +110,7 @@ public class SetupDialog { p.sendMessage(Component.text( "invalid price quantity", NamedTextColor.RED)); - open(p, signBlock, plugin); + open(p, signBlock, plugin, newShopName); return; } @@ -94,11 +122,11 @@ public class SetupDialog { "invalid payment item: " + priceStr, NamedTextColor.RED)); - open(p, signBlock, plugin); + open(p, signBlock, plugin, newShopName); return; } - // 3. Regular parsing logic + // 3. parse product qty int productQty; try { productQty = Integer.parseInt( @@ -107,38 +135,36 @@ public class SetupDialog { p.sendMessage(Component.text( "invalid product quantity", NamedTextColor.RED)); - open(p, signBlock, plugin); + open(p, signBlock, plugin, newShopName); return; } - Material productMat = plugin - .getSignParser() + Material productMat = plugin.getSignParser() .parseMaterial(productStr); if (productMat == null) { p.sendMessage(Component.text( "invalid product: " + productStr, NamedTextColor.RED)); - open(p, signBlock, plugin); + open(p, signBlock, plugin, newShopName); return; } Trade trade = new Trade(priceMat, priceQty, productMat, productQty); - boolean cosmeticSign = view - .getBoolean("cosmetic_sign"); - PendingActivation activation = new PendingActivation( p.getUniqueId(), signBlock.getLocation(), trade, System.currentTimeMillis(), - cosmeticSign); + true); // cosmeticSign always + // true // 4. finalize plugin.getServer().getScheduler() .runTask(plugin, () -> { plugin.getShopActivationListener() - .finalizeShop(p, activation); + .finalizeShop(p, activation, + newShopName); }); }, ClickCallback.Options.builder().uses(1).build())) .build(), diff --git a/src/main/java/party/cybsec/oyeshops/listener/ChestInteractionListener.java b/src/main/java/party/cybsec/oyeshops/listener/ChestInteractionListener.java index d63f39b..92155f5 100644 --- a/src/main/java/party/cybsec/oyeshops/listener/ChestInteractionListener.java +++ b/src/main/java/party/cybsec/oyeshops/listener/ChestInteractionListener.java @@ -24,7 +24,6 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import party.cybsec.oyeshops.OyeShopsPlugin; -import party.cybsec.oyeshops.gui.ConfirmationGui; import party.cybsec.oyeshops.model.Shop; import party.cybsec.oyeshops.model.Trade; import party.cybsec.oyeshops.permission.PermissionManager; @@ -40,8 +39,11 @@ import java.util.Map; public class ChestInteractionListener implements Listener { private final OyeShopsPlugin plugin; - // track players viewing fake shop inventories - private final Map viewingShop = new HashMap<>(); + // track active shop sessions for players + private final Map activeSessions = new HashMap<>(); + + public record ShopSession(Shop shop, int unitsTraded, Inventory realInventory) { + } public ChestInteractionListener(OyeShopsPlugin plugin) { this.plugin = plugin; @@ -63,8 +65,7 @@ public class ChestInteractionListener implements Listener { return; } - // check if there's a shop sign attached to this container or its double chest - // partner + // check if there's a shop sign attached to this container Shop shop = findShopForContainer(block); if (shop == null) { return; // not a shop container @@ -79,16 +80,18 @@ public class ChestInteractionListener implements Listener { return; } - // check if player is owner (unless spoofing) + // check if player is owner or contributor (unless spoofing) boolean isOwner = shop.getOwner().equals(player.getUniqueId()) && !plugin.getSpoofManager().isSpoofing(player); + boolean isContributor = shop.getContributors().contains(player.getUniqueId()) + && !plugin.getSpoofManager().isSpoofing(player); - if (isOwner) { - // owner interaction - check for owed items and dispense + if (isOwner || isContributor) { + // owner/contributor interaction - check for owed items and dispense if (shop.getOwedAmount() > 0) { withdrawOwedItems(player, shop); } - // let owner open the chest normally - don't cancel event + // let them open the chest normally return; } @@ -173,18 +176,13 @@ public class ChestInteractionListener implements Listener { title = Component.text(shop.getCustomTitle()); } - // get real container inventory (handles double chests correctly) + // get real container inventory Inventory realInventory = getContainerInventory(containerBlock); if (realInventory == null) { return; } - // determine inventory size based on container type - int invSize = realInventory.getSize(); - if (invSize > 54) - invSize = 54; // cap at double chest - if (invSize < 27) - invSize = 27; // minimum single chest + int invSize = Math.min(54, Math.max(27, realInventory.getSize())); // create fake inventory showing only product items Inventory shopInventory = Bukkit.createInventory( @@ -192,17 +190,26 @@ public class ChestInteractionListener implements Listener { invSize, title); - // copy matching items to fake inventory - int shopSlot = 0; + // populate with units (productQuantity per slot) + int totalStock = 0; for (ItemStack item : realInventory.getContents()) { - if (shopSlot >= invSize) - break; if (item != null && trade.matchesProduct(item.getType())) { - shopInventory.setItem(shopSlot++, item.clone()); + totalStock += item.getAmount(); } } - viewingShop.put(player, shop); + int unitsAvailable = totalStock / trade.productQuantity(); + int slotsToFill = Math.min(invSize, unitsAvailable); + + // get representative item with NBT for display + ItemStack displayItem = plugin.getTransactionManager().getRepresentativeItem(realInventory, trade.productItem(), + trade.productQuantity()); + + for (int i = 0; i < slotsToFill; i++) { + shopInventory.setItem(i, displayItem); + } + + activeSessions.put(player, new ShopSession(shop, 0, realInventory)); player.openInventory(shopInventory); // play shop owner's configured disc if set @@ -256,95 +263,145 @@ public class ChestInteractionListener implements Listener { return; } - InventoryHolder holder = event.getInventory().getHolder(); - - // handle shop inventory clicks - if (holder instanceof ShopInventoryHolder shopHolder) { - event.setCancelled(true); - Shop shop = shopHolder.shop(); - - // if clicked in the shop inventory area - if (event.getRawSlot() < event.getInventory().getSize()) { - ItemStack clicked = event.getCurrentItem(); - Trade trade = shop.getTrade(); - if (clicked != null && trade.matchesProduct(clicked.getType())) { - // determine quantity based on click type - int units = 1; - if (event.isShiftClick()) { - // shift-click: calculate max units based on items clicked - units = clicked.getAmount() / trade.productQuantity(); - if (units < 1) - units = 1; - } - - // open confirmation gui - player.closeInventory(); - openConfirmationGui(player, shop, units); - } - } else { - // clicked in player inventory - play negative feedback sound - player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); - } + ShopSession session = activeSessions.get(player); + if (session == null) { return; } - // handle confirmation GUI clicks - if (holder instanceof ConfirmationGui confirmGui) { - event.setCancelled(true); - Shop shop = confirmGui.getShop(); + InventoryHolder holder = event.getInventory().getHolder(); + if (!(holder instanceof ShopInventoryHolder)) { + return; + } - int slot = event.getRawSlot(); + event.setCancelled(true); + int slot = event.getRawSlot(); + if (slot >= event.getInventory().getSize()) { + return; // Clicked player inventory + } - // green pane = confirm (slot 11-15 or specifically slot 11) - if (slot == 11 || slot == 12 || slot == 13) { - player.closeInventory(); - executeTransaction(player, shop, confirmGui.getUnits()); + ItemStack clicked = event.getCurrentItem(); + if (clicked == null || clicked.getType() == Material.AIR) { + return; + } + + Shop shop = session.shop(); + Trade trade = shop.getTrade(); + TransactionManager tm = plugin.getTransactionManager(); + + // Case 1: Clicked a Product (to BUY) + if (trade.matchesProduct(clicked.getType())) { + // 1. Check if player has payment + if (!tm.hasItems(player.getInventory(), trade.priceItem(), trade.priceQuantity())) { + player.sendMessage(Component.text("you don't have enough to pay!", NamedTextColor.RED)); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; } - // red pane = cancel (slot 15 or right side) - else if (slot == 14 || slot == 15 || slot == 16) { - player.closeInventory(); - player.sendMessage(Component.text("purchase cancelled", NamedTextColor.YELLOW)); + + // 2. Check if player has space for product + if (!tm.hasSpace(player.getInventory(), trade.productItem(), trade.productQuantity())) { + player.sendMessage(Component.text("your inventory is full!", NamedTextColor.RED)); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; } + + // 3. Remove product from REAL chest (NBT PRESERVED) + if (tm.hasItems(session.realInventory(), trade.productItem(), trade.productQuantity())) { + ItemStack[] products = tm.removeItemsAndReturn(session.realInventory(), trade.productItem(), + trade.productQuantity()); + + // 4. Give product to Player + player.getInventory().addItem(products); + + // 5. Remove payment from Player (NBT preserved if needed) + tm.removeItems(player.getInventory(), trade.priceItem(), trade.priceQuantity()); + + // Success! + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f); + + // Show "Payment" in GUI slot to allow reversal + ItemStack paymentPlaceholder = new ItemStack(trade.priceItem(), trade.priceQuantity()); + event.getInventory().setItem(slot, paymentPlaceholder); + + activeSessions.put(player, new ShopSession(shop, session.unitsTraded() + 1, session.realInventory())); + } else { + player.sendMessage(Component.text("shop is out of stock!", NamedTextColor.RED)); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + } + } + // Case 2: Clicked a Payment (to REFUND) + else if (clicked.getType() == trade.priceItem()) { + // 1. Check if player has product to return + if (!tm.hasItems(player.getInventory(), trade.productItem(), trade.productQuantity())) { + player.sendMessage(Component.text("you don't have the product to return!", NamedTextColor.RED)); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // 2. Check if chest has space for product + if (!tm.hasSpace(session.realInventory(), trade.productItem(), trade.productQuantity())) { + player.sendMessage(Component.text("the shop chest is full! cannot return!", NamedTextColor.RED)); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + return; + } + + // 3. Return product from Player -> Chest (NBT PRESERVED) + ItemStack[] products = tm.removeItemsAndReturn(player.getInventory(), trade.productItem(), + trade.productQuantity()); + session.realInventory().addItem(products); + + // 4. Return payment to Player + tm.removeItems(session.realInventory(), trade.priceItem(), trade.priceQuantity()); // Not used currently but + // for completeness + player.getInventory().addItem(tm.createItemStacks(trade.priceItem(), trade.priceQuantity())); + + // Success refund! + player.playSound(player.getLocation(), Sound.BLOCK_CHEST_CLOSE, 1.0f, 1.2f); + + // Restore Product to GUI slot (with NBT) + ItemStack productPreview = tm.getRepresentativeItem(session.realInventory(), trade.productItem(), + trade.productQuantity()); + event.getInventory().setItem(slot, productPreview); + + activeSessions.put(player, new ShopSession(shop, session.unitsTraded() - 1, session.realInventory())); } } @EventHandler public void onInventoryClose(InventoryCloseEvent event) { - if (event.getPlayer() instanceof Player player) { - // stop disc when closing shop - Shop shop = viewingShop.remove(player); - if (shop != null) { - String discName = shop.getDisc(); - if (discName != null && !discName.isEmpty()) { - stopDisc(player, discName); - } + if (!(event.getPlayer() instanceof Player player)) { + return; + } + + ShopSession session = activeSessions.remove(player); + if (session == null) { + return; + } + + Shop shop = session.shop(); + + // Finalize transaction in database if any units were traded + if (session.unitsTraded() > 0) { + try { + int totalPaid = session.unitsTraded() * shop.getTrade().priceQuantity(); + plugin.getShopRepository().updateOwedAmount(shop.getId(), shop.getOwedAmount() + totalPaid); + shop.setOwedAmount(shop.getOwedAmount() + totalPaid); + + plugin.getTransactionRepository().recordTransaction( + shop.getId(), + player.getUniqueId(), + session.unitsTraded()); + + player.sendMessage(Component.text("transaction finalized!", NamedTextColor.GREEN)); + } catch (SQLException e) { + e.printStackTrace(); + player.sendMessage(Component.text("error saving transaction to database", NamedTextColor.RED)); } } - } - /** - * open confirmation GUI - */ - private void openConfirmationGui(Player player, Shop shop, int units) { - ConfirmationGui gui = new ConfirmationGui(shop, units); - player.openInventory(gui.getInventory()); - } - - /** - * execute the transaction - */ - private void executeTransaction(Player player, Shop shop, int units) { - TransactionManager.Result result = plugin.getTransactionManager().execute(player, shop, units); - - switch (result) { - case SUCCESS -> player.sendMessage(Component.text("purchase complete!", NamedTextColor.GREEN)); - case INSUFFICIENT_FUNDS -> - player.sendMessage(Component.text("you don't have enough items to pay", NamedTextColor.RED)); - case INSUFFICIENT_STOCK -> player.sendMessage(Component.text("shop is out of stock", NamedTextColor.RED)); - case INVENTORY_FULL -> player.sendMessage(Component.text("your inventory is full", NamedTextColor.RED)); - case DATABASE_ERROR -> - player.sendMessage(Component.text("transaction failed - database error", NamedTextColor.RED)); - case SHOP_DISABLED -> player.sendMessage(Component.text("this shop is disabled", NamedTextColor.RED)); + // stop disc + String discName = shop.getDisc(); + if (discName != null && !discName.isEmpty()) { + stopDisc(player, discName); } } diff --git a/src/main/java/party/cybsec/oyeshops/listener/LoginListener.java b/src/main/java/party/cybsec/oyeshops/listener/LoginListener.java index 19cadb2..9114fc8 100644 --- a/src/main/java/party/cybsec/oyeshops/listener/LoginListener.java +++ b/src/main/java/party/cybsec/oyeshops/listener/LoginListener.java @@ -39,28 +39,21 @@ public class LoginListener implements Listener { return; } - plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - // find all shops owned by player - List lowStockShops = new ArrayList<>(); - for (Shop shop : plugin.getShopRegistry().getAllShops()) { - if (shop.getOwner().equals(player.getUniqueId())) { - if (isLowStock(shop)) { - lowStockShops.add(shop); - } + // find all shops owned by player and check stock on main thread + List lowStockShops = new ArrayList<>(); + for (Shop shop : plugin.getShopRegistry().getAllShops()) { + if (shop.getOwner().equals(player.getUniqueId())) { + if (isLowStock(shop)) { + lowStockShops.add(shop); } } + } - if (!lowStockShops.isEmpty()) { - final int count = lowStockShops.size(); - plugin.getServer().getScheduler().runTask(plugin, () -> { - if (player.isOnline()) { - player.sendMessage( - Component.text("notification: " + count + " of your shops are low on stock.", - NamedTextColor.YELLOW)); - } - }); - } - }); + if (!lowStockShops.isEmpty()) { + player.sendMessage( + Component.text("notification: " + lowStockShops.size() + " of your shops are low on stock.", + NamedTextColor.YELLOW)); + } } private boolean isLowStock(Shop shop) { diff --git a/src/main/java/party/cybsec/oyeshops/listener/ShopActivationListener.java b/src/main/java/party/cybsec/oyeshops/listener/ShopActivationListener.java index 86f81ac..7f5e0ef 100644 --- a/src/main/java/party/cybsec/oyeshops/listener/ShopActivationListener.java +++ b/src/main/java/party/cybsec/oyeshops/listener/ShopActivationListener.java @@ -92,9 +92,9 @@ public class ShopActivationListener implements Listener { } // 2. session-based ownership check - if (!PermissionManager.isAdmin(player)) { + if (plugin.getConfigManager().isRequirePlacement() && !PermissionManager.isAdmin(player)) { UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation()); - if (sessionOwner == null) { + if (sessionOwner == null || !sessionOwner.equals(player.getUniqueId())) { // block was placed before the server started or by no one player.sendMessage(Component.text("you can only create shops on containers you placed this session", NamedTextColor.RED)); @@ -127,7 +127,7 @@ public class ShopActivationListener implements Listener { // clear sign text to avoid "setup" staying on the sign event.line(0, Component.text("")); - SetupDialog.open(player, block, plugin); + SetupDialog.open(player, block, plugin, "shop-" + (plugin.getShopRegistry().getAllShops().size() + 1)); return; } @@ -158,6 +158,16 @@ public class ShopActivationListener implements Listener { private void sendConfirmationMessage(Player player, Trade trade) { String priceText = trade.priceQuantity() + " " + formatMaterial(trade.priceItem()); String productText = trade.productQuantity() + " " + formatMaterial(trade.productItem()); + String shopName = "shop-" + (plugin.getShopRegistry().getAllShops().size() + 1); + + // check uniqueness for default name (player scoped) + if (plugin.getShopRegistry().getShopByOwnerAndName(player.getUniqueId(), shopName) != null) { + int i = 2; + while (plugin.getShopRegistry().getShopByOwnerAndName(player.getUniqueId(), shopName + "-" + i) != null) { + i++; + } + shopName = shopName + "-" + i; + } // clear display: buyer pays vs buyer gets player.sendMessage(Component.text("shop detected!", NamedTextColor.GOLD)); @@ -170,17 +180,17 @@ public class ShopActivationListener implements Listener { .append(Component.text("[accept]", NamedTextColor.GREEN, TextDecoration.BOLD) .hoverEvent(HoverEvent.showText( Component.text("create shop exactly as shown above", NamedTextColor.WHITE))) - .clickEvent(ClickEvent.runCommand("/oyes _activate accept"))) + .clickEvent(ClickEvent.runCommand("/oyes _activate accept " + shopName))) .append(Component.text(" ")) .append(Component.text("[invert]", NamedTextColor.GOLD, TextDecoration.BOLD) .hoverEvent(HoverEvent.showText(Component.text( "swap: buyer pays " + productText + " and gets " + priceText, NamedTextColor.WHITE))) - .clickEvent(ClickEvent.runCommand("/oyes _activate invert"))) + .clickEvent(ClickEvent.runCommand("/oyes _activate invert " + shopName))) .append(Component.text(" ")) .append(Component.text("[cancel]", NamedTextColor.RED, TextDecoration.BOLD) .hoverEvent(HoverEvent.showText( Component.text("ignore sign. no shop created.", NamedTextColor.WHITE))) - .clickEvent(ClickEvent.runCommand("/oyes _activate cancel"))); + .clickEvent(ClickEvent.runCommand("/oyes _activate cancel " + shopName))); player.sendMessage(buttons); } @@ -189,7 +199,7 @@ public class ShopActivationListener implements Listener { * complete the shop creation process * called from AdminCommands when user clicks [accept] or [invert] */ - public void finalizeShop(Player player, PendingActivation activation) { + public void finalizeShop(Player player, PendingActivation activation, String shopName) { Location signLocation = activation.location(); Block block = signLocation.getBlock(); @@ -201,8 +211,9 @@ public class ShopActivationListener implements Listener { Trade trade = activation.trade(); long createdAt = System.currentTimeMillis(); - Shop shop = new Shop(-1, signLocation, player.getUniqueId(), trade, 0, true, createdAt, null, - activation.cosmeticSign(), null); + // default cosmeticSign to true as requested + Shop shop = new Shop(-1, shopName, signLocation, player.getUniqueId(), new ArrayList<>(), trade, 0, true, + createdAt, null, true, null); plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try { @@ -210,22 +221,24 @@ public class ShopActivationListener implements Listener { plugin.getShopRepository().deleteShopByLocation(signLocation); int shopId = plugin.getShopRepository().createShop(shop); - plugin.getLogger().info("DEBUG: created shop id " + shopId + " at " + signLocation); + plugin.getLogger().info("DEBUG: created shop id " + shopId + " (" + shopName + ") at " + signLocation); plugin.getServer().getScheduler().runTask(plugin, () -> { // re-verify sign on main thread - if (!(signLocation.getBlock().getState() instanceof Sign finalSign)) { + if (!(signLocation.getBlock().getState() instanceof Sign)) { plugin.getLogger().info("DEBUG: sign missing at " + signLocation); return; } - Shop registeredShop = new Shop(shopId, signLocation, player.getUniqueId(), trade, 0, true, - createdAt, null, activation.cosmeticSign(), null); + Shop registeredShop = new Shop(shopId, shopName, signLocation, player.getUniqueId(), + new ArrayList<>(), trade, 0, true, createdAt, null, true, null); plugin.getShopRegistry().register(registeredShop); plugin.getLogger().info("DEBUG: registered shop " + shopId + " in registry"); - rewriteSignLines(finalSign, registeredShop); - player.sendMessage(Component.text("shop #" + shopId + " initialized!", NamedTextColor.GREEN)); + // rewriteSignLines(finalSign, registeredShop); // REMOVED: Sign should just be + // left alone + player.sendMessage(Component.text("shop '" + shopName + "' (#" + shopId + ") initialized!", + NamedTextColor.GREEN)); }); } catch (SQLException e) { plugin.getServer().getScheduler().runTask(plugin, () -> { @@ -237,20 +250,7 @@ public class ShopActivationListener implements Listener { } public void rewriteSignLines(Sign sign, Shop shop) { - if (shop.isCosmeticSign()) { - return; - } - - Trade trade = shop.getTrade(); - String pricePart = trade.priceQuantity() + " " + abbreviateMaterial(trade.priceItem()); - - String productPart = trade.productQuantity() + " " + abbreviateMaterial(trade.productItem()); - - sign.line(0, Component.text(pricePart)); - sign.line(1, Component.text("for.")); - sign.line(2, Component.text(productPart)); - sign.line(3, Component.text("")); - sign.update(); + // REMOVED: Sign should just be left alone as requested } /** @@ -355,15 +355,6 @@ public class ShopActivationListener implements Listener { return bestQuantity; } - private String abbreviateMaterial(Material material) { - String name = material.name().toLowerCase().replace("_", " "); - name = name.replace("diamond", "dia").replace("emerald", "em").replace("netherite", "neth") - .replace("ingot", "").replace("block", "blk").replace("pickaxe", "pick") - .replace("chestplate", "chest").replace("leggings", "legs"); - name = name.trim(); - return name.length() > 14 ? name.substring(0, 14) : name; - } - private Inventory getContainerInventory(Block block) { if (block.getState() instanceof Chest chest) return chest.getInventory(); diff --git a/src/main/java/party/cybsec/oyeshops/model/Shop.java b/src/main/java/party/cybsec/oyeshops/model/Shop.java index 504af76..9b2587a 100644 --- a/src/main/java/party/cybsec/oyeshops/model/Shop.java +++ b/src/main/java/party/cybsec/oyeshops/model/Shop.java @@ -2,6 +2,7 @@ package party.cybsec.oyeshops.model; import org.bukkit.Location; +import java.util.List; import java.util.UUID; /** @@ -9,8 +10,10 @@ import java.util.UUID; */ public class Shop { private final int id; + private String name; private final Location signLocation; private final UUID owner; + private final List contributors; private final Trade trade; private int owedAmount; private boolean enabled; @@ -19,11 +22,13 @@ public class Shop { private boolean cosmeticSign; private String disc; - public Shop(int id, Location signLocation, UUID owner, Trade trade, int owedAmount, boolean enabled, - long createdAt, String customTitle, boolean cosmeticSign, String disc) { + public Shop(int id, String name, Location signLocation, UUID owner, List contributors, Trade trade, + int owedAmount, boolean enabled, long createdAt, String customTitle, boolean cosmeticSign, String disc) { this.id = id; + this.name = name; this.signLocation = signLocation; this.owner = owner; + this.contributors = contributors; this.trade = trade; this.owedAmount = owedAmount; this.enabled = enabled; @@ -33,6 +38,18 @@ public class Shop { this.disc = disc; } + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getContributors() { + return contributors; + } + public int getId() { return id; } @@ -97,8 +114,6 @@ public class Shop { * get chest location from sign location */ public Location getChestLocation() { - // chest is attached to the sign - // we'll determine this from the sign's attached block face - return signLocation.clone(); // placeholder - will be properly implemented + return signLocation.clone(); } } diff --git a/src/main/java/party/cybsec/oyeshops/registry/ShopRegistry.java b/src/main/java/party/cybsec/oyeshops/registry/ShopRegistry.java index 8ac5e47..47489b2 100644 --- a/src/main/java/party/cybsec/oyeshops/registry/ShopRegistry.java +++ b/src/main/java/party/cybsec/oyeshops/registry/ShopRegistry.java @@ -75,6 +75,34 @@ public class ShopRegistry { return shopsById.get(shopId); } + /** + * get shop by name (first match) + */ + public Shop getShopByName(String name) { + if (name == null) + return null; + for (Shop shop : shopsById.values()) { + if (name.equalsIgnoreCase(shop.getName())) { + return shop; + } + } + return null; + } + + /** + * get shop by owner and name + */ + public Shop getShopByOwnerAndName(java.util.UUID owner, String name) { + if (name == null || owner == null) + return null; + for (Shop shop : shopsById.values()) { + if (owner.equals(shop.getOwner()) && name.equalsIgnoreCase(shop.getName())) { + return shop; + } + } + return null; + } + /** * get all shops */ diff --git a/src/main/java/party/cybsec/oyeshops/transaction/TransactionManager.java b/src/main/java/party/cybsec/oyeshops/transaction/TransactionManager.java index ac244d0..862b818 100644 --- a/src/main/java/party/cybsec/oyeshops/transaction/TransactionManager.java +++ b/src/main/java/party/cybsec/oyeshops/transaction/TransactionManager.java @@ -33,7 +33,41 @@ public class TransactionManager { INSUFFICIENT_STOCK, INVENTORY_FULL, DATABASE_ERROR, - SHOP_DISABLED + SHOP_DISABLED, + INVALID_AMOUNT + } + + /** + * check if inventory has enough space for a material and amount + */ + public boolean hasSpace(Inventory inventory, Material material, int amount) { + int space = 0; + int maxStack = material.getMaxStackSize(); + + for (ItemStack item : inventory.getStorageContents()) { + if (item == null || item.getType() == Material.AIR) { + space += maxStack; + } else if (item.getType() == material) { + space += (maxStack - item.getAmount()); + } + if (space >= amount) + return true; + } + return space >= amount; + } + + /** + * transfers items from one inventory to another atomically + * returns true if successful + */ + public boolean transferItems(Inventory from, Inventory to, Material material, int amount) { + if (!hasItems(from, material, amount) || !hasSpace(to, material, amount)) { + return false; + } + + ItemStack[] items = removeItemsAndReturn(from, material, amount); + to.addItem(items); + return true; } /** @@ -80,15 +114,15 @@ public class TransactionManager { } try { - // remove price items from buyer - removeItems(buyer.getInventory(), trade.priceItem(), totalPrice); + // remove price items from buyer (NBT preserved if needed, though usually + // currency) + removeItemsAndReturn(buyer.getInventory(), trade.priceItem(), totalPrice); - // remove product items from chest - removeItems(chestInventory, trade.productItem(), totalProduct); + // remove product items from chest (NBT CRITICAL HERE) + ItemStack[] productItems = removeItemsAndReturn(chestInventory, trade.productItem(), totalProduct); // add product items to buyer - HashMap overflow = buyer.getInventory().addItem( - createItemStacks(trade.productItem(), totalProduct)); + HashMap overflow = buyer.getInventory().addItem(productItems); if (!overflow.isEmpty()) { // rollback @@ -130,7 +164,7 @@ public class TransactionManager { /** * check if inventory has required items */ - private boolean hasItems(Inventory inventory, Material material, int amount) { + public boolean hasItems(Inventory inventory, Material material, int amount) { int count = 0; for (ItemStack item : inventory.getContents()) { if (item != null && item.getType() == material) { @@ -144,15 +178,20 @@ public class TransactionManager { } /** - * remove items from inventory + * remove items from inventory and return them (preserving NBT) */ - private void removeItems(Inventory inventory, Material material, int amount) { + public ItemStack[] removeItemsAndReturn(Inventory inventory, Material material, int amount) { int remaining = amount; + java.util.List removed = new java.util.ArrayList<>(); for (int i = 0; i < inventory.getSize() && remaining > 0; i++) { ItemStack item = inventory.getItem(i); if (item != null && item.getType() == material) { int toRemove = Math.min(item.getAmount(), remaining); + ItemStack clone = item.clone(); + clone.setAmount(toRemove); + removed.add(clone); + if (toRemove == item.getAmount()) { inventory.setItem(i, null); } else { @@ -161,12 +200,32 @@ public class TransactionManager { remaining -= toRemove; } } + return removed.toArray(new ItemStack[0]); } /** - * create item stacks for given total amount + * remove items from inventory */ - private ItemStack[] createItemStacks(Material material, int totalAmount) { + public void removeItems(Inventory inventory, Material material, int amount) { + removeItemsAndReturn(inventory, material, amount); + } + + /** + * get a sample item from inventory for display (preserving NBT) + */ + public ItemStack getRepresentativeItem(Inventory inventory, Material material, int amount) { + for (ItemStack item : inventory.getContents()) { + if (item != null && item.getType() == material) { + ItemStack preview = item.clone(); + preview.setAmount(amount); + return preview; + } + } + // fallback to blank material if not found + return new ItemStack(material, amount); + } + + public ItemStack[] createItemStacks(Material material, int totalAmount) { int maxStack = material.getMaxStackSize(); int fullStacks = totalAmount / maxStack; int remainder = totalAmount % maxStack; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e172fbe..8ea3590 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -5,6 +5,9 @@ transactions: auto-prune: false max-per-shop: 100 +# whether to require players to have placed the container themselves this session +require-placement: true + # custom material aliases (add your own shortcuts) aliases: # example: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4a4ad51..ba54ac8 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: oyeShops -version: 1.3.0 +version: 1.3.1 main: party.cybsec.oyeshops.OyeShopsPlugin api-version: '1.21' description: deterministic item-for-item chest barter @@ -9,7 +9,7 @@ website: https://party.cybsec commands: oyeshops: description: oyeShops commands - usage: /oyeshops + usage: /oyeshops aliases: [oyes, oshop] permissions: