Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58b7d44d8f | |||
| 84b4471fcb | |||
| 0134b768a8 | |||
| 191a02234e | |||
| 37cb4453ac | |||
| 02c53849cf | |||
| e8e4e35b6e | |||
| 087bf7eb40 | |||
| 3e1698614e | |||
| a53a4dac61 | |||
| 9eecc99f56 | |||
| e5bc3d1f14 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,3 +30,4 @@ out/
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
oyetickets/
|
||||
|
||||
@@ -3,16 +3,17 @@ plugins {
|
||||
}
|
||||
|
||||
group = "party.cybsec"
|
||||
version = "1.0.0"
|
||||
version = "1.3.1"
|
||||
description = "deterministic item-for-item chest barter"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
maven("https://repo.papermc.io/repository/maven-snapshots/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
|
||||
implementation("org.xerial:sqlite-jdbc:3.47.1.0")
|
||||
}
|
||||
|
||||
|
||||
57
oyeOwner/INTEGRATION_GUIDE.md
Normal file
57
oyeOwner/INTEGRATION_GUIDE.md
Normal 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
73
oyeOwner/pom.xml
Normal 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>
|
||||
56
oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java
Normal file
56
oyeOwner/src/main/java/party/cybsec/CoreProtectHook.java
Normal 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;
|
||||
}
|
||||
}
|
||||
50
oyeOwner/src/main/java/party/cybsec/OyeOwner.java
Normal file
50
oyeOwner/src/main/java/party/cybsec/OyeOwner.java
Normal 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;
|
||||
}
|
||||
}
|
||||
59
oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java
Normal file
59
oyeOwner/src/main/java/party/cybsec/OyeOwnerAPI.java
Normal 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));
|
||||
}
|
||||
}
|
||||
47
oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java
Normal file
47
oyeOwner/src/main/java/party/cybsec/command/WhoCommand.java
Normal 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;
|
||||
}
|
||||
}
|
||||
14
oyeOwner/src/main/resources/plugin.yml
Normal file
14
oyeOwner/src/main/resources/plugin.yml
Normal 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
|
||||
BIN
oyeOwner/target/classes/party/cybsec/CoreProtectHook.class
Normal file
BIN
oyeOwner/target/classes/party/cybsec/CoreProtectHook.class
Normal file
Binary file not shown.
BIN
oyeOwner/target/classes/party/cybsec/OyeOwner.class
Normal file
BIN
oyeOwner/target/classes/party/cybsec/OyeOwner.class
Normal file
Binary file not shown.
BIN
oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class
Normal file
BIN
oyeOwner/target/classes/party/cybsec/OyeOwnerAPI.class
Normal file
Binary file not shown.
BIN
oyeOwner/target/classes/party/cybsec/command/WhoCommand.class
Normal file
BIN
oyeOwner/target/classes/party/cybsec/command/WhoCommand.class
Normal file
Binary file not shown.
14
oyeOwner/target/classes/plugin.yml
Normal file
14
oyeOwner/target/classes/plugin.yml
Normal 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
|
||||
3
oyeOwner/target/maven-archiver/pom.properties
Normal file
3
oyeOwner/target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=oyeOwner
|
||||
groupId=party.cybsec
|
||||
version=1.0-SNAPSHOT
|
||||
@@ -0,0 +1,4 @@
|
||||
party/cybsec/command/WhoCommand.class
|
||||
party/cybsec/OyeOwner.class
|
||||
party/cybsec/CoreProtectHook.class
|
||||
party/cybsec/OyeOwnerAPI.class
|
||||
@@ -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
|
||||
@@ -11,7 +11,12 @@ import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.PendingActivation;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.permission.PermissionManager;
|
||||
import party.cybsec.oyeshops.listener.ShopActivationListener;
|
||||
import party.cybsec.oyeshops.gui.HelpBook;
|
||||
import party.cybsec.oyeshops.gui.SetupDialog;
|
||||
import party.cybsec.oyeshops.gui.ConfigDialog;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
@@ -42,12 +47,17 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
case "toggle" -> handleToggle(sender);
|
||||
case "notify" -> handleNotifyToggle(sender);
|
||||
case "info" -> handleInfo(sender);
|
||||
case "help" -> handleHelp(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);
|
||||
default -> sendHelp(sender);
|
||||
case "toggleplacement", "toggle-placement" -> handleTogglePlacement(sender);
|
||||
default -> handleHelp(sender);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -55,6 +65,12 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
|
||||
private void sendHelp(CommandSender sender) {
|
||||
sender.sendMessage(Component.text("=== oyeshops commands ===", NamedTextColor.GOLD));
|
||||
sender.sendMessage(Component.text("/oyeshops help", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - open interactive guide", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops setup", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - open shop wizard", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops config", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - configure looking shop", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops on", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - enable shop creation", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops off", NamedTextColor.YELLOW)
|
||||
@@ -79,6 +95,10 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
.append(Component.text(" - disable a shop", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops unregister <id>", NamedTextColor.YELLOW)
|
||||
.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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +106,14 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
sender.sendMessage(Component.text("cybsec made this plugin", NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
private void handleHelp(CommandSender sender) {
|
||||
if (sender instanceof Player player) {
|
||||
HelpBook.open(player);
|
||||
} else {
|
||||
sendHelp(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReload(CommandSender sender) {
|
||||
if (!PermissionManager.isAdmin(sender)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
@@ -200,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 <action> <name>", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
String action = args[1].toLowerCase();
|
||||
String name = args[2];
|
||||
PendingActivation activation = plugin.getActivationManager().getAndRemove(player.getUniqueId());
|
||||
|
||||
if (activation == null) {
|
||||
@@ -213,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(
|
||||
@@ -225,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) {
|
||||
@@ -236,12 +267,24 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
plugin.getPlayerPreferenceRepository().enableShops(player.getUniqueId());
|
||||
player.sendMessage(Component.text("shop creation enabled. you can now make chest shops!",
|
||||
NamedTextColor.GREEN));
|
||||
|
||||
// show help book on first enable
|
||||
if (!plugin.getPlayerPreferenceRepository().hasSeenIntro(player.getUniqueId())) {
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
HelpBook.open(player);
|
||||
try {
|
||||
plugin.getPlayerPreferenceRepository().setSeenIntro(player.getUniqueId());
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, 20L); // delay slightly
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
player.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage(Component.text("usage: /oyeshops enable <id>", NamedTextColor.RED));
|
||||
sender.sendMessage(Component.text("usage: /oyeshops enable <id/name>", NamedTextColor.RED));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -251,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();
|
||||
@@ -281,7 +331,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage(Component.text("usage: /oyeshops disable <id>", NamedTextColor.RED));
|
||||
sender.sendMessage(Component.text("usage: /oyeshops disable <id/name>", NamedTextColor.RED));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -291,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();
|
||||
@@ -316,22 +373,29 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -342,9 +406,11 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (args.length == 1) {
|
||||
List<String> subCommands = new ArrayList<>(List.of("on", "off", "toggle", "notify", "enable", "disable"));
|
||||
List<String> 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) {
|
||||
@@ -353,16 +419,219 @@ 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<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;
|
||||
}
|
||||
|
||||
private void handleSetup(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("players only", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.canCreate(player)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
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);
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
||||
// duplicate validation logic from ShopActivationListener for safety
|
||||
BlockFace attachedFace = wallSign.getFacing().getOppositeFace();
|
||||
Block attachedBlock = block.getRelative(attachedFace);
|
||||
|
||||
if (!party.cybsec.oyeshops.listener.ShopActivationListener.isContainer(attachedBlock.getType())) {
|
||||
player.sendMessage(Component.text("sign must be on a chest or barrel", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
Component.text("you can only create shops on containers you placed this session",
|
||||
NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SetupDialog.open(player, block, plugin, shopName);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
if (block == null) {
|
||||
player.sendMessage(
|
||||
Component.text("you must look at a shop sign or its container, or use /oyes config <name>",
|
||||
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) {
|
||||
player.sendMessage(Component.text("that is not a part of any shop", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
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 <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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package party.cybsec.oyeshops.config;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
|
||||
/**
|
||||
* configuration loading and management
|
||||
* manages plugin configuration
|
||||
*/
|
||||
public class ConfigManager {
|
||||
private final Plugin plugin;
|
||||
private final OyeShopsPlugin plugin;
|
||||
private FileConfiguration config;
|
||||
|
||||
public ConfigManager(Plugin plugin) {
|
||||
public ConfigManager(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
reload();
|
||||
}
|
||||
@@ -18,26 +18,32 @@ public class ConfigManager {
|
||||
public void reload() {
|
||||
plugin.saveDefaultConfig();
|
||||
plugin.reloadConfig();
|
||||
this.config = plugin.getConfig();
|
||||
}
|
||||
|
||||
public boolean isAllowProductOutput() {
|
||||
return config.getBoolean("hoppers.allow-product-output", true);
|
||||
}
|
||||
|
||||
public boolean isBlockPriceInput() {
|
||||
return config.getBoolean("hoppers.block-price-input", true);
|
||||
}
|
||||
|
||||
public int getMaxTransactionsPerShop() {
|
||||
return config.getInt("history.max-transactions-per-shop", 100);
|
||||
}
|
||||
|
||||
public boolean isAutoPrune() {
|
||||
return config.getBoolean("history.auto-prune", true);
|
||||
config = plugin.getConfig();
|
||||
}
|
||||
|
||||
public FileConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
plugin.saveConfig();
|
||||
}
|
||||
|
||||
// transaction settings
|
||||
public boolean isAutoPrune() {
|
||||
return config.getBoolean("transactions.auto-prune", false);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,58 @@ public class DatabaseManager {
|
||||
player_uuid text primary key,
|
||||
shops_enabled boolean not null default false,
|
||||
notify_low_stock boolean not null default false,
|
||||
seen_intro boolean not null default false,
|
||||
enabled_at integer
|
||||
)
|
||||
""");
|
||||
|
||||
// migration: add seen_intro if missing
|
||||
try {
|
||||
stmt.execute("alter table player_preferences add column seen_intro boolean not null default false");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
|
||||
// migration: add owed_amount, enabled, created_at to shops if missing
|
||||
try {
|
||||
stmt.execute("alter table shops add column owed_amount integer not null default 0");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
try {
|
||||
stmt.execute("alter table shops add column enabled boolean not null default true");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
try {
|
||||
stmt.execute("alter table shops add column created_at integer not null default 0");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
try {
|
||||
stmt.execute("alter table shops add column merchant_ui boolean not null default false");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
stmt.execute("alter table shops add column cosmetic_sign boolean not null default false");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
stmt.execute("alter table shops add column custom_title text");
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
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
|
||||
|
||||
@@ -15,9 +15,9 @@ import java.util.UUID;
|
||||
public class PlayerPreferenceRepository {
|
||||
private final DatabaseManager dbManager;
|
||||
|
||||
// in-memory cache for performance
|
||||
private final Set<UUID> enabledPlayers = new HashSet<>();
|
||||
private final Set<UUID> notifyPlayers = new HashSet<>();
|
||||
private final Set<UUID> seenIntroPlayers = new HashSet<>();
|
||||
|
||||
public PlayerPreferenceRepository(DatabaseManager dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
@@ -27,7 +27,7 @@ public class PlayerPreferenceRepository {
|
||||
* load all player preferences into cache
|
||||
*/
|
||||
public void loadPreferences() throws SQLException {
|
||||
String sql = "select player_uuid, shops_enabled, notify_low_stock from player_preferences";
|
||||
String sql = "select player_uuid, shops_enabled, notify_low_stock, seen_intro from player_preferences";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
@@ -39,6 +39,9 @@ public class PlayerPreferenceRepository {
|
||||
if (rs.getBoolean("notify_low_stock")) {
|
||||
notifyPlayers.add(uuid);
|
||||
}
|
||||
if (rs.getBoolean("seen_intro")) {
|
||||
seenIntroPlayers.add(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +60,31 @@ public class PlayerPreferenceRepository {
|
||||
return notifyPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player has seen the intro
|
||||
*/
|
||||
public boolean hasSeenIntro(UUID playerUuid) {
|
||||
return seenIntroPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* set seen intro flag
|
||||
*/
|
||||
public void setSeenIntro(UUID playerUuid) throws SQLException {
|
||||
String sql = """
|
||||
insert into player_preferences (player_uuid, seen_intro)
|
||||
values (?, true)
|
||||
on conflict(player_uuid) do update set seen_intro = true
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
seenIntroPlayers.add(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* enable shops for player
|
||||
*/
|
||||
|
||||
@@ -29,8 +29,9 @@ public class ShopRepository {
|
||||
String sql = """
|
||||
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)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
owed_amount, enabled, created_at, custom_title,
|
||||
cosmetic_sign, disc, name, contributors)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql,
|
||||
@@ -48,6 +49,11 @@ public class ShopRepository {
|
||||
stmt.setInt(10, shop.getOwedAmount());
|
||||
stmt.setBoolean(11, shop.isEnabled());
|
||||
stmt.setLong(12, shop.getCreatedAt());
|
||||
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();
|
||||
|
||||
@@ -166,6 +172,25 @@ public class ShopRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update shop configuration fields
|
||||
*/
|
||||
public void updateShopConfig(Shop shop) throws SQLException {
|
||||
String sql = """
|
||||
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.setString(3, shop.getName());
|
||||
stmt.setString(4, serializeContributors(shop.getContributors()));
|
||||
stmt.setInt(5, shop.getId());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* delete shop
|
||||
*/
|
||||
@@ -178,6 +203,21 @@ public class ShopRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* delete shop by location (world, x, y, z)
|
||||
*/
|
||||
public void deleteShopByLocation(Location loc) throws SQLException {
|
||||
String sql = "delete from shops where world_uuid = ? and sign_x = ? and sign_y = ? and sign_z = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, loc.getWorld().getUID().toString());
|
||||
stmt.setInt(2, loc.getBlockX());
|
||||
stmt.setInt(3, loc.getBlockY());
|
||||
stmt.setInt(4, loc.getBlockZ());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all shops owned by player
|
||||
*/
|
||||
@@ -215,16 +255,47 @@ public class ShopRepository {
|
||||
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
|
||||
*/
|
||||
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<UUID> 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"));
|
||||
@@ -232,6 +303,9 @@ public class ShopRepository {
|
||||
int owedAmount = rs.getInt("owed_amount");
|
||||
boolean enabled = rs.getBoolean("enabled");
|
||||
long createdAt = rs.getLong("created_at");
|
||||
String customTitle = rs.getString("custom_title");
|
||||
boolean cosmeticSign = rs.getBoolean("cosmetic_sign");
|
||||
String disc = rs.getString("disc");
|
||||
|
||||
World world = Bukkit.getWorld(worldUuid);
|
||||
if (world == null) {
|
||||
@@ -241,6 +315,8 @@ 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
205
src/main/java/party/cybsec/oyeshops/gui/ConfigDialog.java
Normal file
205
src/main/java/party/cybsec/oyeshops/gui/ConfigDialog.java
Normal file
@@ -0,0 +1,205 @@
|
||||
package party.cybsec.oyeshops.gui;
|
||||
|
||||
import io.papermc.paper.dialog.Dialog;
|
||||
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||
import io.papermc.paper.registry.data.dialog.input.DialogInput;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickCallback;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.entity.Player;
|
||||
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<String> 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("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");
|
||||
|
||||
if (newName == null || newName.isEmpty()) {
|
||||
p.sendMessage(Component.text(
|
||||
"shop name cannot be empty",
|
||||
NamedTextColor.RED));
|
||||
open(p, shop, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
174
src/main/java/party/cybsec/oyeshops/gui/HelpBook.java
Normal file
174
src/main/java/party/cybsec/oyeshops/gui/HelpBook.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package party.cybsec.oyeshops.gui;
|
||||
|
||||
import net.kyori.adventure.inventory.Book;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* utility to open an interactive help book for players
|
||||
* all text is lowercase for consistency
|
||||
*/
|
||||
public class HelpBook {
|
||||
|
||||
public static void open(Player player) {
|
||||
List<Component> pages = new ArrayList<>();
|
||||
|
||||
// page 1: introduction
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("oyeshops guide", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("welcome to the simple item barter system.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("steps to start:", NamedTextColor.GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("1. type ", NamedTextColor.BLACK))
|
||||
.append(Component.text("/oyes on", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("2. place a chest", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("3. place a wall sign", NamedTextColor.BLACK))
|
||||
.build());
|
||||
|
||||
// page 2: setup wizard (intro)
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("setup wizard", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("hate typing?", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("1. look at a sign", NamedTextColor.GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("2. type ", NamedTextColor.GRAY))
|
||||
.append(Component.text("/oyes setup", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("...or turn the page", NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 3: setup wizard (sign trigger)
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("sign trigger", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("just write ", NamedTextColor.GRAY))
|
||||
.append(Component.text("setup", NamedTextColor.BLUE, TextDecoration.BOLD))
|
||||
.append(Component.text(" on the first line of the sign.", NamedTextColor.GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text(
|
||||
"this opens a fancy menu where you can click to create your shop.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 4: manual creation
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("manual setup", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("write exactly this:", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("line 1: ", NamedTextColor.GRAY))
|
||||
.append(Component.text("1 diamond", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("line 2: ", NamedTextColor.GRAY))
|
||||
.append(Component.text("for", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("line 3: ", NamedTextColor.GRAY))
|
||||
.append(Component.text("64 dirt", NamedTextColor.BLACK))
|
||||
.build());
|
||||
|
||||
// page 5: auto detection
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("auto detection", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("lazy? just put items in the chest and write:",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("auto", NamedTextColor.BLUE, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("for 10 gold", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("the plugin will check the chest contents.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 6: ownership
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("ownership rules", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text(
|
||||
"you can only make shops on containers you placed ",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("this session", NamedTextColor.BLACK, TextDecoration.ITALIC))
|
||||
.append(Component.text(".", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text(
|
||||
"prevents stealing old chests!",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 7: containers
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("supported blocks", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("- chests", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("- barrels", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("- trapped chests", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("double chests supported.", NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 8: commands
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("commands", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("/oyes notify", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("get low stock alerts.", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("/oyes info", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("plugin info.", NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 9: tips
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("pro tips", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("- use wall signs.", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("- abbreviations: ", NamedTextColor.BLACK))
|
||||
.append(Component.text("dia", NamedTextColor.BLUE))
|
||||
.append(Component.text(" = diamond.", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("- shops are ", NamedTextColor.BLACK))
|
||||
.append(Component.text("off", NamedTextColor.RED))
|
||||
.append(Component.text(" by default.", NamedTextColor.BLACK))
|
||||
.build());
|
||||
|
||||
Book book = Book.book(Component.text("oyeshops manual"), Component.text("oyeshops"), pages);
|
||||
player.openBook(book);
|
||||
}
|
||||
}
|
||||
183
src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java
Normal file
183
src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java
Normal file
@@ -0,0 +1,183 @@
|
||||
package party.cybsec.oyeshops.gui;
|
||||
|
||||
import io.papermc.paper.dialog.Dialog;
|
||||
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||
import io.papermc.paper.registry.data.dialog.input.DialogInput;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickCallback;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.PendingActivation;
|
||||
import party.cybsec.oyeshops.model.Trade;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* opens a setup wizard for creating shops using paper's dialog api
|
||||
*/
|
||||
public class SetupDialog {
|
||||
|
||||
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)",
|
||||
NamedTextColor.YELLOW))
|
||||
.build(),
|
||||
DialogInput.text("product_qty",
|
||||
Component.text("how many? (e.g. 1)",
|
||||
NamedTextColor.YELLOW))
|
||||
.initial("1")
|
||||
.build(),
|
||||
|
||||
// price (buying)
|
||||
DialogInput.text("price_item",
|
||||
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()))
|
||||
.build())
|
||||
.type(DialogType.confirmation(
|
||||
ActionButton.builder(Component.text("create shop",
|
||||
TextColor.color(0xAEFFC1)))
|
||||
.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
|
||||
.getText("product_qty");
|
||||
String priceStr = view.getText("price_item");
|
||||
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 {
|
||||
priceQty = Integer
|
||||
.parseInt(priceQtyStr);
|
||||
} catch (NumberFormatException e) {
|
||||
p.sendMessage(Component.text(
|
||||
"invalid price quantity",
|
||||
NamedTextColor.RED));
|
||||
open(p, signBlock, plugin, newShopName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. parse price material
|
||||
Material priceMat = plugin.getSignParser()
|
||||
.parseMaterial(priceStr);
|
||||
if (priceMat == null) {
|
||||
p.sendMessage(Component.text(
|
||||
"invalid payment item: "
|
||||
+ priceStr,
|
||||
NamedTextColor.RED));
|
||||
open(p, signBlock, plugin, newShopName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. parse product qty
|
||||
int productQty;
|
||||
try {
|
||||
productQty = Integer.parseInt(
|
||||
productQtyStr);
|
||||
} catch (NumberFormatException e) {
|
||||
p.sendMessage(Component.text(
|
||||
"invalid product quantity",
|
||||
NamedTextColor.RED));
|
||||
open(p, signBlock, plugin, newShopName);
|
||||
return;
|
||||
}
|
||||
Material productMat = plugin.getSignParser()
|
||||
.parseMaterial(productStr);
|
||||
if (productMat == null) {
|
||||
p.sendMessage(Component.text(
|
||||
"invalid product: "
|
||||
+ productStr,
|
||||
NamedTextColor.RED));
|
||||
open(p, signBlock, plugin, newShopName);
|
||||
return;
|
||||
}
|
||||
Trade trade = new Trade(priceMat, priceQty,
|
||||
productMat, productQty);
|
||||
|
||||
PendingActivation activation = new PendingActivation(
|
||||
p.getUniqueId(),
|
||||
signBlock.getLocation(),
|
||||
trade,
|
||||
System.currentTimeMillis(),
|
||||
true); // cosmeticSign always
|
||||
// true
|
||||
|
||||
// 4. finalize
|
||||
plugin.getServer().getScheduler()
|
||||
.runTask(plugin, () -> {
|
||||
plugin.getShopActivationListener()
|
||||
.finalizeShop(p, activation,
|
||||
newShopName);
|
||||
});
|
||||
}, 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(
|
||||
"setup cancelled",
|
||||
NamedTextColor.YELLOW));
|
||||
}, ClickCallback.Options.builder().build()))
|
||||
.build())));
|
||||
|
||||
player.showDialog(dialog);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.DoubleChest;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -15,12 +17,13 @@ import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.DoubleChestInventory;
|
||||
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;
|
||||
@@ -36,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<Player, Shop> viewingShop = new HashMap<>();
|
||||
// track active shop sessions for players
|
||||
private final Map<Player, ShopSession> activeSessions = new HashMap<>();
|
||||
|
||||
public record ShopSession(Shop shop, int unitsTraded, Inventory realInventory) {
|
||||
}
|
||||
|
||||
public ChestInteractionListener(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
@@ -59,7 +65,7 @@ public class ChestInteractionListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if there's a shop sign attached
|
||||
// check if there's a shop sign attached to this container
|
||||
Shop shop = findShopForContainer(block);
|
||||
if (shop == null) {
|
||||
return; // not a shop container
|
||||
@@ -74,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;
|
||||
}
|
||||
|
||||
@@ -138,7 +146,7 @@ public class ChestInteractionListener implements Listener {
|
||||
// update database
|
||||
int newOwed = owed - withdrawn;
|
||||
shop.setOwedAmount(newOwed);
|
||||
plugin.getShopRepository().updateOwedAmount(shop.getId(), -withdrawn);
|
||||
plugin.getShopRepository().updateOwedAmount(shop.getId(), newOwed);
|
||||
|
||||
player.sendMessage(
|
||||
Component.text("withdrew " + withdrawn + " " + formatMaterial(priceItem), NamedTextColor.GREEN)
|
||||
@@ -154,17 +162,19 @@ public class ChestInteractionListener implements Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* open shop GUI for buyer
|
||||
* open shop gui for buyer
|
||||
*/
|
||||
private void openShopGui(Player player, Shop shop, Block containerBlock) {
|
||||
Trade trade = shop.getTrade();
|
||||
|
||||
// create fake inventory showing only product items
|
||||
Inventory shopInventory = Bukkit.createInventory(
|
||||
new ShopInventoryHolder(shop),
|
||||
27,
|
||||
Component.text("shop: " + trade.priceQuantity() + " " + formatMaterial(trade.priceItem()) +
|
||||
" → " + trade.productQuantity() + " " + formatMaterial(trade.productItem())));
|
||||
// create title
|
||||
Component title = Component.text("shop: " + trade.priceQuantity() + " " + formatMaterial(trade.priceItem()) +
|
||||
" → " + trade.productQuantity() + " " + formatMaterial(trade.productItem()));
|
||||
|
||||
// respect custom title if set
|
||||
if (shop.getCustomTitle() != null && !shop.getCustomTitle().isEmpty()) {
|
||||
title = Component.text(shop.getCustomTitle());
|
||||
}
|
||||
|
||||
// get real container inventory
|
||||
Inventory realInventory = getContainerInventory(containerBlock);
|
||||
@@ -172,18 +182,79 @@ public class ChestInteractionListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// copy only product items to fake inventory (first 27 found)
|
||||
int shopSlot = 0;
|
||||
int invSize = Math.min(54, Math.max(27, realInventory.getSize()));
|
||||
|
||||
// create fake inventory showing only product items
|
||||
Inventory shopInventory = Bukkit.createInventory(
|
||||
new ShopInventoryHolder(shop),
|
||||
invSize,
|
||||
title);
|
||||
|
||||
// populate with units (productQuantity per slot)
|
||||
int totalStock = 0;
|
||||
for (ItemStack item : realInventory.getContents()) {
|
||||
if (shopSlot >= 27)
|
||||
break;
|
||||
if (item != null && item.getType() == trade.productItem()) {
|
||||
shopInventory.setItem(shopSlot++, item.clone());
|
||||
if (item != null && trade.matchesProduct(item.getType())) {
|
||||
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
|
||||
String discName = shop.getDisc();
|
||||
if (discName != null && !discName.isEmpty()) {
|
||||
playDisc(player, discName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* play a music disc for a player
|
||||
*/
|
||||
private void playDisc(Player player, String discName) {
|
||||
Sound discSound = getDiscSound(discName);
|
||||
if (discSound != null) {
|
||||
player.playSound(player.getLocation(), discSound, 1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* stop a music disc for a player
|
||||
*/
|
||||
private void stopDisc(Player player, String discName) {
|
||||
Sound discSound = getDiscSound(discName);
|
||||
if (discSound != null) {
|
||||
player.stopSound(discSound);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get Sound enum for disc name
|
||||
*/
|
||||
private Sound getDiscSound(String discName) {
|
||||
return switch (discName.toLowerCase()) {
|
||||
case "blocks" -> Sound.MUSIC_DISC_BLOCKS;
|
||||
case "chirp" -> Sound.MUSIC_DISC_CHIRP;
|
||||
case "far" -> Sound.MUSIC_DISC_FAR;
|
||||
case "mall" -> Sound.MUSIC_DISC_MALL;
|
||||
case "mellohi" -> Sound.MUSIC_DISC_MELLOHI;
|
||||
case "stal" -> Sound.MUSIC_DISC_STAL;
|
||||
case "strad" -> Sound.MUSIC_DISC_STRAD;
|
||||
case "ward" -> Sound.MUSIC_DISC_WARD;
|
||||
case "wait" -> Sound.MUSIC_DISC_WAIT;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -192,84 +263,189 @@ 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();
|
||||
if (clicked != null && clicked.getType() == shop.getTrade().productItem()) {
|
||||
// determine quantity based on click type
|
||||
int units = 1;
|
||||
if (event.isShiftClick()) {
|
||||
// shift-click: calculate max units based on items clicked
|
||||
units = clicked.getAmount() / shop.getTrade().productQuantity();
|
||||
if (units < 1)
|
||||
units = 1;
|
||||
}
|
||||
|
||||
// open confirmation GUI
|
||||
player.closeInventory();
|
||||
openConfirmationGui(player, shop, units);
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// stop disc
|
||||
String discName = shop.getDisc();
|
||||
if (discName != null && !discName.isEmpty()) {
|
||||
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
|
||||
* find shop attached to a container block (handles double chests)
|
||||
*/
|
||||
private Shop findShopForContainer(Block containerBlock) {
|
||||
// first check this block directly
|
||||
Shop shop = findShopOnBlock(containerBlock);
|
||||
if (shop != null) {
|
||||
return shop;
|
||||
}
|
||||
|
||||
// if this is a chest, check the other half of a double chest
|
||||
if (containerBlock.getState() instanceof Chest chest) {
|
||||
Inventory inv = chest.getInventory();
|
||||
if (inv instanceof DoubleChestInventory doubleInv) {
|
||||
DoubleChest doubleChest = doubleInv.getHolder();
|
||||
if (doubleChest != null) {
|
||||
// check both sides
|
||||
Chest left = (Chest) doubleChest.getLeftSide();
|
||||
Chest right = (Chest) doubleChest.getRightSide();
|
||||
|
||||
if (left != null) {
|
||||
shop = findShopOnBlock(left.getBlock());
|
||||
if (shop != null)
|
||||
return shop;
|
||||
}
|
||||
if (right != null) {
|
||||
shop = findShopOnBlock(right.getBlock());
|
||||
if (shop != null)
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* find shop sign on a specific block
|
||||
*/
|
||||
private Shop findShopOnBlock(Block containerBlock) {
|
||||
// check all adjacent blocks for a wall sign pointing at this container
|
||||
for (BlockFace face : new BlockFace[] { BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST }) {
|
||||
Block adjacent = containerBlock.getRelative(face);
|
||||
@@ -287,11 +463,12 @@ public class ChestInteractionListener implements Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* get container inventory
|
||||
* get container inventory (handles double chests to return full 54 slot
|
||||
* inventory)
|
||||
*/
|
||||
private Inventory getContainerInventory(Block block) {
|
||||
if (block.getState() instanceof Chest chest) {
|
||||
return chest.getInventory();
|
||||
return chest.getInventory(); // returns DoubleChestInventory if double chest
|
||||
} else if (block.getState() instanceof Barrel barrel) {
|
||||
return barrel.getInventory();
|
||||
}
|
||||
|
||||
@@ -39,28 +39,21 @@ public class LoginListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// find all shops owned by player
|
||||
List<Shop> 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<Shop> 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) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import party.cybsec.oyeshops.gui.SetupDialog;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
@@ -90,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));
|
||||
@@ -114,6 +116,21 @@ public class ShopActivationListener implements Listener {
|
||||
: "";
|
||||
}
|
||||
|
||||
// check for setup wizard
|
||||
if (lines[0].equalsIgnoreCase("setup") && lines[1].isEmpty() && lines[2].isEmpty() && lines[3].isEmpty()) {
|
||||
// check if player has use permission (needed to activate shops)
|
||||
if (!PermissionManager.canUse(player)) {
|
||||
player.sendMessage(
|
||||
Component.text("you need oyeshops.use permission to use the setup wizard", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
// clear sign text to avoid "setup" staying on the sign
|
||||
event.line(0, Component.text(""));
|
||||
SetupDialog.open(player, block, plugin, "shop-" + (plugin.getShopRegistry().getAllShops().size() + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
// parse trade
|
||||
Trade trade = parser.parse(lines);
|
||||
if (trade == null) {
|
||||
@@ -132,7 +149,7 @@ public class ShopActivationListener implements Listener {
|
||||
|
||||
// trigger confirmation instead of immediate creation
|
||||
PendingActivation activation = new PendingActivation(player.getUniqueId(), block.getLocation(), trade,
|
||||
System.currentTimeMillis());
|
||||
System.currentTimeMillis(), false);
|
||||
plugin.getActivationManager().add(player.getUniqueId(), activation);
|
||||
|
||||
sendConfirmationMessage(player, trade);
|
||||
@@ -141,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));
|
||||
@@ -153,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);
|
||||
}
|
||||
@@ -172,33 +199,46 @@ 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();
|
||||
|
||||
// verify it's still a sign
|
||||
if (!(block.getState() instanceof Sign sign)) {
|
||||
if (!(block.getState() instanceof Sign)) {
|
||||
player.sendMessage(Component.text("activation failed: sign is gone", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
Trade trade = activation.trade();
|
||||
long createdAt = System.currentTimeMillis();
|
||||
Shop shop = new Shop(-1, signLocation, player.getUniqueId(), trade, 0, true, createdAt);
|
||||
// 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 {
|
||||
// cleanup potential existing shop to avoid unique constraint error
|
||||
plugin.getShopRepository().deleteShopByLocation(signLocation);
|
||||
|
||||
int shopId = plugin.getShopRepository().createShop(shop);
|
||||
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);
|
||||
Shop registeredShop = new Shop(shopId, shopName, signLocation, player.getUniqueId(),
|
||||
new ArrayList<>(), trade, 0, true, createdAt, null, true, null);
|
||||
plugin.getShopRegistry().register(registeredShop);
|
||||
rewriteSignLines(finalSign, trade);
|
||||
player.sendMessage(Component.text("shop #" + shopId + " initialized!", NamedTextColor.GREEN));
|
||||
plugin.getLogger().info("DEBUG: registered shop " + shopId + " in registry");
|
||||
|
||||
// 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, () -> {
|
||||
@@ -209,15 +249,8 @@ public class ShopActivationListener implements Listener {
|
||||
});
|
||||
}
|
||||
|
||||
private void rewriteSignLines(Sign sign, Trade trade) {
|
||||
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();
|
||||
public void rewriteSignLines(Sign sign, Shop shop) {
|
||||
// REMOVED: Sign should just be left alone as requested
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,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();
|
||||
@@ -339,7 +363,7 @@ public class ShopActivationListener implements Listener {
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isContainer(Material material) {
|
||||
public static boolean isContainer(Material material) {
|
||||
return material == Material.CHEST || material == Material.TRAPPED_CHEST || material == Material.BARREL;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.DoubleChest;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.inventory.DoubleChestInventory;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.permission.PermissionManager;
|
||||
@@ -49,7 +54,7 @@ public class ShopProtectionListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// find shop with this chest location
|
||||
// find shop with this chest location (handles double chests)
|
||||
Shop chestShop = findShopByChest(block);
|
||||
if (chestShop != null) {
|
||||
if (canBreakShop(player, chestShop)) {
|
||||
@@ -88,17 +93,54 @@ public class ShopProtectionListener implements Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* find shop by chest location
|
||||
* find shop by chest location (handles double chests)
|
||||
*/
|
||||
private Shop findShopByChest(Block chestBlock) {
|
||||
// first check this block directly
|
||||
Shop shop = findShopOnBlock(chestBlock);
|
||||
if (shop != null) {
|
||||
return shop;
|
||||
}
|
||||
|
||||
// if this is a chest, check the other half of a double chest
|
||||
if (chestBlock.getState() instanceof Chest chest) {
|
||||
Inventory inv = chest.getInventory();
|
||||
if (inv instanceof DoubleChestInventory doubleInv) {
|
||||
DoubleChest doubleChest = doubleInv.getHolder();
|
||||
if (doubleChest != null) {
|
||||
// check both sides
|
||||
Chest left = (Chest) doubleChest.getLeftSide();
|
||||
Chest right = (Chest) doubleChest.getRightSide();
|
||||
|
||||
if (left != null) {
|
||||
shop = findShopOnBlock(left.getBlock());
|
||||
if (shop != null)
|
||||
return shop;
|
||||
}
|
||||
if (right != null) {
|
||||
shop = findShopOnBlock(right.getBlock());
|
||||
if (shop != null)
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* find shop sign attached to a specific block
|
||||
*/
|
||||
private Shop findShopOnBlock(Block containerBlock) {
|
||||
// check all adjacent blocks for wall signs
|
||||
for (org.bukkit.block.BlockFace face : new org.bukkit.block.BlockFace[] {
|
||||
org.bukkit.block.BlockFace.NORTH,
|
||||
org.bukkit.block.BlockFace.SOUTH,
|
||||
org.bukkit.block.BlockFace.EAST,
|
||||
org.bukkit.block.BlockFace.WEST
|
||||
for (BlockFace face : new BlockFace[] {
|
||||
BlockFace.NORTH,
|
||||
BlockFace.SOUTH,
|
||||
BlockFace.EAST,
|
||||
BlockFace.WEST
|
||||
}) {
|
||||
Block adjacent = chestBlock.getRelative(face);
|
||||
Block adjacent = containerBlock.getRelative(face);
|
||||
Shop shop = plugin.getShopRegistry().getShop(adjacent.getLocation());
|
||||
if (shop != null) {
|
||||
return shop;
|
||||
|
||||
@@ -10,7 +10,8 @@ public record PendingActivation(
|
||||
UUID owner,
|
||||
Location location,
|
||||
Trade trade,
|
||||
long createdAt) {
|
||||
long createdAt,
|
||||
boolean cosmeticSign) {
|
||||
|
||||
/**
|
||||
* check if this activation has expired
|
||||
@@ -28,6 +29,6 @@ public record PendingActivation(
|
||||
trade.productQuantity(),
|
||||
trade.priceItem(),
|
||||
trade.priceQuantity());
|
||||
return new PendingActivation(owner, location, invertedTrade, createdAt);
|
||||
return new PendingActivation(owner, location, invertedTrade, createdAt, cosmeticSign);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package party.cybsec.oyeshops.model;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -9,22 +10,44 @@ 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<UUID> contributors;
|
||||
private final Trade trade;
|
||||
private int owedAmount;
|
||||
private boolean enabled;
|
||||
private final long createdAt;
|
||||
private String customTitle;
|
||||
private boolean cosmeticSign;
|
||||
private String disc;
|
||||
|
||||
public Shop(int id, Location signLocation, UUID owner, Trade trade, int owedAmount, boolean enabled,
|
||||
long createdAt) {
|
||||
public Shop(int id, String name, Location signLocation, UUID owner, List<UUID> 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;
|
||||
this.createdAt = createdAt;
|
||||
this.customTitle = customTitle;
|
||||
this.cosmeticSign = cosmeticSign;
|
||||
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() {
|
||||
@@ -63,12 +86,34 @@ public class Shop {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getCustomTitle() {
|
||||
return customTitle;
|
||||
}
|
||||
|
||||
public void setCustomTitle(String customTitle) {
|
||||
this.customTitle = customTitle;
|
||||
}
|
||||
|
||||
public boolean isCosmeticSign() {
|
||||
return cosmeticSign;
|
||||
}
|
||||
|
||||
public void setCosmeticSign(boolean cosmeticSign) {
|
||||
this.cosmeticSign = cosmeticSign;
|
||||
}
|
||||
|
||||
public String getDisc() {
|
||||
return disc;
|
||||
}
|
||||
|
||||
public void setDisc(String disc) {
|
||||
this.disc = disc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.bukkit.Material;
|
||||
|
||||
/**
|
||||
* immutable trade definition
|
||||
* quantity of -1 indicates AUTO detection needed for that side
|
||||
* quantity of -1 indicates auto detection needed for that side
|
||||
*/
|
||||
public record Trade(
|
||||
Material priceItem,
|
||||
@@ -12,38 +12,46 @@ public record Trade(
|
||||
Material productItem,
|
||||
int productQuantity) {
|
||||
|
||||
// primary constructor
|
||||
public Trade {
|
||||
// allow -1 for AUTO detection, but otherwise must be positive
|
||||
// allow -1 for auto detection, but otherwise must be positive
|
||||
if (priceQuantity <= 0 && priceQuantity != -1) {
|
||||
throw new IllegalArgumentException("price quantity must be positive or -1 for auto");
|
||||
}
|
||||
if (productQuantity <= 0 && productQuantity != -1) {
|
||||
throw new IllegalArgumentException("product quantity must be positive or -1 for auto");
|
||||
}
|
||||
// for AUTO, materials might be AIR (unknown)
|
||||
// for auto, materials might be air (unknown)
|
||||
if (priceQuantity != -1 && productQuantity != -1 && priceItem == productItem && priceItem != Material.AIR) {
|
||||
throw new IllegalArgumentException("price and product must be different materials");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if this trade needs AUTO detection on the product side
|
||||
* check if this trade needs auto detection on the product side
|
||||
*/
|
||||
public boolean isAutoProduct() {
|
||||
return productQuantity == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if this trade needs AUTO detection on the price side
|
||||
* check if this trade needs auto detection on the price side
|
||||
*/
|
||||
public boolean isAutoPrice() {
|
||||
return priceQuantity == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if this trade needs any AUTO detection
|
||||
* check if this trade needs any auto detection
|
||||
*/
|
||||
public boolean isAutoDetect() {
|
||||
return isAutoProduct() || isAutoPrice();
|
||||
}
|
||||
|
||||
/**
|
||||
* check if a material matches this trade's product
|
||||
*/
|
||||
public boolean matchesProduct(Material material) {
|
||||
return productItem == material;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class MaterialAliasRegistry {
|
||||
|
||||
// short forms
|
||||
Map.entry("dia", "diamond"),
|
||||
Map.entry("d", "diamond"),
|
||||
Map.entry("dias", "diamond"),
|
||||
Map.entry("em", "emerald"),
|
||||
Map.entry("ems", "emerald"),
|
||||
@@ -133,6 +134,7 @@ public class MaterialAliasRegistry {
|
||||
Map.entry("netherrack", "netherrack"),
|
||||
Map.entry("endstone", "end_stone"),
|
||||
Map.entry("end stone", "end_stone"),
|
||||
Map.entry("bamboo block", "bamboo_block"),
|
||||
|
||||
// food
|
||||
Map.entry("steak", "cooked_beef"),
|
||||
@@ -202,13 +204,13 @@ public class MaterialAliasRegistry {
|
||||
|
||||
/**
|
||||
* resolve material from normalized text
|
||||
* tries multiple strategies:
|
||||
* 1. exact alias match (longest first)
|
||||
* 2. word-by-word alias match
|
||||
* 3. space-to-underscore conversion for direct enum match
|
||||
* 4. direct material enum match
|
||||
* 5. with _INGOT/_BLOCK suffixes
|
||||
* 6. strip trailing 's' for plurals
|
||||
* priority order:
|
||||
* 1. exact full text match (space to underscore) - handles "bamboo block" ->
|
||||
* BAMBOO_BLOCK
|
||||
* 2. exact alias match (longest first for multi-word aliases)
|
||||
* 3. direct material enum match for each word
|
||||
* 4. with _INGOT/_BLOCK suffixes
|
||||
* 5. strip trailing 's' for plurals
|
||||
*/
|
||||
public Material resolve(String text) {
|
||||
text = text.toLowerCase().trim();
|
||||
@@ -217,13 +219,25 @@ public class MaterialAliasRegistry {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. try longest alias match first (for multi-word aliases)
|
||||
// remove numbers and extra whitespace for material matching
|
||||
String materialText = text.replaceAll("\\d+", "").trim().replaceAll("\\s+", " ");
|
||||
|
||||
// 1. try exact full text match with underscore conversion first
|
||||
// this handles "bamboo block" -> "bamboo_block" -> BAMBOO_BLOCK
|
||||
String underscored = materialText.replace(" ", "_");
|
||||
try {
|
||||
return Material.valueOf(underscored.toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
// 2. try exact alias match (longest first for multi-word aliases)
|
||||
String longestMatch = null;
|
||||
Material longestMaterial = null;
|
||||
|
||||
for (Map.Entry<String, Material> entry : aliases.entrySet()) {
|
||||
String alias = entry.getKey();
|
||||
if (text.contains(alias)) {
|
||||
// check if the text equals or contains the alias
|
||||
if (materialText.equals(alias) || materialText.contains(alias)) {
|
||||
if (longestMatch == null || alias.length() > longestMatch.length()) {
|
||||
longestMatch = alias;
|
||||
longestMaterial = entry.getValue();
|
||||
@@ -235,30 +249,19 @@ public class MaterialAliasRegistry {
|
||||
return longestMaterial;
|
||||
}
|
||||
|
||||
// 2. try word-by-word alias match
|
||||
String[] words = text.split("\\s+");
|
||||
// 3. try each word directly as material
|
||||
String[] words = materialText.split("\\s+");
|
||||
for (String word : words) {
|
||||
if (aliases.containsKey(word)) {
|
||||
return aliases.get(word);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. try space-to-underscore conversion for multi-word materials
|
||||
// e.g., "netherite pickaxe" -> "netherite_pickaxe"
|
||||
String underscored = text.replace(" ", "_");
|
||||
try {
|
||||
return Material.valueOf(underscored.toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
// 4. try each word directly as material
|
||||
for (String word : words) {
|
||||
try {
|
||||
return Material.valueOf(word.toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
// 5. try with common suffixes
|
||||
// 4. try with common suffixes
|
||||
try {
|
||||
return Material.valueOf(word.toUpperCase() + "_INGOT");
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
@@ -269,7 +272,7 @@ public class MaterialAliasRegistry {
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
// 6. try stripping trailing 's' for plurals
|
||||
// 5. try stripping trailing 's' for plurals
|
||||
if (word.endsWith("s") && word.length() > 1) {
|
||||
String singular = word.substring(0, word.length() - 1);
|
||||
try {
|
||||
@@ -279,19 +282,30 @@ public class MaterialAliasRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// 7. try the whole text with underscores for complex names
|
||||
// 6. last resort: scan all materials for longest match
|
||||
Material bestMatch = null;
|
||||
int bestLength = 0;
|
||||
|
||||
for (Material material : Material.values()) {
|
||||
String materialName = material.name().toLowerCase();
|
||||
String materialSpaced = materialName.replace("_", " ");
|
||||
|
||||
if (text.contains(materialSpaced) || text.contains(materialName)) {
|
||||
if (longestMatch == null || materialName.length() > longestMatch.length()) {
|
||||
longestMatch = materialName;
|
||||
longestMaterial = material;
|
||||
if (materialText.equals(materialSpaced) || materialText.equals(materialName)) {
|
||||
return material; // exact match
|
||||
}
|
||||
|
||||
if (materialText.contains(materialSpaced) || materialText.contains(materialName)) {
|
||||
if (materialName.length() > bestLength) {
|
||||
bestLength = materialName.length();
|
||||
bestMatch = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longestMaterial;
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
public Material parseMaterial(String name) {
|
||||
return resolve(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,19 +50,14 @@ public class SignParser {
|
||||
|
||||
/**
|
||||
* parse sign lines into a trade
|
||||
*
|
||||
*
|
||||
* @return trade if valid, null if invalid or ambiguous
|
||||
* trade with quantity -1 means AUTO detection needed
|
||||
* trade with quantity -1 means auto detection needed
|
||||
*/
|
||||
public Trade parse(String[] lines) {
|
||||
// concatenate all lines with spaces
|
||||
String fullText = String.join(" ", lines);
|
||||
|
||||
// REQUIREMENT: must contain "." to be parsed as a shop
|
||||
if (!fullText.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// normalize text
|
||||
String normalized = normalize(fullText);
|
||||
|
||||
@@ -79,6 +74,7 @@ public class SignParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check for auto detection
|
||||
boolean isAuto = productSection.toLowerCase().contains(AUTO_KEYWORD);
|
||||
ItemQuantity product;
|
||||
if (isAuto) {
|
||||
@@ -259,4 +255,8 @@ public class SignParser {
|
||||
|
||||
private record ItemQuantity(Material material, int quantity) {
|
||||
}
|
||||
|
||||
public Material parseMaterial(String name) {
|
||||
return aliasRegistry.resolve(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -97,6 +125,25 @@ public class ShopRegistry {
|
||||
shopsById.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* find shop by container block (checks adjacent faces for signs)
|
||||
*/
|
||||
public Shop getShopByContainer(org.bukkit.block.Block container) {
|
||||
for (org.bukkit.block.BlockFace face : new org.bukkit.block.BlockFace[] {
|
||||
org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH,
|
||||
org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST }) {
|
||||
org.bukkit.block.Block adjacent = container.getRelative(face);
|
||||
if (adjacent.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||
if (wallSign.getFacing().getOppositeFace() == face.getOppositeFace()) {
|
||||
Shop shop = getShop(adjacent.getLocation());
|
||||
if (shop != null)
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* create location key for map lookup
|
||||
*/
|
||||
|
||||
@@ -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<Integer, ItemStack> overflow = buyer.getInventory().addItem(
|
||||
createItemStacks(trade.productItem(), totalProduct));
|
||||
HashMap<Integer, ItemStack> overflow = buyer.getInventory().addItem(productItems);
|
||||
|
||||
if (!overflow.isEmpty()) {
|
||||
// rollback
|
||||
@@ -97,22 +131,7 @@ public class TransactionManager {
|
||||
return Result.INVENTORY_FULL;
|
||||
}
|
||||
|
||||
// update owed amount in database
|
||||
plugin.getShopRepository().updateOwedAmount(shop.getId(), totalPrice);
|
||||
shop.setOwedAmount(shop.getOwedAmount() + totalPrice);
|
||||
|
||||
// record transaction
|
||||
plugin.getTransactionRepository().recordTransaction(
|
||||
shop.getId(),
|
||||
buyer.getUniqueId(),
|
||||
units);
|
||||
|
||||
// prune old transactions if configured
|
||||
if (plugin.getConfigManager().isAutoPrune()) {
|
||||
int maxHistory = plugin.getConfigManager().getMaxTransactionsPerShop();
|
||||
plugin.getTransactionRepository().pruneTransactions(shop.getId(), maxHistory);
|
||||
}
|
||||
|
||||
finalizeTransaction(buyer, shop, units, totalPrice);
|
||||
return Result.SUCCESS;
|
||||
|
||||
} catch (SQLException e) {
|
||||
@@ -124,10 +143,28 @@ public class TransactionManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizeTransaction(Player buyer, Shop shop, int units, int totalPrice) throws SQLException {
|
||||
// update owed amount in database
|
||||
plugin.getShopRepository().updateOwedAmount(shop.getId(), shop.getOwedAmount() + totalPrice);
|
||||
shop.setOwedAmount(shop.getOwedAmount() + totalPrice);
|
||||
|
||||
// record transaction
|
||||
plugin.getTransactionRepository().recordTransaction(
|
||||
shop.getId(),
|
||||
buyer != null ? buyer.getUniqueId() : null,
|
||||
units);
|
||||
|
||||
// prune old transactions if configured
|
||||
if (plugin.getConfigManager().isAutoPrune()) {
|
||||
int maxHistory = plugin.getConfigManager().getMaxTransactionsPerShop();
|
||||
plugin.getTransactionRepository().pruneTransactions(shop.getId(), maxHistory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
@@ -141,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<ItemStack> 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 {
|
||||
@@ -158,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;
|
||||
@@ -185,7 +247,7 @@ public class TransactionManager {
|
||||
/**
|
||||
* get the shop's container inventory
|
||||
*/
|
||||
private Inventory getShopInventory(Shop shop) {
|
||||
public Inventory getShopInventory(Shop shop) {
|
||||
Location signLoc = shop.getSignLocation();
|
||||
Block signBlock = signLoc.getBlock();
|
||||
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
# hopper protection
|
||||
hoppers:
|
||||
allow-product-output: true
|
||||
block-price-input: true
|
||||
# oyeShops configuration
|
||||
|
||||
# transaction history
|
||||
history:
|
||||
max-transactions-per-shop: 100
|
||||
auto-prune: true
|
||||
# transaction settings
|
||||
transactions:
|
||||
auto-prune: false
|
||||
max-per-shop: 100
|
||||
|
||||
# material aliases
|
||||
# whether to require players to have placed the container themselves this session
|
||||
require-placement: true
|
||||
|
||||
# custom material aliases (add your own shortcuts)
|
||||
aliases:
|
||||
# common abbreviations
|
||||
dia: diamond
|
||||
dias: diamond
|
||||
iron: iron_ingot
|
||||
gold: gold_ingot
|
||||
emerald: emerald
|
||||
ems: emerald
|
||||
|
||||
# blocks
|
||||
stone: stone
|
||||
dirt: dirt
|
||||
cobble: cobblestone
|
||||
|
||||
# tools
|
||||
pick: diamond_pickaxe
|
||||
sword: diamond_sword
|
||||
axe: diamond_axe
|
||||
# example:
|
||||
# myalias: diamond
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: oyeShops
|
||||
version: 1.0.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 <on|off|toggle|reload|inspect|spoof|enable|disable|unregister>
|
||||
usage: /oyeshops <on|off|toggle|reload|setup|config|tpshop|toggle-placement>
|
||||
aliases: [oyes, oshop]
|
||||
|
||||
permissions:
|
||||
|
||||
Reference in New Issue
Block a user