1 Commits

Author SHA1 Message Date
58b7d44d8f v1.3.1: NBT preservation, GUI previews, and global placement toggle 2026-02-08 10:59:02 -05:00
31 changed files with 1204 additions and 443 deletions

View File

@@ -3,7 +3,7 @@ plugins {
} }
group = "party.cybsec" group = "party.cybsec"
version = "1.3.0" version = "1.3.1"
description = "deterministic item-for-item chest barter" description = "deterministic item-for-item chest barter"
repositories { repositories {

View File

@@ -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
<dependency>
<groupId>party.cybsec</groupId>
<artifactId>oyeOwner</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
```
## 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.

73
oyeOwner/pom.xml Normal file
View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>party.cybsec</groupId>
<artifactId>oyeOwner</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>oyeOwner</name>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>playpro</id>
<url>https://maven.playpro.com</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.coreprotect</groupId>
<artifactId>coreprotect</artifactId>
<version>23.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<String[]> 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<String> getBlockOwnerAsync(Block block) {
return CompletableFuture.supplyAsync(() -> getBlockOwner(block));
}
}

View File

@@ -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;
}
}

View File

@@ -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: /<command>
permissions:
oyeowner.use:
description: Allows player to use the /who command
default: op

Binary file not shown.

Binary file not shown.

View File

@@ -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: /<command>
permissions:
oyeowner.use:
description: Allows player to use the /who command
default: op

View File

@@ -0,0 +1,3 @@
artifactId=oyeOwner
groupId=party.cybsec
version=1.0-SNAPSHOT

View File

@@ -0,0 +1,4 @@
party/cybsec/command/WhoCommand.class
party/cybsec/OyeOwner.class
party/cybsec/CoreProtectHook.class
party/cybsec/OyeOwnerAPI.class

View File

@@ -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

View File

@@ -48,13 +48,15 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
case "notify" -> handleNotifyToggle(sender); case "notify" -> handleNotifyToggle(sender);
case "info" -> handleInfo(sender); case "info" -> handleInfo(sender);
case "help" -> handleHelp(sender); case "help" -> handleHelp(sender);
case "setup" -> handleSetup(sender); case "setup" -> handleSetup(sender, args);
case "config" -> handleConfig(sender); case "config" -> handleConfig(sender, args);
case "reload" -> handleReload(sender); case "reload" -> handleReload(sender);
case "inspect", "i" -> handleInspect(sender); case "inspect", "i" -> handleInspect(sender);
case "spoof", "s" -> handleSpoof(sender); case "spoof", "s" -> handleSpoof(sender);
case "unregister", "delete", "remove" -> handleUnregister(sender, args); case "unregister", "delete", "remove" -> handleUnregister(sender, args);
case "tpshop" -> handleTpShop(sender, args);
case "_activate" -> handleActivate(sender, args); case "_activate" -> handleActivate(sender, args);
case "toggleplacement", "toggle-placement" -> handleTogglePlacement(sender);
default -> handleHelp(sender); default -> handleHelp(sender);
} }
@@ -93,6 +95,10 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
.append(Component.text(" - disable a shop", NamedTextColor.GRAY))); .append(Component.text(" - disable a shop", NamedTextColor.GRAY)));
sender.sendMessage(Component.text("/oyeshops unregister <id>", NamedTextColor.YELLOW) sender.sendMessage(Component.text("/oyeshops unregister <id>", NamedTextColor.YELLOW)
.append(Component.text(" - delete a shop", NamedTextColor.GRAY))); .append(Component.text(" - delete a shop", NamedTextColor.GRAY)));
sender.sendMessage(Component.text("/oyeshops tpshop <owner> <shop>", 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) { private void handleActivate(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) if (!(sender instanceof Player player))
return; return;
if (args.length < 2) if (args.length < 3) {
player.sendMessage(Component.text("usage: /oyes _activate <action> <name>", NamedTextColor.RED));
return; return;
}
String action = args[1].toLowerCase(); String action = args[1].toLowerCase();
String name = args[2];
PendingActivation activation = plugin.getActivationManager().getAndRemove(player.getUniqueId()); PendingActivation activation = plugin.getActivationManager().getAndRemove(player.getUniqueId());
if (activation == null) { if (activation == null) {
@@ -235,10 +244,10 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
switch (action) { switch (action) {
case "accept" -> { case "accept" -> {
finalizeShop(player, activation); finalizeShop(player, activation, name);
} }
case "invert" -> { case "invert" -> {
finalizeShop(player, activation.invert()); finalizeShop(player, activation.invert(), name);
} }
case "cancel" -> { case "cancel" -> {
player.sendMessage( player.sendMessage(
@@ -247,8 +256,8 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
} }
} }
private void finalizeShop(Player player, PendingActivation activation) { private void finalizeShop(Player player, PendingActivation activation, String shopName) {
plugin.getShopActivationListener().finalizeShop(player, activation); plugin.getShopActivationListener().finalizeShop(player, activation, shopName);
} }
private void handleEnable(CommandSender sender, String[] args) { private void handleEnable(CommandSender sender, String[] args) {
@@ -275,7 +284,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
e.printStackTrace(); e.printStackTrace();
} }
} else { } else {
sender.sendMessage(Component.text("usage: /oyeshops enable <id>", NamedTextColor.RED)); sender.sendMessage(Component.text("usage: /oyeshops enable <id/name>", NamedTextColor.RED));
} }
return; return;
} }
@@ -285,18 +294,25 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
return; return;
} }
try { String target = args[1];
int shopId = Integer.parseInt(args[1]); Shop shop = plugin.getShopRegistry().getShopByName(target);
Shop shop = plugin.getShopRegistry().getShopById(shopId);
if (shop == null) { if (shop == null) {
sender.sendMessage(Component.text("shop not found: " + shopId, NamedTextColor.RED)); 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; return;
} }
try {
shop.setEnabled(true); shop.setEnabled(true);
plugin.getShopRepository().setEnabled(shopId, true); plugin.getShopRepository().setEnabled(shop.getId(), true);
sender.sendMessage(Component.text("shop #" + shopId + " enabled", NamedTextColor.GREEN)); sender.sendMessage(Component.text("shop '" + shop.getName() + "' enabled", NamedTextColor.GREEN));
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("invalid shop id", NamedTextColor.RED));
} catch (SQLException e) { } catch (SQLException e) {
sender.sendMessage(Component.text("database error", NamedTextColor.RED)); sender.sendMessage(Component.text("database error", NamedTextColor.RED));
e.printStackTrace(); e.printStackTrace();
@@ -315,7 +331,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
e.printStackTrace(); e.printStackTrace();
} }
} else { } else {
sender.sendMessage(Component.text("usage: /oyeshops disable <id>", NamedTextColor.RED)); sender.sendMessage(Component.text("usage: /oyeshops disable <id/name>", NamedTextColor.RED));
} }
return; return;
} }
@@ -325,18 +341,25 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
return; return;
} }
try { String target = args[1];
int shopId = Integer.parseInt(args[1]); Shop shop = plugin.getShopRegistry().getShopByName(target);
Shop shop = plugin.getShopRegistry().getShopById(shopId);
if (shop == null) { if (shop == null) {
sender.sendMessage(Component.text("shop not found: " + shopId, NamedTextColor.RED)); 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; return;
} }
try {
shop.setEnabled(false); shop.setEnabled(false);
plugin.getShopRepository().setEnabled(shopId, false); plugin.getShopRepository().setEnabled(shop.getId(), false);
sender.sendMessage(Component.text("shop #" + shopId + " disabled", NamedTextColor.YELLOW)); sender.sendMessage(Component.text("shop '" + shop.getName() + "' disabled", NamedTextColor.YELLOW));
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("invalid shop id", NamedTextColor.RED));
} catch (SQLException e) { } catch (SQLException e) {
sender.sendMessage(Component.text("database error", NamedTextColor.RED)); sender.sendMessage(Component.text("database error", NamedTextColor.RED));
e.printStackTrace(); e.printStackTrace();
@@ -350,22 +373,29 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
} }
if (args.length < 2) { if (args.length < 2) {
sender.sendMessage(Component.text("usage: /oyeshops unregister <id>", NamedTextColor.RED)); sender.sendMessage(Component.text("usage: /oyeshops unregister <id/name>", 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; return;
} }
try { 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.getShopRegistry().unregister(shop);
plugin.getShopRepository().deleteShop(shopId); plugin.getShopRepository().deleteShop(shop.getId());
sender.sendMessage(Component.text("shop #" + shopId + " deleted", NamedTextColor.RED)); sender.sendMessage(Component.text("shop '" + shop.getName() + "' deleted", NamedTextColor.RED));
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("invalid shop id", NamedTextColor.RED));
} catch (SQLException e) { } catch (SQLException e) {
sender.sendMessage(Component.text("database error", NamedTextColor.RED)); sender.sendMessage(Component.text("database error", NamedTextColor.RED));
e.printStackTrace(); e.printStackTrace();
@@ -379,7 +409,8 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
List<String> subCommands = new ArrayList<>( List<String> subCommands = new ArrayList<>(
List.of("help", "setup", "config", "on", "off", "toggle", "notify", "info", "enable", "disable")); List.of("help", "setup", "config", "on", "off", "toggle", "notify", "info", "enable", "disable"));
if (PermissionManager.isAdmin(sender)) { 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(); String partial = args[0].toLowerCase();
for (String sub : subCommands) { for (String sub : subCommands) {
@@ -388,20 +419,61 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
} }
} else if (args.length == 2) { } else if (args.length == 2) {
String subCommand = args[0].toLowerCase(); String subCommand = args[0].toLowerCase();
if (PermissionManager.isAdmin(sender) && (subCommand.equals("enable") || subCommand.equals("disable") if (subCommand.equals("config") && sender instanceof Player player) {
|| subCommand.equals("unregister"))) { String partial = args[1].toLowerCase();
String partial = args[1];
for (Shop shop : plugin.getShopRegistry().getAllShops()) { 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()); String id = String.valueOf(shop.getId());
if (id.startsWith(partial)) if (id.startsWith(partial))
completions.add(id); completions.add(id);
} }
} else if (subCommand.equals("tpshop") && PermissionManager.isAdmin(sender)) {
String partial = args[1].toLowerCase();
java.util.Set<String> 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; return completions;
} }
private void handleSetup(CommandSender sender) { private void handleSetup(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("players only", NamedTextColor.RED)); sender.sendMessage(Component.text("players only", NamedTextColor.RED));
return; return;
@@ -412,6 +484,25 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
return; return;
} }
if (args.length < 2) {
player.sendMessage(Component.text("usage: /oyes setup <name>", 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); Block block = player.getTargetBlockExact(5);
if (block == null || !(block.getBlockData() instanceof WallSign wallSign)) { 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)); 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; return;
} }
if (!PermissionManager.isAdmin(player)) { if (plugin.getConfigManager().isRequirePlacement() && !PermissionManager.isAdmin(player)) {
java.util.UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation()); java.util.UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation());
if (sessionOwner == null || !sessionOwner.equals(player.getUniqueId())) { if (sessionOwner == null || !sessionOwner.equals(player.getUniqueId())) {
player.sendMessage( player.sendMessage(
@@ -437,39 +528,110 @@ 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)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("players only", NamedTextColor.RED)); sender.sendMessage(Component.text("players only", NamedTextColor.RED));
return; return;
} }
Shop shop = null;
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); Block block = player.getTargetBlockExact(5);
if (block == null) { if (block == null) {
player.sendMessage(Component.text("you must look at a shop sign or its container", NamedTextColor.RED)); player.sendMessage(
Component.text("you must look at a shop sign or its container, or use /oyes config <name>",
NamedTextColor.RED));
return; return;
} }
Shop shop = null;
if (block.getBlockData() instanceof WallSign) { if (block.getBlockData() instanceof WallSign) {
shop = plugin.getShopRegistry().getShop(block.getLocation()); shop = plugin.getShopRegistry().getShop(block.getLocation());
} else if (party.cybsec.oyeshops.listener.ShopActivationListener.isContainer(block.getType())) { } 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); shop = plugin.getShopRegistry().getShopByContainer(block);
} }
}
if (shop == null) { if (shop == null) {
player.sendMessage(Component.text("that is not a part of any shop", NamedTextColor.RED)); player.sendMessage(Component.text("that is not a part of any shop", NamedTextColor.RED));
return; return;
} }
if (!shop.getOwner().equals(player.getUniqueId()) && !PermissionManager.isAdmin(player)) { if (!shop.getOwner().equals(player.getUniqueId()) && !shop.getContributors().contains(player.getUniqueId())
player.sendMessage(Component.text("you do not own this shop", NamedTextColor.RED)); && !PermissionManager.isAdmin(player)) {
player.sendMessage(Component.text("you do not own or contribute to this shop", NamedTextColor.RED));
return; return;
} }
ConfigDialog.open(player, shop, plugin); 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 <ownerName> <shopName>", 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));
}
} }

View File

@@ -37,4 +37,13 @@ public class ConfigManager {
public int getMaxTransactionsPerShop() { public int getMaxTransactionsPerShop() {
return config.getInt("transactions.max-per-shop", 100); 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();
}
} }

View File

@@ -109,10 +109,18 @@ public class DatabaseManager {
} }
try { try {
stmt.execute("alter table shops add column disc text"); stmt.execute("alter table shops add column name text");
} catch (SQLException ignored) { } 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 // indexes
stmt.execute(""" stmt.execute("""
create index if not exists idx_shop_location create index if not exists idx_shop_location

View File

@@ -30,8 +30,8 @@ public class ShopRepository {
insert into shops (world_uuid, sign_x, sign_y, sign_z, owner_uuid, insert into shops (world_uuid, sign_x, sign_y, sign_z, owner_uuid,
price_item, price_quantity, product_item, product_quantity, price_item, price_quantity, product_item, product_quantity,
owed_amount, enabled, created_at, custom_title, owed_amount, enabled, created_at, custom_title,
cosmetic_sign, disc) cosmetic_sign, disc, name, contributors)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql, try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql,
@@ -52,6 +52,8 @@ public class ShopRepository {
stmt.setString(13, shop.getCustomTitle()); stmt.setString(13, shop.getCustomTitle());
stmt.setBoolean(14, shop.isCosmeticSign()); stmt.setBoolean(14, shop.isCosmeticSign());
stmt.setString(15, shop.getDisc()); stmt.setString(15, shop.getDisc());
stmt.setString(16, shop.getName());
stmt.setString(17, serializeContributors(shop.getContributors()));
stmt.executeUpdate(); stmt.executeUpdate();
@@ -175,14 +177,16 @@ public class ShopRepository {
*/ */
public void updateShopConfig(Shop shop) throws SQLException { public void updateShopConfig(Shop shop) throws SQLException {
String sql = """ String sql = """
update shops set custom_title = ?, disc = ? update shops set custom_title = ?, disc = ?, name = ?, contributors = ?
where shop_id = ? where shop_id = ?
"""; """;
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) { try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
stmt.setString(1, shop.getCustomTitle()); stmt.setString(1, shop.getCustomTitle());
stmt.setString(2, shop.getDisc()); 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(); stmt.executeUpdate();
} }
} }
@@ -251,16 +255,47 @@ public class ShopRepository {
return shops; return shops;
} }
/**
* serialize list of UUIDs to comma-separated string
*/
private String serializeContributors(List<UUID> contributors) {
if (contributors == null || contributors.isEmpty())
return "";
List<String> 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<UUID> deserializeContributors(String data) {
List<UUID> 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 * convert result set to shop object
*/ */
private Shop shopFromResultSet(ResultSet rs) throws SQLException { private Shop shopFromResultSet(ResultSet rs) throws SQLException {
int id = rs.getInt("shop_id"); int id = rs.getInt("shop_id");
String name = rs.getString("name");
UUID worldUuid = UUID.fromString(rs.getString("world_uuid")); UUID worldUuid = UUID.fromString(rs.getString("world_uuid"));
int x = rs.getInt("sign_x"); int x = rs.getInt("sign_x");
int y = rs.getInt("sign_y"); int y = rs.getInt("sign_y");
int z = rs.getInt("sign_z"); int z = rs.getInt("sign_z");
UUID ownerUuid = UUID.fromString(rs.getString("owner_uuid")); UUID ownerUuid = UUID.fromString(rs.getString("owner_uuid"));
List<UUID> contributors = deserializeContributors(rs.getString("contributors"));
Material priceItem = Material.valueOf(rs.getString("price_item")); Material priceItem = Material.valueOf(rs.getString("price_item"));
int priceQty = rs.getInt("price_quantity"); int priceQty = rs.getInt("price_quantity");
Material productItem = Material.valueOf(rs.getString("product_item")); Material productItem = Material.valueOf(rs.getString("product_item"));
@@ -280,7 +315,7 @@ public class ShopRepository {
Location location = new Location(world, x, y, z); Location location = new Location(world, x, y, z);
Trade trade = new Trade(priceItem, priceQty, productItem, productQty); 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); customTitle, cosmeticSign, disc);
} }

View File

@@ -15,8 +15,10 @@ import party.cybsec.oyeshops.OyeShopsPlugin;
import party.cybsec.oyeshops.model.Shop; import party.cybsec.oyeshops.model.Shop;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
/** /**
* config dialog for shop settings * config dialog for shop settings
@@ -32,17 +34,34 @@ public class ConfigDialog {
*/ */
public static void open(Player player, Shop shop, OyeShopsPlugin plugin) { public static void open(Player player, Shop shop, OyeShopsPlugin plugin) {
Dialog dialog = Dialog.create(builder -> builder.empty() Dialog dialog = Dialog.create(builder -> builder.empty()
.base(DialogBase.builder(Component.text("shop #" + shop.getId() + " config", NamedTextColor.GOLD)) .base(DialogBase.builder(
Component.text("config: " + shop.getName(), NamedTextColor.GOLD))
.inputs(List.of( .inputs(List.of(
DialogInput.text("name",
Component.text("rename shop",
NamedTextColor.YELLOW))
.initial(shop.getName())
.build(),
DialogInput.text("custom_title", DialogInput.text("custom_title",
Component.text("custom title (optional)", NamedTextColor.YELLOW)) Component.text("custom title (optional)",
.initial(shop.getCustomTitle() != null ? shop.getCustomTitle() : "") NamedTextColor.YELLOW))
.initial(shop.getCustomTitle() != null
? shop.getCustomTitle()
: "")
.build(), .build(),
DialogInput.text("disc", DialogInput.text("disc",
Component.text( Component.text(
"music disc: none/blocks/chirp/far/mall/mellohi/stal/strad/ward/wait", "music disc: none/blocks/chirp/far/mall/mellohi/stal/strad/ward/wait",
NamedTextColor.AQUA)) NamedTextColor.AQUA))
.initial(shop.getDisc() != null ? shop.getDisc() : "none") .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()))
.build()) .build())
.type(DialogType.confirmation( .type(DialogType.confirmation(
@@ -50,46 +69,137 @@ public class ConfigDialog {
.tooltip(Component.text("save configuration")) .tooltip(Component.text("save configuration"))
.action(DialogAction.customClick((view, audience) -> { .action(DialogAction.customClick((view, audience) -> {
Player p = (Player) audience; Player p = (Player) audience;
String newName = view.getText("name");
String title = view.getText("custom_title"); String title = view.getText("custom_title");
String disc = view.getText("disc"); String disc = view.getText("disc");
String contributorsStr = view
.getText("contributors");
// validate disc if (newName == null || newName.isEmpty()) {
disc = disc.toLowerCase().trim(); p.sendMessage(Component.text(
if (!disc.isEmpty() && !VALID_DISCS.contains(disc)) { "shop name cannot be empty",
p.sendMessage(Component.text("invalid disc: " + disc
+ ". valid options: none, blocks, chirp, far, mall, mellohi, stal, strad, ward, wait",
NamedTextColor.RED)); NamedTextColor.RED));
open(p, shop, plugin); // reopen dialog open(p, shop, plugin);
return; return;
} }
shop.setCustomTitle(title.isEmpty() ? null : title); if (newName.equalsIgnoreCase(
shop.setDisc(disc.isEmpty() || disc.equals("none") ? null : disc); "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, () -> { // 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;
}
}
// 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<UUID> contributorUuids = resolveContributors(
contributorsStr);
shop.getContributors().clear();
shop.getContributors().addAll(contributorUuids);
plugin.getServer().getScheduler()
.runTaskAsynchronously(plugin,
() -> {
try { try {
plugin.getShopRepository().updateShopConfig(shop); plugin.getShopRepository()
plugin.getServer().getScheduler().runTask(plugin, () -> { .updateShopConfig(
shop);
plugin.getServer()
.getScheduler()
.runTask(plugin, () -> {
p.sendMessage( p.sendMessage(
Component.text("shop config saved", NamedTextColor.GREEN)); Component.text("shop config saved",
NamedTextColor.GREEN));
}); });
} catch (SQLException e) { } catch (SQLException e) {
plugin.getServer().getScheduler().runTask(plugin, () -> { plugin.getServer()
p.sendMessage(Component.text("database error", NamedTextColor.RED)); .getScheduler()
.runTask(plugin, () -> {
p.sendMessage(Component
.text("database error",
NamedTextColor.RED));
}); });
e.printStackTrace(); e.printStackTrace();
} }
}); });
}, ClickCallback.Options.builder().uses(1).build())) }, ClickCallback.Options.builder().uses(1).build()))
.build(), .build(),
ActionButton.builder(Component.text("cancel", TextColor.color(0xFFA0B1))) ActionButton.builder(
Component.text("cancel", TextColor.color(0xFFA0B1)))
.tooltip(Component.text("discard changes")) .tooltip(Component.text("discard changes"))
.action(DialogAction.customClick((view, audience) -> { .action(DialogAction.customClick((view, audience) -> {
((Player) audience) ((Player) audience)
.sendMessage(Component.text("config cancelled", NamedTextColor.YELLOW)); .sendMessage(Component.text(
"config cancelled",
NamedTextColor.YELLOW));
}, ClickCallback.Options.builder().build())) }, ClickCallback.Options.builder().build()))
.build()))); .build())));
player.showDialog(dialog); player.showDialog(dialog);
} }
private static String getContributorNames(List<UUID> uuids) {
if (uuids == null || uuids.isEmpty())
return "";
List<String> 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<UUID> resolveContributors(String data) {
List<UUID> 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;
}
} }

View File

@@ -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<Component> 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<Component> 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;
}
}

View File

@@ -24,10 +24,16 @@ import java.util.List;
*/ */
public class SetupDialog { 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() Dialog dialog = Dialog.create(builder -> builder.empty()
.base(DialogBase.builder(Component.text("shop setup wizard", NamedTextColor.GOLD)) .base(DialogBase.builder(Component.text("shop setup wizard", NamedTextColor.GOLD))
.inputs(List.of( .inputs(List.of(
// shop name
DialogInput.text("shop_name",
Component.text("shop name (unique)",
NamedTextColor.YELLOW))
.initial(shopName)
.build(),
// product (selling) // product (selling)
DialogInput.text("product_item", DialogInput.text("product_item",
Component.text("selling what? (e.g. dirt)", Component.text("selling what? (e.g. dirt)",
@@ -41,22 +47,13 @@ public class SetupDialog {
// price (buying) // price (buying)
DialogInput.text("price_item", DialogInput.text("price_item",
Component.text("what do you want? (e.g. diamond)", Component.text("what do you want? (prop: diamond)",
NamedTextColor.GREEN)) NamedTextColor.GREEN))
.build(), .build(),
DialogInput.text("price_qty", DialogInput.text("price_qty",
Component.text("how many? (e.g. 10)", Component.text("how many? (e.g. 10)",
NamedTextColor.GREEN)) NamedTextColor.GREEN))
.initial("1") .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()))
.build()) .build())
.type(DialogType.confirmation( .type(DialogType.confirmation(
@@ -65,6 +62,7 @@ public class SetupDialog {
.tooltip(Component .tooltip(Component
.text("click to confirm trade details")) .text("click to confirm trade details"))
.action(DialogAction.customClick((view, audience) -> { .action(DialogAction.customClick((view, audience) -> {
String newShopName = view.getText("shop_name");
String productStr = view String productStr = view
.getText("product_item"); .getText("product_item");
String productQtyStr = view String productQtyStr = view
@@ -73,6 +71,36 @@ public class SetupDialog {
String priceQtyStr = view.getText("price_qty"); String priceQtyStr = view.getText("price_qty");
Player p = (Player) audience; 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 // 1. parse price qty
int priceQty; int priceQty;
try { try {
@@ -82,7 +110,7 @@ public class SetupDialog {
p.sendMessage(Component.text( p.sendMessage(Component.text(
"invalid price quantity", "invalid price quantity",
NamedTextColor.RED)); NamedTextColor.RED));
open(p, signBlock, plugin); open(p, signBlock, plugin, newShopName);
return; return;
} }
@@ -94,11 +122,11 @@ public class SetupDialog {
"invalid payment item: " "invalid payment item: "
+ priceStr, + priceStr,
NamedTextColor.RED)); NamedTextColor.RED));
open(p, signBlock, plugin); open(p, signBlock, plugin, newShopName);
return; return;
} }
// 3. Regular parsing logic // 3. parse product qty
int productQty; int productQty;
try { try {
productQty = Integer.parseInt( productQty = Integer.parseInt(
@@ -107,38 +135,36 @@ public class SetupDialog {
p.sendMessage(Component.text( p.sendMessage(Component.text(
"invalid product quantity", "invalid product quantity",
NamedTextColor.RED)); NamedTextColor.RED));
open(p, signBlock, plugin); open(p, signBlock, plugin, newShopName);
return; return;
} }
Material productMat = plugin Material productMat = plugin.getSignParser()
.getSignParser()
.parseMaterial(productStr); .parseMaterial(productStr);
if (productMat == null) { if (productMat == null) {
p.sendMessage(Component.text( p.sendMessage(Component.text(
"invalid product: " "invalid product: "
+ productStr, + productStr,
NamedTextColor.RED)); NamedTextColor.RED));
open(p, signBlock, plugin); open(p, signBlock, plugin, newShopName);
return; return;
} }
Trade trade = new Trade(priceMat, priceQty, Trade trade = new Trade(priceMat, priceQty,
productMat, productQty); productMat, productQty);
boolean cosmeticSign = view
.getBoolean("cosmetic_sign");
PendingActivation activation = new PendingActivation( PendingActivation activation = new PendingActivation(
p.getUniqueId(), p.getUniqueId(),
signBlock.getLocation(), signBlock.getLocation(),
trade, trade,
System.currentTimeMillis(), System.currentTimeMillis(),
cosmeticSign); true); // cosmeticSign always
// true
// 4. finalize // 4. finalize
plugin.getServer().getScheduler() plugin.getServer().getScheduler()
.runTask(plugin, () -> { .runTask(plugin, () -> {
plugin.getShopActivationListener() plugin.getShopActivationListener()
.finalizeShop(p, activation); .finalizeShop(p, activation,
newShopName);
}); });
}, ClickCallback.Options.builder().uses(1).build())) }, ClickCallback.Options.builder().uses(1).build()))
.build(), .build(),

View File

@@ -24,7 +24,6 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import party.cybsec.oyeshops.OyeShopsPlugin; import party.cybsec.oyeshops.OyeShopsPlugin;
import party.cybsec.oyeshops.gui.ConfirmationGui;
import party.cybsec.oyeshops.model.Shop; import party.cybsec.oyeshops.model.Shop;
import party.cybsec.oyeshops.model.Trade; import party.cybsec.oyeshops.model.Trade;
import party.cybsec.oyeshops.permission.PermissionManager; import party.cybsec.oyeshops.permission.PermissionManager;
@@ -40,8 +39,11 @@ import java.util.Map;
public class ChestInteractionListener implements Listener { public class ChestInteractionListener implements Listener {
private final OyeShopsPlugin plugin; private final OyeShopsPlugin plugin;
// track players viewing fake shop inventories // track active shop sessions for players
private final Map<Player, Shop> viewingShop = new HashMap<>(); private final Map<Player, ShopSession> activeSessions = new HashMap<>();
public record ShopSession(Shop shop, int unitsTraded, Inventory realInventory) {
}
public ChestInteractionListener(OyeShopsPlugin plugin) { public ChestInteractionListener(OyeShopsPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
@@ -63,8 +65,7 @@ public class ChestInteractionListener implements Listener {
return; return;
} }
// check if there's a shop sign attached to this container or its double chest // check if there's a shop sign attached to this container
// partner
Shop shop = findShopForContainer(block); Shop shop = findShopForContainer(block);
if (shop == null) { if (shop == null) {
return; // not a shop container return; // not a shop container
@@ -79,16 +80,18 @@ public class ChestInteractionListener implements Listener {
return; return;
} }
// check if player is owner (unless spoofing) // check if player is owner or contributor (unless spoofing)
boolean isOwner = shop.getOwner().equals(player.getUniqueId()) boolean isOwner = shop.getOwner().equals(player.getUniqueId())
&& !plugin.getSpoofManager().isSpoofing(player); && !plugin.getSpoofManager().isSpoofing(player);
boolean isContributor = shop.getContributors().contains(player.getUniqueId())
&& !plugin.getSpoofManager().isSpoofing(player);
if (isOwner) { if (isOwner || isContributor) {
// owner interaction - check for owed items and dispense // owner/contributor interaction - check for owed items and dispense
if (shop.getOwedAmount() > 0) { if (shop.getOwedAmount() > 0) {
withdrawOwedItems(player, shop); withdrawOwedItems(player, shop);
} }
// let owner open the chest normally - don't cancel event // let them open the chest normally
return; return;
} }
@@ -173,18 +176,13 @@ public class ChestInteractionListener implements Listener {
title = Component.text(shop.getCustomTitle()); title = Component.text(shop.getCustomTitle());
} }
// get real container inventory (handles double chests correctly) // get real container inventory
Inventory realInventory = getContainerInventory(containerBlock); Inventory realInventory = getContainerInventory(containerBlock);
if (realInventory == null) { if (realInventory == null) {
return; return;
} }
// determine inventory size based on container type int invSize = Math.min(54, Math.max(27, realInventory.getSize()));
int invSize = realInventory.getSize();
if (invSize > 54)
invSize = 54; // cap at double chest
if (invSize < 27)
invSize = 27; // minimum single chest
// create fake inventory showing only product items // create fake inventory showing only product items
Inventory shopInventory = Bukkit.createInventory( Inventory shopInventory = Bukkit.createInventory(
@@ -192,17 +190,26 @@ public class ChestInteractionListener implements Listener {
invSize, invSize,
title); title);
// copy matching items to fake inventory // populate with units (productQuantity per slot)
int shopSlot = 0; int totalStock = 0;
for (ItemStack item : realInventory.getContents()) { for (ItemStack item : realInventory.getContents()) {
if (shopSlot >= invSize)
break;
if (item != null && trade.matchesProduct(item.getType())) { 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); player.openInventory(shopInventory);
// play shop owner's configured disc if set // play shop owner's configured disc if set
@@ -256,97 +263,147 @@ public class ChestInteractionListener implements Listener {
return; return;
} }
InventoryHolder holder = event.getInventory().getHolder(); ShopSession session = activeSessions.get(player);
if (session == null) {
// handle shop inventory clicks return;
if (holder instanceof ShopInventoryHolder shopHolder) { }
event.setCancelled(true);
Shop shop = shopHolder.shop(); InventoryHolder holder = event.getInventory().getHolder();
if (!(holder instanceof ShopInventoryHolder)) {
// 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);
}
return; return;
} }
// handle confirmation GUI clicks
if (holder instanceof ConfirmationGui confirmGui) {
event.setCancelled(true); event.setCancelled(true);
Shop shop = confirmGui.getShop();
int slot = event.getRawSlot(); int slot = event.getRawSlot();
if (slot >= event.getInventory().getSize()) {
return; // Clicked player inventory
}
// green pane = confirm (slot 11-15 or specifically slot 11) ItemStack clicked = event.getCurrentItem();
if (slot == 11 || slot == 12 || slot == 13) { if (clicked == null || clicked.getType() == Material.AIR) {
player.closeInventory(); return;
executeTransaction(player, shop, confirmGui.getUnits());
} }
// red pane = cancel (slot 15 or right side)
else if (slot == 14 || slot == 15 || slot == 16) { Shop shop = session.shop();
player.closeInventory(); Trade trade = shop.getTrade();
player.sendMessage(Component.text("purchase cancelled", NamedTextColor.YELLOW)); 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;
} }
// 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 @EventHandler
public void onInventoryClose(InventoryCloseEvent event) { public void onInventoryClose(InventoryCloseEvent event) {
if (event.getPlayer() instanceof Player player) { if (!(event.getPlayer() instanceof Player player)) {
// stop disc when closing shop return;
Shop shop = viewingShop.remove(player); }
if (shop != null) {
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));
}
}
// stop disc
String discName = shop.getDisc(); String discName = shop.getDisc();
if (discName != null && !discName.isEmpty()) { if (discName != null && !discName.isEmpty()) {
stopDisc(player, discName); stopDisc(player, discName);
} }
} }
}
}
/**
* 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));
}
}
/** /**
* find shop attached to a container block (handles double chests) * find shop attached to a container block (handles double chests)

View File

@@ -39,8 +39,7 @@ public class LoginListener implements Listener {
return; return;
} }
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { // find all shops owned by player and check stock on main thread
// find all shops owned by player
List<Shop> lowStockShops = new ArrayList<>(); List<Shop> lowStockShops = new ArrayList<>();
for (Shop shop : plugin.getShopRegistry().getAllShops()) { for (Shop shop : plugin.getShopRegistry().getAllShops()) {
if (shop.getOwner().equals(player.getUniqueId())) { if (shop.getOwner().equals(player.getUniqueId())) {
@@ -51,16 +50,10 @@ public class LoginListener implements Listener {
} }
if (!lowStockShops.isEmpty()) { if (!lowStockShops.isEmpty()) {
final int count = lowStockShops.size();
plugin.getServer().getScheduler().runTask(plugin, () -> {
if (player.isOnline()) {
player.sendMessage( player.sendMessage(
Component.text("notification: " + count + " of your shops are low on stock.", Component.text("notification: " + lowStockShops.size() + " of your shops are low on stock.",
NamedTextColor.YELLOW)); NamedTextColor.YELLOW));
} }
});
}
});
} }
private boolean isLowStock(Shop shop) { private boolean isLowStock(Shop shop) {

View File

@@ -92,9 +92,9 @@ public class ShopActivationListener implements Listener {
} }
// 2. session-based ownership check // 2. session-based ownership check
if (!PermissionManager.isAdmin(player)) { if (plugin.getConfigManager().isRequirePlacement() && !PermissionManager.isAdmin(player)) {
UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation()); 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 // 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", player.sendMessage(Component.text("you can only create shops on containers you placed this session",
NamedTextColor.RED)); NamedTextColor.RED));
@@ -127,7 +127,7 @@ public class ShopActivationListener implements Listener {
// clear sign text to avoid "setup" staying on the sign // clear sign text to avoid "setup" staying on the sign
event.line(0, Component.text("")); event.line(0, Component.text(""));
SetupDialog.open(player, block, plugin); SetupDialog.open(player, block, plugin, "shop-" + (plugin.getShopRegistry().getAllShops().size() + 1));
return; return;
} }
@@ -158,6 +158,16 @@ public class ShopActivationListener implements Listener {
private void sendConfirmationMessage(Player player, Trade trade) { private void sendConfirmationMessage(Player player, Trade trade) {
String priceText = trade.priceQuantity() + " " + formatMaterial(trade.priceItem()); String priceText = trade.priceQuantity() + " " + formatMaterial(trade.priceItem());
String productText = trade.productQuantity() + " " + formatMaterial(trade.productItem()); 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 // clear display: buyer pays vs buyer gets
player.sendMessage(Component.text("shop detected!", NamedTextColor.GOLD)); 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) .append(Component.text("[accept]", NamedTextColor.GREEN, TextDecoration.BOLD)
.hoverEvent(HoverEvent.showText( .hoverEvent(HoverEvent.showText(
Component.text("create shop exactly as shown above", NamedTextColor.WHITE))) 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(" "))
.append(Component.text("[invert]", NamedTextColor.GOLD, TextDecoration.BOLD) .append(Component.text("[invert]", NamedTextColor.GOLD, TextDecoration.BOLD)
.hoverEvent(HoverEvent.showText(Component.text( .hoverEvent(HoverEvent.showText(Component.text(
"swap: buyer pays " + productText + " and gets " + priceText, NamedTextColor.WHITE))) "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(" "))
.append(Component.text("[cancel]", NamedTextColor.RED, TextDecoration.BOLD) .append(Component.text("[cancel]", NamedTextColor.RED, TextDecoration.BOLD)
.hoverEvent(HoverEvent.showText( .hoverEvent(HoverEvent.showText(
Component.text("ignore sign. no shop created.", NamedTextColor.WHITE))) 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); player.sendMessage(buttons);
} }
@@ -189,7 +199,7 @@ public class ShopActivationListener implements Listener {
* complete the shop creation process * complete the shop creation process
* called from AdminCommands when user clicks [accept] or [invert] * 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(); Location signLocation = activation.location();
Block block = signLocation.getBlock(); Block block = signLocation.getBlock();
@@ -201,8 +211,9 @@ public class ShopActivationListener implements Listener {
Trade trade = activation.trade(); Trade trade = activation.trade();
long createdAt = System.currentTimeMillis(); long createdAt = System.currentTimeMillis();
Shop shop = new Shop(-1, signLocation, player.getUniqueId(), trade, 0, true, createdAt, null, // default cosmeticSign to true as requested
activation.cosmeticSign(), null); Shop shop = new Shop(-1, shopName, signLocation, player.getUniqueId(), new ArrayList<>(), trade, 0, true,
createdAt, null, true, null);
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
@@ -210,22 +221,24 @@ public class ShopActivationListener implements Listener {
plugin.getShopRepository().deleteShopByLocation(signLocation); plugin.getShopRepository().deleteShopByLocation(signLocation);
int shopId = plugin.getShopRepository().createShop(shop); 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, () -> { plugin.getServer().getScheduler().runTask(plugin, () -> {
// re-verify sign on main thread // 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); plugin.getLogger().info("DEBUG: sign missing at " + signLocation);
return; return;
} }
Shop registeredShop = new Shop(shopId, signLocation, player.getUniqueId(), trade, 0, true, Shop registeredShop = new Shop(shopId, shopName, signLocation, player.getUniqueId(),
createdAt, null, activation.cosmeticSign(), null); new ArrayList<>(), trade, 0, true, createdAt, null, true, null);
plugin.getShopRegistry().register(registeredShop); plugin.getShopRegistry().register(registeredShop);
plugin.getLogger().info("DEBUG: registered shop " + shopId + " in registry"); plugin.getLogger().info("DEBUG: registered shop " + shopId + " in registry");
rewriteSignLines(finalSign, registeredShop); // rewriteSignLines(finalSign, registeredShop); // REMOVED: Sign should just be
player.sendMessage(Component.text("shop #" + shopId + " initialized!", NamedTextColor.GREEN)); // left alone
player.sendMessage(Component.text("shop '" + shopName + "' (#" + shopId + ") initialized!",
NamedTextColor.GREEN));
}); });
} catch (SQLException e) { } catch (SQLException e) {
plugin.getServer().getScheduler().runTask(plugin, () -> { plugin.getServer().getScheduler().runTask(plugin, () -> {
@@ -237,20 +250,7 @@ public class ShopActivationListener implements Listener {
} }
public void rewriteSignLines(Sign sign, Shop shop) { public void rewriteSignLines(Sign sign, Shop shop) {
if (shop.isCosmeticSign()) { // REMOVED: Sign should just be left alone as requested
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();
} }
/** /**
@@ -355,15 +355,6 @@ public class ShopActivationListener implements Listener {
return bestQuantity; 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) { private Inventory getContainerInventory(Block block) {
if (block.getState() instanceof Chest chest) if (block.getState() instanceof Chest chest)
return chest.getInventory(); return chest.getInventory();

View File

@@ -2,6 +2,7 @@ package party.cybsec.oyeshops.model;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.List;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -9,8 +10,10 @@ import java.util.UUID;
*/ */
public class Shop { public class Shop {
private final int id; private final int id;
private String name;
private final Location signLocation; private final Location signLocation;
private final UUID owner; private final UUID owner;
private final List<UUID> contributors;
private final Trade trade; private final Trade trade;
private int owedAmount; private int owedAmount;
private boolean enabled; private boolean enabled;
@@ -19,11 +22,13 @@ public class Shop {
private boolean cosmeticSign; private boolean cosmeticSign;
private String disc; private String disc;
public Shop(int id, Location signLocation, UUID owner, Trade trade, int owedAmount, boolean enabled, public Shop(int id, String name, Location signLocation, UUID owner, List<UUID> contributors, Trade trade,
long createdAt, String customTitle, boolean cosmeticSign, String disc) { int owedAmount, boolean enabled, long createdAt, String customTitle, boolean cosmeticSign, String disc) {
this.id = id; this.id = id;
this.name = name;
this.signLocation = signLocation; this.signLocation = signLocation;
this.owner = owner; this.owner = owner;
this.contributors = contributors;
this.trade = trade; this.trade = trade;
this.owedAmount = owedAmount; this.owedAmount = owedAmount;
this.enabled = enabled; this.enabled = enabled;
@@ -33,6 +38,18 @@ public class Shop {
this.disc = disc; this.disc = disc;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<UUID> getContributors() {
return contributors;
}
public int getId() { public int getId() {
return id; return id;
} }
@@ -97,8 +114,6 @@ public class Shop {
* get chest location from sign location * get chest location from sign location
*/ */
public Location getChestLocation() { public Location getChestLocation() {
// chest is attached to the sign return signLocation.clone();
// we'll determine this from the sign's attached block face
return signLocation.clone(); // placeholder - will be properly implemented
} }
} }

View File

@@ -75,6 +75,34 @@ public class ShopRegistry {
return shopsById.get(shopId); 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 * get all shops
*/ */

View File

@@ -33,7 +33,41 @@ public class TransactionManager {
INSUFFICIENT_STOCK, INSUFFICIENT_STOCK,
INVENTORY_FULL, INVENTORY_FULL,
DATABASE_ERROR, 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 { try {
// remove price items from buyer // remove price items from buyer (NBT preserved if needed, though usually
removeItems(buyer.getInventory(), trade.priceItem(), totalPrice); // currency)
removeItemsAndReturn(buyer.getInventory(), trade.priceItem(), totalPrice);
// remove product items from chest // remove product items from chest (NBT CRITICAL HERE)
removeItems(chestInventory, trade.productItem(), totalProduct); ItemStack[] productItems = removeItemsAndReturn(chestInventory, trade.productItem(), totalProduct);
// add product items to buyer // add product items to buyer
HashMap<Integer, ItemStack> overflow = buyer.getInventory().addItem( HashMap<Integer, ItemStack> overflow = buyer.getInventory().addItem(productItems);
createItemStacks(trade.productItem(), totalProduct));
if (!overflow.isEmpty()) { if (!overflow.isEmpty()) {
// rollback // rollback
@@ -130,7 +164,7 @@ public class TransactionManager {
/** /**
* check if inventory has required items * 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; int count = 0;
for (ItemStack item : inventory.getContents()) { for (ItemStack item : inventory.getContents()) {
if (item != null && item.getType() == material) { 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; int remaining = amount;
java.util.List<ItemStack> removed = new java.util.ArrayList<>();
for (int i = 0; i < inventory.getSize() && remaining > 0; i++) { for (int i = 0; i < inventory.getSize() && remaining > 0; i++) {
ItemStack item = inventory.getItem(i); ItemStack item = inventory.getItem(i);
if (item != null && item.getType() == material) { if (item != null && item.getType() == material) {
int toRemove = Math.min(item.getAmount(), remaining); int toRemove = Math.min(item.getAmount(), remaining);
ItemStack clone = item.clone();
clone.setAmount(toRemove);
removed.add(clone);
if (toRemove == item.getAmount()) { if (toRemove == item.getAmount()) {
inventory.setItem(i, null); inventory.setItem(i, null);
} else { } else {
@@ -161,12 +200,32 @@ public class TransactionManager {
remaining -= toRemove; 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 maxStack = material.getMaxStackSize();
int fullStacks = totalAmount / maxStack; int fullStacks = totalAmount / maxStack;
int remainder = totalAmount % maxStack; int remainder = totalAmount % maxStack;

View File

@@ -5,6 +5,9 @@ transactions:
auto-prune: false auto-prune: false
max-per-shop: 100 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) # custom material aliases (add your own shortcuts)
aliases: aliases:
# example: # example:

View File

@@ -1,5 +1,5 @@
name: oyeShops name: oyeShops
version: 1.3.0 version: 1.3.1
main: party.cybsec.oyeshops.OyeShopsPlugin main: party.cybsec.oyeshops.OyeShopsPlugin
api-version: '1.21' api-version: '1.21'
description: deterministic item-for-item chest barter description: deterministic item-for-item chest barter
@@ -9,7 +9,7 @@ website: https://party.cybsec
commands: commands:
oyeshops: oyeshops:
description: oyeShops commands description: oyeShops commands
usage: /oyeshops <on|off|toggle|reload|inspect|spoof|enable|disable|unregister> usage: /oyeshops <on|off|toggle|reload|setup|config|tpshop|toggle-placement>
aliases: [oyes, oshop] aliases: [oyes, oshop]
permissions: permissions: