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 0000000..7185bf0
Binary files /dev/null and b/oyeOwner/target/classes/party/cybsec/CoreProtectHook.class differ
diff --git a/oyeOwner/target/classes/party/cybsec/OyeOwner.class b/oyeOwner/target/classes/party/cybsec/OyeOwner.class
new file mode 100644
index 0000000..d23a755
Binary files /dev/null and b/oyeOwner/target/classes/party/cybsec/OyeOwner.class differ
diff --git a/oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class b/oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class
new file mode 100644
index 0000000..37bd04a
Binary files /dev/null and b/oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class differ
diff --git a/oyeOwner/target/classes/party/cybsec/command/WhoCommand.class b/oyeOwner/target/classes/party/cybsec/command/WhoCommand.class
new file mode 100644
index 0000000..71db6d7
Binary files /dev/null and b/oyeOwner/target/classes/party/cybsec/command/WhoCommand.class differ
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: