forgot to do small contribs lmao
This commit is contained in:
182
src/main/java/party/cybsec/oyeshops/OyeShopsPlugin.java
Normal file
182
src/main/java/party/cybsec/oyeshops/OyeShopsPlugin.java
Normal file
@@ -0,0 +1,182 @@
|
||||
package party.cybsec.oyeshops;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import party.cybsec.oyeshops.admin.InspectModeManager;
|
||||
import party.cybsec.oyeshops.admin.SpoofManager;
|
||||
import party.cybsec.oyeshops.command.AdminCommands;
|
||||
import party.cybsec.oyeshops.config.ConfigManager;
|
||||
import party.cybsec.oyeshops.database.DatabaseManager;
|
||||
import party.cybsec.oyeshops.database.PlayerPreferenceRepository;
|
||||
import party.cybsec.oyeshops.database.ShopRepository;
|
||||
import party.cybsec.oyeshops.database.TransactionRepository;
|
||||
import party.cybsec.oyeshops.listener.*;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.parser.MaterialAliasRegistry;
|
||||
import party.cybsec.oyeshops.parser.SignParser;
|
||||
import party.cybsec.oyeshops.registry.ShopRegistry;
|
||||
import party.cybsec.oyeshops.transaction.TransactionManager;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* main plugin class
|
||||
*/
|
||||
public class OyeShopsPlugin extends JavaPlugin {
|
||||
// managers
|
||||
private ConfigManager configManager;
|
||||
private DatabaseManager databaseManager;
|
||||
private ShopRepository shopRepository;
|
||||
private TransactionRepository transactionRepository;
|
||||
private PlayerPreferenceRepository playerPreferenceRepository;
|
||||
private ShopRegistry shopRegistry;
|
||||
private MaterialAliasRegistry aliasRegistry;
|
||||
private SignParser signParser;
|
||||
private TransactionManager transactionManager;
|
||||
private InspectModeManager inspectModeManager;
|
||||
private SpoofManager spoofManager;
|
||||
private party.cybsec.oyeshops.manager.ActivationManager activationManager;
|
||||
private party.cybsec.oyeshops.manager.ContainerMemoryManager containerMemoryManager;
|
||||
private ShopActivationListener shopActivationListener;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("initializing oyeShops...");
|
||||
|
||||
try {
|
||||
// initialize config
|
||||
configManager = new ConfigManager(this);
|
||||
|
||||
// initialize database
|
||||
databaseManager = new DatabaseManager(getDataFolder());
|
||||
databaseManager.initialize();
|
||||
|
||||
// initialize repositories
|
||||
shopRepository = new ShopRepository(databaseManager);
|
||||
transactionRepository = new TransactionRepository(databaseManager);
|
||||
playerPreferenceRepository = new PlayerPreferenceRepository(databaseManager);
|
||||
|
||||
// load player preferences cache
|
||||
playerPreferenceRepository.loadPreferences();
|
||||
|
||||
// initialize registry
|
||||
shopRegistry = new ShopRegistry();
|
||||
|
||||
// initialize parsers
|
||||
aliasRegistry = new MaterialAliasRegistry(configManager.getConfig().getConfigurationSection("aliases"));
|
||||
signParser = new SignParser(aliasRegistry);
|
||||
|
||||
// initialize managers
|
||||
transactionManager = new TransactionManager(this);
|
||||
inspectModeManager = new InspectModeManager();
|
||||
spoofManager = new SpoofManager();
|
||||
activationManager = new party.cybsec.oyeshops.manager.ActivationManager();
|
||||
containerMemoryManager = new party.cybsec.oyeshops.manager.ContainerMemoryManager();
|
||||
shopActivationListener = new ShopActivationListener(this, signParser);
|
||||
|
||||
// load existing shops from database
|
||||
loadShops();
|
||||
|
||||
// register listeners
|
||||
registerListeners();
|
||||
|
||||
// register commands
|
||||
registerCommands();
|
||||
|
||||
getLogger().info("oyeShops enabled successfully!");
|
||||
|
||||
} catch (SQLException e) {
|
||||
getLogger().severe("failed to initialize database: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getLogger().info("shutting down oyeShops...");
|
||||
|
||||
// close database connection
|
||||
if (databaseManager != null) {
|
||||
databaseManager.close();
|
||||
}
|
||||
|
||||
getLogger().info("oyeShops disabled");
|
||||
}
|
||||
|
||||
private void loadShops() throws SQLException {
|
||||
List<Shop> shops = shopRepository.loadAllShops();
|
||||
for (Shop shop : shops) {
|
||||
shopRegistry.register(shop);
|
||||
}
|
||||
getLogger().info("loaded " + shops.size() + " shops from database");
|
||||
}
|
||||
|
||||
private void registerListeners() {
|
||||
getServer().getPluginManager().registerEvents(shopActivationListener, this);
|
||||
getServer().getPluginManager().registerEvents(new ShopProtectionListener(this), this);
|
||||
getServer().getPluginManager().registerEvents(new ChestInteractionListener(this), this);
|
||||
getServer().getPluginManager().registerEvents(new InspectListener(this), this);
|
||||
getServer().getPluginManager().registerEvents(new LoginListener(this), this);
|
||||
getServer().getPluginManager().registerEvents(new ContainerPlacementListener(this), this);
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
AdminCommands adminCommands = new AdminCommands(this);
|
||||
getCommand("oyeshops").setExecutor(adminCommands);
|
||||
getCommand("oyeshops").setTabCompleter(adminCommands);
|
||||
}
|
||||
|
||||
// getters for managers
|
||||
public ConfigManager getConfigManager() {
|
||||
return configManager;
|
||||
}
|
||||
|
||||
public ShopRepository getShopRepository() {
|
||||
return shopRepository;
|
||||
}
|
||||
|
||||
public TransactionRepository getTransactionRepository() {
|
||||
return transactionRepository;
|
||||
}
|
||||
|
||||
public PlayerPreferenceRepository getPlayerPreferenceRepository() {
|
||||
return playerPreferenceRepository;
|
||||
}
|
||||
|
||||
public ShopRegistry getShopRegistry() {
|
||||
return shopRegistry;
|
||||
}
|
||||
|
||||
public TransactionManager getTransactionManager() {
|
||||
return transactionManager;
|
||||
}
|
||||
|
||||
public InspectModeManager getInspectModeManager() {
|
||||
return inspectModeManager;
|
||||
}
|
||||
|
||||
public SpoofManager getSpoofManager() {
|
||||
return spoofManager;
|
||||
}
|
||||
|
||||
public party.cybsec.oyeshops.manager.ActivationManager getActivationManager() {
|
||||
return activationManager;
|
||||
}
|
||||
|
||||
public party.cybsec.oyeshops.manager.ContainerMemoryManager getContainerMemoryManager() {
|
||||
return containerMemoryManager;
|
||||
}
|
||||
|
||||
public ShopActivationListener getShopActivationListener() {
|
||||
return shopActivationListener;
|
||||
}
|
||||
|
||||
public SignParser getSignParser() {
|
||||
return signParser;
|
||||
}
|
||||
|
||||
public MaterialAliasRegistry getAliasRegistry() {
|
||||
return aliasRegistry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package party.cybsec.oyeshops.admin;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* toggles inspect mode for admins
|
||||
*/
|
||||
public class InspectModeManager {
|
||||
private final Set<UUID> inspectMode = new HashSet<>();
|
||||
|
||||
/**
|
||||
* toggle inspect mode for player
|
||||
*
|
||||
* @return true if now inspecting, false if stopped
|
||||
*/
|
||||
public boolean toggle(Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
if (inspectMode.contains(uuid)) {
|
||||
inspectMode.remove(uuid);
|
||||
return false;
|
||||
} else {
|
||||
inspectMode.add(uuid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player is in inspect mode
|
||||
*/
|
||||
public boolean isInspecting(Player player) {
|
||||
return inspectMode.contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* enable inspect mode
|
||||
*/
|
||||
public void enable(Player player) {
|
||||
inspectMode.add(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* disable inspect mode
|
||||
*/
|
||||
public void disable(Player player) {
|
||||
inspectMode.remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
46
src/main/java/party/cybsec/oyeshops/admin/SpoofManager.java
Normal file
46
src/main/java/party/cybsec/oyeshops/admin/SpoofManager.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package party.cybsec.oyeshops.admin;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* manages admin spoof mode
|
||||
* when spoofing, admin is treated as non-owner for their shops
|
||||
* allows testing purchases from own shops
|
||||
*/
|
||||
public class SpoofManager {
|
||||
private final Set<UUID> spoofing = new HashSet<>();
|
||||
|
||||
/**
|
||||
* check if player is in spoof mode
|
||||
*/
|
||||
public boolean isSpoofing(Player player) {
|
||||
return spoofing.contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle spoof mode for player
|
||||
*
|
||||
* @return true if now spoofing, false if stopped spoofing
|
||||
*/
|
||||
public boolean toggle(Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
if (spoofing.contains(uuid)) {
|
||||
spoofing.remove(uuid);
|
||||
return false;
|
||||
} else {
|
||||
spoofing.add(uuid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* disable spoof mode for player
|
||||
*/
|
||||
public void disable(Player player) {
|
||||
spoofing.remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
368
src/main/java/party/cybsec/oyeshops/command/AdminCommands.java
Normal file
368
src/main/java/party/cybsec/oyeshops/command/AdminCommands.java
Normal file
@@ -0,0 +1,368 @@
|
||||
package party.cybsec.oyeshops.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
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 java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* handle /oyeshops commands for both admins and players
|
||||
*/
|
||||
public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
private final OyeShopsPlugin plugin;
|
||||
|
||||
public AdminCommands(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
sendHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
String subCommand = args[0].toLowerCase();
|
||||
|
||||
switch (subCommand) {
|
||||
case "on", "enable" -> handleEnable(sender, args);
|
||||
case "off", "disable" -> handleDisable(sender, args);
|
||||
case "toggle" -> handleToggle(sender);
|
||||
case "notify" -> handleNotifyToggle(sender);
|
||||
case "info" -> handleInfo(sender);
|
||||
case "reload" -> handleReload(sender);
|
||||
case "inspect", "i" -> handleInspect(sender);
|
||||
case "spoof", "s" -> handleSpoof(sender);
|
||||
case "unregister", "delete", "remove" -> handleUnregister(sender, args);
|
||||
case "_activate" -> handleActivate(sender, args);
|
||||
default -> sendHelp(sender);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sendHelp(CommandSender sender) {
|
||||
sender.sendMessage(Component.text("=== oyeshops commands ===", NamedTextColor.GOLD));
|
||||
sender.sendMessage(Component.text("/oyeshops on", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - enable shop creation", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops off", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - disable shop creation", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops toggle", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - toggle shop creation", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops notify", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - toggle low stock login notifications", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops info", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - plugin information", NamedTextColor.GRAY)));
|
||||
|
||||
if (PermissionManager.isAdmin(sender)) {
|
||||
sender.sendMessage(Component.text("/oyeshops reload", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - reload config", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops inspect", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - toggle inspect mode", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops spoof", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - toggle spoof mode", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops enable <id>", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - enable a shop", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops disable <id>", NamedTextColor.YELLOW)
|
||||
.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)));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInfo(CommandSender sender) {
|
||||
sender.sendMessage(Component.text("cybsec made this plugin", NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
private void handleReload(CommandSender sender) {
|
||||
if (!PermissionManager.isAdmin(sender)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
plugin.getConfigManager().reload();
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
sender.sendMessage(Component.text("config reloaded", NamedTextColor.GREEN));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void handleInspect(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("players only", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.canInspect(player)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean nowInspecting = plugin.getInspectModeManager().toggle(player);
|
||||
if (nowInspecting) {
|
||||
player.sendMessage(
|
||||
Component.text("inspect mode enabled - right-click shops to view info", NamedTextColor.GREEN));
|
||||
} else {
|
||||
player.sendMessage(Component.text("inspect mode disabled", NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSpoof(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("players only", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.isAdmin(player)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean nowSpoofing = plugin.getSpoofManager().toggle(player);
|
||||
if (nowSpoofing) {
|
||||
player.sendMessage(
|
||||
Component.text("spoof mode enabled - you can now buy from your own shops", NamedTextColor.GREEN));
|
||||
} else {
|
||||
player.sendMessage(Component.text("spoof mode disabled", NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleToggle(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("players only", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
boolean nowEnabled = plugin.getPlayerPreferenceRepository().toggleShops(player.getUniqueId());
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (nowEnabled) {
|
||||
player.sendMessage(
|
||||
Component.text("shop creation enabled. you can now make chest shops!",
|
||||
NamedTextColor.GREEN));
|
||||
} else {
|
||||
player.sendMessage(
|
||||
Component.text("shop creation disabled. oyeshops will now ignore your signs.",
|
||||
NamedTextColor.YELLOW));
|
||||
}
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleNotifyToggle(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("players only", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
boolean nowEnabled = plugin.getPlayerPreferenceRepository().toggleNotify(player.getUniqueId());
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (nowEnabled) {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"stock notifications enabled. you will be notified of low stock on login.",
|
||||
NamedTextColor.GREEN));
|
||||
} else {
|
||||
player.sendMessage(Component.text("stock notifications disabled.", NamedTextColor.YELLOW));
|
||||
}
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleActivate(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof Player player))
|
||||
return;
|
||||
if (args.length < 2)
|
||||
return;
|
||||
|
||||
String action = args[1].toLowerCase();
|
||||
PendingActivation activation = plugin.getActivationManager().getAndRemove(player.getUniqueId());
|
||||
|
||||
if (activation == null) {
|
||||
player.sendMessage(Component.text("activation expired or not found", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "accept" -> {
|
||||
finalizeShop(player, activation);
|
||||
}
|
||||
case "invert" -> {
|
||||
finalizeShop(player, activation.invert());
|
||||
}
|
||||
case "cancel" -> {
|
||||
player.sendMessage(
|
||||
Component.text("shop creation cancelled. sign remains as vanilla.", NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizeShop(Player player, PendingActivation activation) {
|
||||
plugin.getShopActivationListener().finalizeShop(player, activation);
|
||||
}
|
||||
|
||||
private void handleEnable(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
if (sender instanceof Player player) {
|
||||
try {
|
||||
plugin.getPlayerPreferenceRepository().enableShops(player.getUniqueId());
|
||||
player.sendMessage(Component.text("shop creation enabled. you can now make chest shops!",
|
||||
NamedTextColor.GREEN));
|
||||
} catch (SQLException e) {
|
||||
player.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage(Component.text("usage: /oyeshops enable <id>", NamedTextColor.RED));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.isAdmin(sender)) {
|
||||
sender.sendMessage(Component.text("no permission", 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;
|
||||
}
|
||||
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));
|
||||
} catch (SQLException e) {
|
||||
sender.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisable(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
if (sender instanceof Player player) {
|
||||
try {
|
||||
plugin.getPlayerPreferenceRepository().disableShops(player.getUniqueId());
|
||||
player.sendMessage(Component.text("shop creation disabled. oyeshops will now ignore your signs.",
|
||||
NamedTextColor.YELLOW));
|
||||
} catch (SQLException e) {
|
||||
player.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage(Component.text("usage: /oyeshops disable <id>", NamedTextColor.RED));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.isAdmin(sender)) {
|
||||
sender.sendMessage(Component.text("no permission", 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;
|
||||
}
|
||||
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));
|
||||
} catch (SQLException e) {
|
||||
sender.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnregister(CommandSender sender, String[] args) {
|
||||
if (!PermissionManager.isAdmin(sender)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(Component.text("usage: /oyeshops unregister <id>", 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));
|
||||
} catch (SQLException e) {
|
||||
sender.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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"));
|
||||
if (PermissionManager.isAdmin(sender)) {
|
||||
subCommands.addAll(List.of("reload", "inspect", "spoof", "unregister"));
|
||||
}
|
||||
String partial = args[0].toLowerCase();
|
||||
for (String sub : subCommands) {
|
||||
if (sub.startsWith(partial))
|
||||
completions.add(sub);
|
||||
}
|
||||
} 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];
|
||||
for (Shop shop : plugin.getShopRegistry().getAllShops()) {
|
||||
String id = String.valueOf(shop.getId());
|
||||
if (id.startsWith(partial))
|
||||
completions.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package party.cybsec.oyeshops.config;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* configuration loading and management
|
||||
*/
|
||||
public class ConfigManager {
|
||||
private final Plugin plugin;
|
||||
private FileConfiguration config;
|
||||
|
||||
public ConfigManager(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
reload();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public FileConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package party.cybsec.oyeshops.database;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* sqlite database initialization and connection management
|
||||
*/
|
||||
public class DatabaseManager {
|
||||
private final File dataFolder;
|
||||
private Connection connection;
|
||||
|
||||
public DatabaseManager(File dataFolder) {
|
||||
this.dataFolder = dataFolder;
|
||||
}
|
||||
|
||||
public void initialize() throws SQLException {
|
||||
if (!dataFolder.exists()) {
|
||||
dataFolder.mkdirs();
|
||||
}
|
||||
|
||||
File dbFile = new File(dataFolder, "oyeshops.db");
|
||||
String url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
|
||||
|
||||
connection = DriverManager.getConnection(url);
|
||||
createTables();
|
||||
}
|
||||
|
||||
private void createTables() throws SQLException {
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// shops table
|
||||
stmt.execute("""
|
||||
create table if not exists shops (
|
||||
shop_id integer primary key autoincrement,
|
||||
world_uuid text not null,
|
||||
sign_x integer not null,
|
||||
sign_y integer not null,
|
||||
sign_z integer not null,
|
||||
owner_uuid text not null,
|
||||
price_item text not null,
|
||||
price_quantity integer not null,
|
||||
product_item text not null,
|
||||
product_quantity integer not null,
|
||||
owed_amount integer not null default 0,
|
||||
enabled boolean not null default true,
|
||||
created_at integer not null,
|
||||
unique(world_uuid, sign_x, sign_y, sign_z)
|
||||
)
|
||||
""");
|
||||
|
||||
// transaction history table
|
||||
stmt.execute("""
|
||||
create table if not exists transactions (
|
||||
transaction_id integer primary key autoincrement,
|
||||
shop_id integer not null,
|
||||
buyer_uuid text not null,
|
||||
quantity_traded integer not null,
|
||||
timestamp integer not null,
|
||||
foreign key (shop_id) references shops(shop_id) on delete cascade
|
||||
)
|
||||
""");
|
||||
|
||||
// player preferences table (opt-in for shop creation)
|
||||
stmt.execute("""
|
||||
create table if not exists player_preferences (
|
||||
player_uuid text primary key,
|
||||
shops_enabled boolean not null default false,
|
||||
notify_low_stock boolean not null default false,
|
||||
enabled_at integer
|
||||
)
|
||||
""");
|
||||
|
||||
// indexes
|
||||
stmt.execute("""
|
||||
create index if not exists idx_shop_location
|
||||
on shops(world_uuid, sign_x, sign_y, sign_z)
|
||||
""");
|
||||
|
||||
stmt.execute("""
|
||||
create index if not exists idx_transaction_shop
|
||||
on transactions(shop_id, timestamp desc)
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package party.cybsec.oyeshops.database;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* manages player opt-in preferences for shop creation and notifications
|
||||
* shops are disabled by default - players must explicitly enable
|
||||
* stock notifications are disabled by default
|
||||
*/
|
||||
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<>();
|
||||
|
||||
public PlayerPreferenceRepository(DatabaseManager dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* load all player preferences into cache
|
||||
*/
|
||||
public void loadPreferences() throws SQLException {
|
||||
String sql = "select player_uuid, shops_enabled, notify_low_stock from player_preferences";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
UUID uuid = UUID.fromString(rs.getString("player_uuid"));
|
||||
if (rs.getBoolean("shops_enabled")) {
|
||||
enabledPlayers.add(uuid);
|
||||
}
|
||||
if (rs.getBoolean("notify_low_stock")) {
|
||||
notifyPlayers.add(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player has shops enabled (opt-in)
|
||||
*/
|
||||
public boolean isShopsEnabled(UUID playerUuid) {
|
||||
return enabledPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player has stock notifications enabled
|
||||
*/
|
||||
public boolean isNotifyEnabled(UUID playerUuid) {
|
||||
return notifyPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* enable shops for player
|
||||
*/
|
||||
public void enableShops(UUID playerUuid) throws SQLException {
|
||||
String sql = """
|
||||
insert into player_preferences (player_uuid, shops_enabled, enabled_at)
|
||||
values (?, true, ?)
|
||||
on conflict(player_uuid) do update set shops_enabled = true, enabled_at = ?
|
||||
""";
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
stmt.setLong(2, now);
|
||||
stmt.setLong(3, now);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
enabledPlayers.add(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable shops for player
|
||||
*/
|
||||
public void disableShops(UUID playerUuid) throws SQLException {
|
||||
String sql = """
|
||||
insert into player_preferences (player_uuid, shops_enabled, enabled_at)
|
||||
values (?, false, null)
|
||||
on conflict(player_uuid) do update set shops_enabled = false
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
enabledPlayers.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle shops for player
|
||||
*
|
||||
* @return true if now enabled, false if now disabled
|
||||
*/
|
||||
public boolean toggleShops(UUID playerUuid) throws SQLException {
|
||||
if (isShopsEnabled(playerUuid)) {
|
||||
disableShops(playerUuid);
|
||||
return false;
|
||||
} else {
|
||||
enableShops(playerUuid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set stock notification preference
|
||||
*/
|
||||
public void setNotifyEnabled(UUID playerUuid, boolean enabled) throws SQLException {
|
||||
String sql = """
|
||||
insert into player_preferences (player_uuid, notify_low_stock)
|
||||
values (?, ?)
|
||||
on conflict(player_uuid) do update set notify_low_stock = ?
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
stmt.setBoolean(2, enabled);
|
||||
stmt.setBoolean(3, enabled);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
notifyPlayers.add(playerUuid);
|
||||
} else {
|
||||
notifyPlayers.remove(playerUuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle notification preference
|
||||
*/
|
||||
public boolean toggleNotify(UUID playerUuid) throws SQLException {
|
||||
boolean newState = !isNotifyEnabled(playerUuid);
|
||||
setNotifyEnabled(playerUuid, newState);
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
246
src/main/java/party/cybsec/oyeshops/database/ShopRepository.java
Normal file
246
src/main/java/party/cybsec/oyeshops/database/ShopRepository.java
Normal file
@@ -0,0 +1,246 @@
|
||||
package party.cybsec.oyeshops.database;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.model.Trade;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* crud operations for shops
|
||||
*/
|
||||
public class ShopRepository {
|
||||
private final DatabaseManager dbManager;
|
||||
|
||||
public ShopRepository(DatabaseManager dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* create new shop in database
|
||||
*/
|
||||
public int createShop(Shop shop) throws SQLException {
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql,
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
Location loc = shop.getSignLocation();
|
||||
stmt.setString(1, loc.getWorld().getUID().toString());
|
||||
stmt.setInt(2, loc.getBlockX());
|
||||
stmt.setInt(3, loc.getBlockY());
|
||||
stmt.setInt(4, loc.getBlockZ());
|
||||
stmt.setString(5, shop.getOwner().toString());
|
||||
stmt.setString(6, shop.getTrade().priceItem().name());
|
||||
stmt.setInt(7, shop.getTrade().priceQuantity());
|
||||
stmt.setString(8, shop.getTrade().productItem().name());
|
||||
stmt.setInt(9, shop.getTrade().productQuantity());
|
||||
stmt.setInt(10, shop.getOwedAmount());
|
||||
stmt.setBoolean(11, shop.isEnabled());
|
||||
stmt.setLong(12, shop.getCreatedAt());
|
||||
|
||||
stmt.executeUpdate();
|
||||
|
||||
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new SQLException("failed to create shop, no id generated");
|
||||
}
|
||||
|
||||
/**
|
||||
* get shop by sign location
|
||||
*/
|
||||
public Shop getShop(Location location) throws SQLException {
|
||||
String sql = """
|
||||
select * from shops
|
||||
where world_uuid = ? and sign_x = ? and sign_y = ? and sign_z = ?
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, location.getWorld().getUID().toString());
|
||||
stmt.setInt(2, location.getBlockX());
|
||||
stmt.setInt(3, location.getBlockY());
|
||||
stmt.setInt(4, location.getBlockZ());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return shopFromResultSet(rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* get shop by id
|
||||
*/
|
||||
public Shop getShopById(int shopId) throws SQLException {
|
||||
String sql = "select * from shops where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return shopFromResultSet(rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* update owed amount atomically
|
||||
*/
|
||||
public void updateOwedAmount(int shopId, int amount) throws SQLException {
|
||||
String sql = "update shops set owed_amount = ? where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, amount);
|
||||
stmt.setInt(2, shopId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add to owed amount atomically
|
||||
*/
|
||||
public void addOwedAmount(int shopId, int delta) throws SQLException {
|
||||
String sql = "update shops set owed_amount = owed_amount + ? where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, delta);
|
||||
stmt.setInt(2, shopId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* enable shop
|
||||
*/
|
||||
public void enableShop(int shopId) throws SQLException {
|
||||
String sql = "update shops set enabled = true where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* disable shop
|
||||
*/
|
||||
public void disableShop(int shopId) throws SQLException {
|
||||
String sql = "update shops set enabled = false where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set shop enabled state
|
||||
*/
|
||||
public void setEnabled(int shopId, boolean enabled) throws SQLException {
|
||||
if (enabled) {
|
||||
enableShop(shopId);
|
||||
} else {
|
||||
disableShop(shopId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* delete shop
|
||||
*/
|
||||
public void deleteShop(int shopId) throws SQLException {
|
||||
String sql = "delete from shops where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all shops owned by player
|
||||
*/
|
||||
public List<Shop> getShopsByOwner(UUID ownerUuid) throws SQLException {
|
||||
String sql = "select * from shops where owner_uuid = ?";
|
||||
List<Shop> shops = new ArrayList<>();
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, ownerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
shops.add(shopFromResultSet(rs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shops;
|
||||
}
|
||||
|
||||
/**
|
||||
* load all shops from database
|
||||
*/
|
||||
public List<Shop> loadAllShops() throws SQLException {
|
||||
String sql = "select * from shops";
|
||||
List<Shop> shops = new ArrayList<>();
|
||||
|
||||
try (Statement stmt = dbManager.getConnection().createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
while (rs.next()) {
|
||||
shops.add(shopFromResultSet(rs));
|
||||
}
|
||||
}
|
||||
|
||||
return shops;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert result set to shop object
|
||||
*/
|
||||
private Shop shopFromResultSet(ResultSet rs) throws SQLException {
|
||||
int id = rs.getInt("shop_id");
|
||||
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"));
|
||||
Material priceItem = Material.valueOf(rs.getString("price_item"));
|
||||
int priceQty = rs.getInt("price_quantity");
|
||||
Material productItem = Material.valueOf(rs.getString("product_item"));
|
||||
int productQty = rs.getInt("product_quantity");
|
||||
int owedAmount = rs.getInt("owed_amount");
|
||||
boolean enabled = rs.getBoolean("enabled");
|
||||
long createdAt = rs.getLong("created_at");
|
||||
|
||||
World world = Bukkit.getWorld(worldUuid);
|
||||
if (world == null) {
|
||||
throw new SQLException("world not found: " + worldUuid);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package party.cybsec.oyeshops.database;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* transaction history operations
|
||||
*/
|
||||
public class TransactionRepository {
|
||||
private final DatabaseManager dbManager;
|
||||
|
||||
public TransactionRepository(DatabaseManager dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* record a transaction
|
||||
*/
|
||||
public void recordTransaction(int shopId, UUID buyerUuid, int quantityTraded) throws SQLException {
|
||||
String sql = """
|
||||
insert into transactions (shop_id, buyer_uuid, quantity_traded, timestamp)
|
||||
values (?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.setString(2, buyerUuid.toString());
|
||||
stmt.setInt(3, quantityTraded);
|
||||
stmt.setLong(4, System.currentTimeMillis());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get recent transactions for a shop
|
||||
*/
|
||||
public List<Transaction> getRecentTransactions(int shopId, int limit) throws SQLException {
|
||||
String sql = """
|
||||
select * from transactions
|
||||
where shop_id = ?
|
||||
order by timestamp desc
|
||||
limit ?
|
||||
""";
|
||||
|
||||
List<Transaction> transactions = new ArrayList<>();
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.setInt(2, limit);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
transactions.add(new Transaction(
|
||||
rs.getInt("transaction_id"),
|
||||
rs.getInt("shop_id"),
|
||||
UUID.fromString(rs.getString("buyer_uuid")),
|
||||
rs.getInt("quantity_traded"),
|
||||
rs.getLong("timestamp")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* prune old transactions, keeping only the most recent
|
||||
*/
|
||||
public void pruneOldTransactions(int shopId, int keepCount) throws SQLException {
|
||||
String sql = """
|
||||
delete from transactions
|
||||
where shop_id = ?
|
||||
and transaction_id not in (
|
||||
select transaction_id from transactions
|
||||
where shop_id = ?
|
||||
order by timestamp desc
|
||||
limit ?
|
||||
)
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.setInt(2, shopId);
|
||||
stmt.setInt(3, keepCount);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* prune old transactions (alias)
|
||||
*/
|
||||
public void pruneTransactions(int shopId, int keepCount) throws SQLException {
|
||||
pruneOldTransactions(shopId, keepCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear all transaction history for a shop
|
||||
*/
|
||||
public void clearHistory(int shopId) throws SQLException {
|
||||
String sql = "delete from transactions where shop_id = ?";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setInt(1, shopId);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* transaction record
|
||||
*/
|
||||
public record Transaction(
|
||||
int transactionId,
|
||||
int shopId,
|
||||
UUID buyerUuid,
|
||||
int quantityTraded,
|
||||
long timestamp) {
|
||||
}
|
||||
}
|
||||
116
src/main/java/party/cybsec/oyeshops/gui/ConfirmationGui.java
Normal file
116
src/main/java/party/cybsec/oyeshops/gui/ConfirmationGui.java
Normal file
@@ -0,0 +1,116 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
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.Action;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
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;
|
||||
import party.cybsec.oyeshops.transaction.TransactionManager;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* handles chest/barrel interactions for shop purchases and owner withdrawals
|
||||
*/
|
||||
public class ChestInteractionListener implements Listener {
|
||||
private final OyeShopsPlugin plugin;
|
||||
|
||||
// track players viewing fake shop inventories
|
||||
private final Map<Player, Shop> viewingShop = new HashMap<>();
|
||||
|
||||
public ChestInteractionListener(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block block = event.getClickedBlock();
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if this is a container (chest or barrel)
|
||||
if (!isContainer(block.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if there's a shop sign attached
|
||||
Shop shop = findShopForContainer(block);
|
||||
if (shop == null) {
|
||||
return; // not a shop container
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// check if shop is enabled
|
||||
if (!shop.isEnabled()) {
|
||||
player.sendMessage(Component.text("this shop is disabled", NamedTextColor.RED));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if player is owner (unless spoofing)
|
||||
boolean isOwner = shop.getOwner().equals(player.getUniqueId())
|
||||
&& !plugin.getSpoofManager().isSpoofing(player);
|
||||
|
||||
if (isOwner) {
|
||||
// owner interaction - check for owed items and dispense
|
||||
if (shop.getOwedAmount() > 0) {
|
||||
withdrawOwedItems(player, shop);
|
||||
}
|
||||
// let owner open the chest normally - don't cancel event
|
||||
return;
|
||||
}
|
||||
|
||||
// buyer interaction - must have permission
|
||||
if (!PermissionManager.canUse(player)) {
|
||||
player.sendMessage(Component.text("you don't have permission to use shops", NamedTextColor.RED));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// cancel normal chest opening
|
||||
event.setCancelled(true);
|
||||
|
||||
// open shop GUI
|
||||
openShopGui(player, shop, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* withdraw owed items to owner's inventory
|
||||
*/
|
||||
private void withdrawOwedItems(Player player, Shop shop) {
|
||||
int owed = shop.getOwedAmount();
|
||||
if (owed <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Material priceItem = shop.getTrade().priceItem();
|
||||
int maxStack = priceItem.getMaxStackSize();
|
||||
|
||||
int withdrawn = 0;
|
||||
int remaining = owed;
|
||||
|
||||
while (remaining > 0) {
|
||||
int toGive = Math.min(remaining, maxStack);
|
||||
ItemStack stack = new ItemStack(priceItem, toGive);
|
||||
|
||||
HashMap<Integer, ItemStack> overflow = player.getInventory().addItem(stack);
|
||||
|
||||
if (overflow.isEmpty()) {
|
||||
withdrawn += toGive;
|
||||
remaining -= toGive;
|
||||
} else {
|
||||
// inventory full - stop
|
||||
int notAdded = overflow.values().stream().mapToInt(ItemStack::getAmount).sum();
|
||||
withdrawn += (toGive - notAdded);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (withdrawn > 0) {
|
||||
try {
|
||||
// update database
|
||||
int newOwed = owed - withdrawn;
|
||||
shop.setOwedAmount(newOwed);
|
||||
plugin.getShopRepository().updateOwedAmount(shop.getId(), -withdrawn);
|
||||
|
||||
player.sendMessage(
|
||||
Component.text("withdrew " + withdrawn + " " + formatMaterial(priceItem), NamedTextColor.GREEN)
|
||||
.append(Component.text(newOwed > 0 ? " (" + newOwed + " remaining)" : "",
|
||||
NamedTextColor.GRAY)));
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
player.sendMessage(Component.text("database error during withdrawal", NamedTextColor.RED));
|
||||
}
|
||||
} else {
|
||||
player.sendMessage(Component.text("inventory full - could not withdraw", NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())));
|
||||
|
||||
// get real container inventory
|
||||
Inventory realInventory = getContainerInventory(containerBlock);
|
||||
if (realInventory == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// copy only product items to fake inventory (first 27 found)
|
||||
int shopSlot = 0;
|
||||
for (ItemStack item : realInventory.getContents()) {
|
||||
if (shopSlot >= 27)
|
||||
break;
|
||||
if (item != null && item.getType() == trade.productItem()) {
|
||||
shopInventory.setItem(shopSlot++, item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
viewingShop.put(player, shop);
|
||||
player.openInventory(shopInventory);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// handle confirmation GUI clicks
|
||||
if (holder instanceof ConfirmationGui confirmGui) {
|
||||
event.setCancelled(true);
|
||||
Shop shop = confirmGui.getShop();
|
||||
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
// green pane = confirm (slot 11-15 or specifically slot 11)
|
||||
if (slot == 11 || slot == 12 || slot == 13) {
|
||||
player.closeInventory();
|
||||
executeTransaction(player, shop, confirmGui.getUnits());
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private Shop findShopForContainer(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);
|
||||
if (adjacent.getBlockData() instanceof WallSign wallSign) {
|
||||
// check if the sign is facing away from the container (attached to it)
|
||||
if (wallSign.getFacing().getOppositeFace() == face.getOppositeFace()) {
|
||||
Shop shop = plugin.getShopRegistry().getShop(adjacent.getLocation());
|
||||
if (shop != null) {
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* get container inventory
|
||||
*/
|
||||
private Inventory getContainerInventory(Block block) {
|
||||
if (block.getState() instanceof Chest chest) {
|
||||
return chest.getInventory();
|
||||
} else if (block.getState() instanceof Barrel barrel) {
|
||||
return barrel.getInventory();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if material is a container
|
||||
*/
|
||||
private boolean isContainer(Material material) {
|
||||
return material == Material.CHEST || material == Material.TRAPPED_CHEST || material == Material.BARREL;
|
||||
}
|
||||
|
||||
/**
|
||||
* format material name for display
|
||||
*/
|
||||
private String formatMaterial(Material material) {
|
||||
return material.name().toLowerCase().replace("_", " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* holder for shop inventory
|
||||
*/
|
||||
public record ShopInventoryHolder(Shop shop) implements InventoryHolder {
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* monitors block placement to track container ownership for shop creation
|
||||
*/
|
||||
public class ContainerPlacementListener implements Listener {
|
||||
private final OyeShopsPlugin plugin;
|
||||
|
||||
public ContainerPlacementListener(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlace(BlockPlaceEvent event) {
|
||||
Block block = event.getBlock();
|
||||
if (isContainer(block.getType())) {
|
||||
plugin.getContainerMemoryManager().recordPlacement(block.getLocation(), event.getPlayer().getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBreak(BlockBreakEvent event) {
|
||||
Block block = event.getBlock();
|
||||
if (isContainer(block.getType())) {
|
||||
plugin.getContainerMemoryManager().removePlacement(block.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBlockExplode(BlockExplodeEvent event) {
|
||||
handleExplosion(event.blockList());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onEntityExplode(EntityExplodeEvent event) {
|
||||
handleExplosion(event.blockList());
|
||||
}
|
||||
|
||||
private void handleExplosion(List<Block> blocks) {
|
||||
for (Block block : blocks) {
|
||||
if (isContainer(block.getType())) {
|
||||
plugin.getContainerMemoryManager().removePlacement(block.getLocation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isContainer(Material material) {
|
||||
return material == Material.CHEST || material == Material.TRAPPED_CHEST || material == Material.BARREL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.database.TransactionRepository;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* handles right-click inspection while in inspect mode
|
||||
*/
|
||||
public class InspectListener implements Listener {
|
||||
private final OyeShopsPlugin plugin;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public InspectListener(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// check if player is in inspect mode
|
||||
if (!plugin.getInspectModeManager().isInspecting(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block block = event.getClickedBlock();
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if this is a shop sign
|
||||
Shop shop = plugin.getShopRegistry().getShop(block.getLocation());
|
||||
if (shop == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cancel interaction
|
||||
event.setCancelled(true);
|
||||
|
||||
// display shop info
|
||||
displayShopInfo(player, shop);
|
||||
}
|
||||
|
||||
private void displayShopInfo(Player player, Shop shop) {
|
||||
player.sendMessage(Component.text("=== shop #" + shop.getId() + " ===", NamedTextColor.GOLD));
|
||||
|
||||
// owner
|
||||
String ownerName = Bukkit.getOfflinePlayer(shop.getOwner()).getName();
|
||||
if (ownerName == null) {
|
||||
ownerName = shop.getOwner().toString();
|
||||
}
|
||||
player.sendMessage(Component.text("owner: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(ownerName, NamedTextColor.WHITE)));
|
||||
|
||||
// status
|
||||
String status = shop.isEnabled() ? "enabled" : "disabled";
|
||||
NamedTextColor statusColor = shop.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED;
|
||||
player.sendMessage(Component.text("status: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(status, statusColor)));
|
||||
|
||||
// trade
|
||||
player.sendMessage(Component.text("trade: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(
|
||||
shop.getTrade().priceQuantity() + " " + shop.getTrade().priceItem().name().toLowerCase() +
|
||||
" → " +
|
||||
shop.getTrade().productQuantity() + " "
|
||||
+ shop.getTrade().productItem().name().toLowerCase(),
|
||||
NamedTextColor.YELLOW)));
|
||||
|
||||
// owed amount
|
||||
player.sendMessage(Component.text("owed: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(
|
||||
shop.getOwedAmount() + " " + shop.getTrade().priceItem().name().toLowerCase(),
|
||||
NamedTextColor.GREEN)));
|
||||
|
||||
// stock (check chest)
|
||||
int stock = getStock(shop);
|
||||
int units = stock / shop.getTrade().productQuantity();
|
||||
player.sendMessage(Component.text("stock: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(
|
||||
stock + " " + shop.getTrade().productItem().name().toLowerCase() +
|
||||
" (" + units + " units available)",
|
||||
NamedTextColor.AQUA)));
|
||||
|
||||
// created
|
||||
String createdDate = dateFormat.format(new Date(shop.getCreatedAt()));
|
||||
player.sendMessage(Component.text("created: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(createdDate, NamedTextColor.WHITE)));
|
||||
|
||||
// recent transactions
|
||||
try {
|
||||
List<TransactionRepository.Transaction> transactions = plugin.getTransactionRepository()
|
||||
.getRecentTransactions(shop.getId(), 10);
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
player.sendMessage(Component.text("recent transactions (last 10):", NamedTextColor.GOLD));
|
||||
for (TransactionRepository.Transaction tx : transactions) {
|
||||
String buyerName = Bukkit.getOfflinePlayer(tx.buyerUuid()).getName();
|
||||
if (buyerName == null) {
|
||||
buyerName = tx.buyerUuid().toString();
|
||||
}
|
||||
String txDate = dateFormat.format(new Date(tx.timestamp()));
|
||||
player.sendMessage(Component.text(" - ", NamedTextColor.GRAY)
|
||||
.append(Component.text(buyerName, NamedTextColor.WHITE))
|
||||
.append(Component.text(" bought ", NamedTextColor.GRAY))
|
||||
.append(Component.text(tx.quantityTraded() + " units", NamedTextColor.YELLOW))
|
||||
.append(Component.text(" (" + txDate + ")", NamedTextColor.DARK_GRAY)));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// action buttons (clickable text)
|
||||
player.sendMessage(Component.text(""));
|
||||
player.sendMessage(
|
||||
Component.text("[enable]", NamedTextColor.GREEN)
|
||||
.append(Component.text(" ", NamedTextColor.WHITE))
|
||||
.append(Component.text("[disable]", NamedTextColor.RED))
|
||||
.append(Component.text(" ", NamedTextColor.WHITE))
|
||||
.append(Component.text("[unregister]", NamedTextColor.DARK_RED))
|
||||
.append(Component.text(" ", NamedTextColor.WHITE))
|
||||
.append(Component.text("[clear history]", NamedTextColor.YELLOW)));
|
||||
player.sendMessage(Component.text("use /oyeshops <action> " + shop.getId(), NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
/**
|
||||
* get current stock from chest or barrel
|
||||
*/
|
||||
private int getStock(Shop shop) {
|
||||
Block signBlock = shop.getSignLocation().getBlock();
|
||||
org.bukkit.block.data.BlockData blockData = signBlock.getBlockData();
|
||||
|
||||
if (!(blockData instanceof org.bukkit.block.data.type.WallSign wallSign)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
org.bukkit.block.BlockFace attachedFace = wallSign.getFacing().getOppositeFace();
|
||||
Block containerBlock = signBlock.getRelative(attachedFace);
|
||||
|
||||
org.bukkit.inventory.Inventory inventory = null;
|
||||
|
||||
if (containerBlock.getState() instanceof org.bukkit.block.Chest chest) {
|
||||
inventory = chest.getInventory();
|
||||
} else if (containerBlock.getState() instanceof org.bukkit.block.Barrel barrel) {
|
||||
inventory = barrel.getInventory();
|
||||
}
|
||||
|
||||
if (inventory == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (org.bukkit.inventory.ItemStack item : inventory.getContents()) {
|
||||
if (item != null && item.getType() == shop.getTrade().productItem()) {
|
||||
count += item.getAmount();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* alerts players on login if their shops are low on stock
|
||||
*/
|
||||
public class LoginListener implements Listener {
|
||||
private final OyeShopsPlugin plugin;
|
||||
|
||||
public LoginListener(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// check if user wants notifications
|
||||
if (!plugin.getPlayerPreferenceRepository().isNotifyEnabled(player.getUniqueId())) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isLowStock(Shop shop) {
|
||||
Block signBlock = shop.getSignLocation().getBlock();
|
||||
if (!(signBlock.getBlockData() instanceof WallSign wallSign))
|
||||
return false;
|
||||
|
||||
BlockFace attachedFace = wallSign.getFacing().getOppositeFace();
|
||||
Block attachedBlock = signBlock.getRelative(attachedFace);
|
||||
|
||||
Inventory inventory = getContainerInventory(attachedBlock);
|
||||
if (inventory == null)
|
||||
return true; // container gone? count as low stock warning
|
||||
|
||||
Material product = shop.getTrade().productItem();
|
||||
int count = 0;
|
||||
for (ItemStack item : inventory.getContents()) {
|
||||
if (item != null && item.getType() == product) {
|
||||
count += item.getAmount();
|
||||
}
|
||||
}
|
||||
|
||||
// threshold: less than 1 transaction or less than 10 items
|
||||
return count < shop.getTrade().productQuantity() || count < 10;
|
||||
}
|
||||
|
||||
private Inventory getContainerInventory(Block block) {
|
||||
if (block.getState() instanceof Chest chest)
|
||||
return chest.getInventory();
|
||||
if (block.getState() instanceof Barrel barrel)
|
||||
return barrel.getInventory();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.block.DoubleChest;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
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.SignChangeEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.DoubleChestInventory;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.PendingActivation;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.model.Trade;
|
||||
import party.cybsec.oyeshops.parser.SignParser;
|
||||
import party.cybsec.oyeshops.permission.PermissionManager;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* listens for sign placement and validates shop activation
|
||||
* now uses a confirmation flow via chat
|
||||
*/
|
||||
public class ShopActivationListener implements Listener {
|
||||
private final OyeShopsPlugin plugin;
|
||||
private final SignParser parser;
|
||||
|
||||
public ShopActivationListener(OyeShopsPlugin plugin, SignParser parser) {
|
||||
this.plugin = plugin;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onSignChange(SignChangeEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Block block = event.getBlock();
|
||||
|
||||
// check if player has opted into shop creation (disabled by default)
|
||||
if (!plugin.getPlayerPreferenceRepository().isShopsEnabled(player.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check permission
|
||||
if (!PermissionManager.canCreate(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if wall sign
|
||||
BlockData blockData = block.getBlockData();
|
||||
if (!(blockData instanceof WallSign wallSign)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get attached container (chest or barrel)
|
||||
BlockFace attachedFace = wallSign.getFacing().getOppositeFace();
|
||||
Block attachedBlock = block.getRelative(attachedFace);
|
||||
|
||||
if (!isContainer(attachedBlock.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- protection checks ---
|
||||
|
||||
// 1. check if container is already part of another player's shop
|
||||
Shop existingShop = findAnyShopOnContainer(attachedBlock);
|
||||
if (existingShop != null && !existingShop.getOwner().equals(player.getUniqueId())
|
||||
&& !PermissionManager.isAdmin(player)) {
|
||||
player.sendMessage(Component.text("this container belongs to another player's shop", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. session-based ownership check
|
||||
if (!PermissionManager.isAdmin(player)) {
|
||||
UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation());
|
||||
if (sessionOwner == null) {
|
||||
// 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));
|
||||
return;
|
||||
}
|
||||
if (!sessionOwner.equals(player.getUniqueId())) {
|
||||
player.sendMessage(Component.text("this container was placed by another player", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get sign text
|
||||
String[] lines = new String[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Component lineComponent = event.line(i);
|
||||
lines[i] = lineComponent != null
|
||||
? net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText()
|
||||
.serialize(lineComponent)
|
||||
: "";
|
||||
}
|
||||
|
||||
// parse trade
|
||||
Trade trade = parser.parse(lines);
|
||||
if (trade == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle auto detection for both sides
|
||||
if (trade.isAutoDetect()) {
|
||||
trade = detectAutoTrade(trade, attachedBlock, player);
|
||||
if (trade == null) {
|
||||
player.sendMessage(Component.text("auto detection failed: could not determine items from container",
|
||||
NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// trigger confirmation instead of immediate creation
|
||||
PendingActivation activation = new PendingActivation(player.getUniqueId(), block.getLocation(), trade,
|
||||
System.currentTimeMillis());
|
||||
plugin.getActivationManager().add(player.getUniqueId(), activation);
|
||||
|
||||
sendConfirmationMessage(player, trade);
|
||||
}
|
||||
|
||||
private void sendConfirmationMessage(Player player, Trade trade) {
|
||||
String priceText = trade.priceQuantity() + " " + formatMaterial(trade.priceItem());
|
||||
String productText = trade.productQuantity() + " " + formatMaterial(trade.productItem());
|
||||
|
||||
// clear display: buyer pays vs buyer gets
|
||||
player.sendMessage(Component.text("shop detected!", NamedTextColor.GOLD));
|
||||
player.sendMessage(Component.text("buyer pays: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(priceText, NamedTextColor.GREEN)));
|
||||
player.sendMessage(Component.text("buyer gets: ", NamedTextColor.GRAY)
|
||||
.append(Component.text(productText, NamedTextColor.YELLOW)));
|
||||
|
||||
Component buttons = Component.text(" ")
|
||||
.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")))
|
||||
.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")))
|
||||
.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")));
|
||||
|
||||
player.sendMessage(buttons);
|
||||
}
|
||||
|
||||
/**
|
||||
* complete the shop creation process
|
||||
* called from AdminCommands when user clicks [accept] or [invert]
|
||||
*/
|
||||
public void finalizeShop(Player player, PendingActivation activation) {
|
||||
Location signLocation = activation.location();
|
||||
Block block = signLocation.getBlock();
|
||||
|
||||
// verify it's still a sign
|
||||
if (!(block.getState() instanceof Sign 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);
|
||||
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
int shopId = plugin.getShopRepository().createShop(shop);
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
// re-verify sign on main thread
|
||||
if (!(signLocation.getBlock().getState() instanceof Sign finalSign))
|
||||
return;
|
||||
|
||||
Shop registeredShop = new Shop(shopId, signLocation, player.getUniqueId(), trade, 0, true,
|
||||
createdAt);
|
||||
plugin.getShopRegistry().register(registeredShop);
|
||||
rewriteSignLines(finalSign, trade);
|
||||
player.sendMessage(Component.text("shop #" + shopId + " initialized!", NamedTextColor.GREEN));
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(Component.text("failed to create shop: database error", NamedTextColor.RED));
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* detect auto trade from chest contents
|
||||
* can detect material and quantity for either side (or both)
|
||||
*/
|
||||
private Trade detectAutoTrade(Trade baseTrade, Block containerBlock, Player player) {
|
||||
Inventory inventory = getContainerInventory(containerBlock);
|
||||
if (inventory == null)
|
||||
return null;
|
||||
|
||||
Map<Material, Map<Integer, Integer>> counts = new HashMap<>();
|
||||
for (ItemStack item : inventory.getContents()) {
|
||||
if (item == null || item.getType() == Material.AIR)
|
||||
continue;
|
||||
counts.computeIfAbsent(item.getType(), k -> new HashMap<>())
|
||||
.merge(item.getAmount(), 1, Integer::sum);
|
||||
}
|
||||
|
||||
if (counts.isEmpty())
|
||||
return null;
|
||||
|
||||
Material priceItem = baseTrade.priceItem();
|
||||
int priceQty = baseTrade.priceQuantity();
|
||||
Material productItem = baseTrade.productItem();
|
||||
int productQty = baseTrade.productQuantity();
|
||||
|
||||
// if product needs detection
|
||||
if (productQty == -1) {
|
||||
Material bestMat = null;
|
||||
int bestQty = -1;
|
||||
int bestScore = -1;
|
||||
|
||||
for (Map.Entry<Material, Map<Integer, Integer>> entry : counts.entrySet()) {
|
||||
Material mat = entry.getKey();
|
||||
// skip if it's the price item (unless it's unknown)
|
||||
if (priceItem != null && mat == priceItem)
|
||||
continue;
|
||||
|
||||
int qty = findMostCommonQuantity(entry.getValue());
|
||||
int score = entry.getValue().getOrDefault(qty, 0);
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestQty = qty;
|
||||
bestMat = mat;
|
||||
}
|
||||
}
|
||||
if (bestMat != null) {
|
||||
productItem = bestMat;
|
||||
productQty = bestQty;
|
||||
}
|
||||
}
|
||||
|
||||
// if price needs detection (e.g. "AUTO for 10 dirt")
|
||||
if (priceQty == -1) {
|
||||
Material bestMat = null;
|
||||
int bestQty = -1;
|
||||
int bestScore = -1;
|
||||
|
||||
for (Map.Entry<Material, Map<Integer, Integer>> entry : counts.entrySet()) {
|
||||
Material mat = entry.getKey();
|
||||
// skip if it's the product item
|
||||
if (productItem != null && mat == productItem)
|
||||
continue;
|
||||
|
||||
int qty = findMostCommonQuantity(entry.getValue());
|
||||
int score = entry.getValue().getOrDefault(qty, 0);
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestQty = qty;
|
||||
bestMat = mat;
|
||||
}
|
||||
}
|
||||
if (bestMat != null) {
|
||||
priceItem = bestMat;
|
||||
priceQty = bestQty;
|
||||
}
|
||||
}
|
||||
|
||||
// final checks
|
||||
if (priceItem == null || priceItem == Material.AIR || priceQty <= 0)
|
||||
return null;
|
||||
if (productItem == null || productItem == Material.AIR || productQty <= 0)
|
||||
return null;
|
||||
if (priceItem == productItem)
|
||||
return null;
|
||||
|
||||
return new Trade(priceItem, priceQty, productItem, productQty);
|
||||
}
|
||||
|
||||
private int findMostCommonQuantity(Map<Integer, Integer> counts) {
|
||||
int bestQuantity = -1;
|
||||
int bestCount = -1;
|
||||
for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
|
||||
if (entry.getValue() > bestCount) {
|
||||
bestCount = entry.getValue();
|
||||
bestQuantity = entry.getKey();
|
||||
}
|
||||
}
|
||||
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();
|
||||
if (block.getState() instanceof Barrel barrel)
|
||||
return barrel.getInventory();
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isContainer(Material material) {
|
||||
return material == Material.CHEST || material == Material.TRAPPED_CHEST || material == Material.BARREL;
|
||||
}
|
||||
|
||||
private String formatMaterial(Material material) {
|
||||
return material.name().toLowerCase().replace("_", " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* check if ANY shop exists on this container (or its other half)
|
||||
*/
|
||||
private Shop findAnyShopOnContainer(Block containerBlock) {
|
||||
List<Block> parts = getFullContainer(containerBlock);
|
||||
for (Block part : parts) {
|
||||
for (BlockFace face : new BlockFace[] { BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST,
|
||||
BlockFace.WEST }) {
|
||||
Block adjacent = part.getRelative(face);
|
||||
if (adjacent.getBlockData() instanceof WallSign wallSign) {
|
||||
if (wallSign.getFacing().getOppositeFace() == face.getOppositeFace()) {
|
||||
Shop shop = plugin.getShopRegistry().getShop(adjacent.getLocation());
|
||||
if (shop != null)
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* get all blocks belonging to this container (handles double chests)
|
||||
*/
|
||||
private List<Block> getFullContainer(Block block) {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
blocks.add(block);
|
||||
if (block.getState() instanceof Chest chest) {
|
||||
Inventory inv = chest.getInventory();
|
||||
if (inv instanceof DoubleChestInventory dci) {
|
||||
DoubleChest dc = dci.getHolder();
|
||||
if (dc != null) {
|
||||
blocks.clear();
|
||||
if (dc.getLeftSide() instanceof Chest left)
|
||||
blocks.add(left.getBlock());
|
||||
if (dc.getRightSide() instanceof Chest right)
|
||||
blocks.add(right.getBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
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 party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.permission.PermissionManager;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* protects shop signs and chests from unauthorized breaking
|
||||
*/
|
||||
public class ShopProtectionListener implements Listener {
|
||||
private final OyeShopsPlugin plugin;
|
||||
|
||||
public ShopProtectionListener(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(BlockBreakEvent event) {
|
||||
Block block = event.getBlock();
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// check if this block is a shop sign
|
||||
Shop shop = plugin.getShopRegistry().getShop(block.getLocation());
|
||||
|
||||
if (shop != null) {
|
||||
// this is a shop sign
|
||||
if (canBreakShop(player, shop)) {
|
||||
// authorized - unregister shop
|
||||
unregisterShop(shop);
|
||||
} else {
|
||||
// unauthorized - cancel break
|
||||
event.setCancelled(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// check if this block is a shop chest
|
||||
// we need to check all registered shops to see if any have this chest
|
||||
// for performance, we'll check if the broken block is a chest first
|
||||
if (!isChest(block.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find shop with this chest location
|
||||
Shop chestShop = findShopByChest(block);
|
||||
if (chestShop != null) {
|
||||
if (canBreakShop(player, chestShop)) {
|
||||
// authorized - unregister shop
|
||||
unregisterShop(chestShop);
|
||||
} else {
|
||||
// unauthorized - cancel break
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player can break shop
|
||||
*/
|
||||
private boolean canBreakShop(Player player, Shop shop) {
|
||||
// owner can break
|
||||
if (player.getUniqueId().equals(shop.getOwner())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// admin or break override can break
|
||||
return PermissionManager.isAdmin(player) || PermissionManager.canBreakOverride(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister shop from registry and database
|
||||
*/
|
||||
private void unregisterShop(Shop shop) {
|
||||
plugin.getShopRegistry().unregister(shop.getId());
|
||||
try {
|
||||
plugin.getShopRepository().deleteShop(shop.getId());
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* find shop by chest location
|
||||
*/
|
||||
private Shop findShopByChest(Block chestBlock) {
|
||||
// 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
|
||||
}) {
|
||||
Block adjacent = chestBlock.getRelative(face);
|
||||
Shop shop = plugin.getShopRegistry().getShop(adjacent.getLocation());
|
||||
if (shop != null) {
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if material is a container (chest or barrel)
|
||||
*/
|
||||
private boolean isChest(org.bukkit.Material material) {
|
||||
return material == org.bukkit.Material.CHEST
|
||||
|| material == org.bukkit.Material.TRAPPED_CHEST
|
||||
|| material == org.bukkit.Material.BARREL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package party.cybsec.oyeshops.manager;
|
||||
|
||||
import party.cybsec.oyeshops.model.PendingActivation;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* manages shops awaiting confirmation
|
||||
*/
|
||||
public class ActivationManager {
|
||||
private final Map<UUID, PendingActivation> pending = new ConcurrentHashMap<>();
|
||||
private static final long TIMEOUT_MS = 60 * 1000; // 60 seconds
|
||||
|
||||
/**
|
||||
* register a pending activation for a player
|
||||
*/
|
||||
public void add(UUID playerUuid, PendingActivation activation) {
|
||||
pending.put(playerUuid, activation);
|
||||
}
|
||||
|
||||
/**
|
||||
* get and remove pending activation for a player
|
||||
*/
|
||||
public PendingActivation getAndRemove(UUID playerUuid) {
|
||||
PendingActivation activation = pending.remove(playerUuid);
|
||||
if (activation != null && activation.isExpired(TIMEOUT_MS)) {
|
||||
return null;
|
||||
}
|
||||
return activation;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove a pending activation
|
||||
*/
|
||||
public void remove(UUID playerUuid) {
|
||||
pending.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player has a pending activation
|
||||
*/
|
||||
public boolean hasPending(UUID playerUuid) {
|
||||
PendingActivation activation = pending.get(playerUuid);
|
||||
if (activation != null && activation.isExpired(TIMEOUT_MS)) {
|
||||
pending.remove(playerUuid);
|
||||
return false;
|
||||
}
|
||||
return activation != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package party.cybsec.oyeshops.manager;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* tracks which player placed which container during this session
|
||||
*/
|
||||
public class ContainerMemoryManager {
|
||||
private final Map<String, UUID> placements = new ConcurrentHashMap<>();
|
||||
|
||||
public void recordPlacement(Location location, UUID playerUuid) {
|
||||
placements.put(locationKey(location), playerUuid);
|
||||
}
|
||||
|
||||
public void removePlacement(Location location) {
|
||||
placements.remove(locationKey(location));
|
||||
}
|
||||
|
||||
public UUID getOwner(Location location) {
|
||||
return placements.get(locationKey(location));
|
||||
}
|
||||
|
||||
public boolean isSessionPlaced(Location location) {
|
||||
return placements.containsKey(locationKey(location));
|
||||
}
|
||||
|
||||
private String locationKey(Location loc) {
|
||||
return loc.getWorld().getUID() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package party.cybsec.oyeshops.model;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* data for a shop awaiting player confirmation
|
||||
*/
|
||||
public record PendingActivation(
|
||||
UUID owner,
|
||||
Location location,
|
||||
Trade trade,
|
||||
long createdAt) {
|
||||
|
||||
/**
|
||||
* check if this activation has expired
|
||||
*/
|
||||
public boolean isExpired(long timeoutMs) {
|
||||
return System.currentTimeMillis() - createdAt > timeoutMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* get an inverted version of this activation (swaps price/product)
|
||||
*/
|
||||
public PendingActivation invert() {
|
||||
Trade invertedTrade = new Trade(
|
||||
trade.productItem(),
|
||||
trade.productQuantity(),
|
||||
trade.priceItem(),
|
||||
trade.priceQuantity());
|
||||
return new PendingActivation(owner, location, invertedTrade, createdAt);
|
||||
}
|
||||
}
|
||||
74
src/main/java/party/cybsec/oyeshops/model/Shop.java
Normal file
74
src/main/java/party/cybsec/oyeshops/model/Shop.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package party.cybsec.oyeshops.model;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* shop data model
|
||||
*/
|
||||
public class Shop {
|
||||
private final int id;
|
||||
private final Location signLocation;
|
||||
private final UUID owner;
|
||||
private final Trade trade;
|
||||
private int owedAmount;
|
||||
private boolean enabled;
|
||||
private final long createdAt;
|
||||
|
||||
public Shop(int id, Location signLocation, UUID owner, Trade trade, int owedAmount, boolean enabled,
|
||||
long createdAt) {
|
||||
this.id = id;
|
||||
this.signLocation = signLocation;
|
||||
this.owner = owner;
|
||||
this.trade = trade;
|
||||
this.owedAmount = owedAmount;
|
||||
this.enabled = enabled;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Location getSignLocation() {
|
||||
return signLocation;
|
||||
}
|
||||
|
||||
public UUID getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public Trade getTrade() {
|
||||
return trade;
|
||||
}
|
||||
|
||||
public int getOwedAmount() {
|
||||
return owedAmount;
|
||||
}
|
||||
|
||||
public void setOwedAmount(int owedAmount) {
|
||||
this.owedAmount = owedAmount;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
49
src/main/java/party/cybsec/oyeshops/model/Trade.java
Normal file
49
src/main/java/party/cybsec/oyeshops/model/Trade.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package party.cybsec.oyeshops.model;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
/**
|
||||
* immutable trade definition
|
||||
* quantity of -1 indicates AUTO detection needed for that side
|
||||
*/
|
||||
public record Trade(
|
||||
Material priceItem,
|
||||
int priceQuantity,
|
||||
Material productItem,
|
||||
int productQuantity) {
|
||||
|
||||
public Trade {
|
||||
// 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)
|
||||
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
|
||||
*/
|
||||
public boolean isAutoProduct() {
|
||||
return productQuantity == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public boolean isAutoDetect() {
|
||||
return isAutoProduct() || isAutoPrice();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package party.cybsec.oyeshops.parser;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* material name alias resolution system
|
||||
* handles underscores, plurals, and common abbreviations
|
||||
*/
|
||||
public class MaterialAliasRegistry {
|
||||
private final Map<String, Material> aliases = new HashMap<>();
|
||||
|
||||
// common aliases built-in
|
||||
private static final Map<String, String> COMMON_ALIASES = Map.ofEntries(
|
||||
// plurals
|
||||
Map.entry("diamonds", "diamond"),
|
||||
Map.entry("emeralds", "emerald"),
|
||||
Map.entry("stones", "stone"),
|
||||
Map.entry("dirts", "dirt"),
|
||||
Map.entry("cobblestones", "cobblestone"),
|
||||
Map.entry("coals", "coal"),
|
||||
Map.entry("irons", "iron_ingot"),
|
||||
Map.entry("golds", "gold_ingot"),
|
||||
Map.entry("coppers", "copper_ingot"),
|
||||
Map.entry("netherites", "netherite_ingot"),
|
||||
|
||||
// short forms
|
||||
Map.entry("dia", "diamond"),
|
||||
Map.entry("dias", "diamond"),
|
||||
Map.entry("em", "emerald"),
|
||||
Map.entry("ems", "emerald"),
|
||||
Map.entry("iron", "iron_ingot"),
|
||||
Map.entry("gold", "gold_ingot"),
|
||||
Map.entry("copper", "copper_ingot"),
|
||||
Map.entry("netherite", "netherite_ingot"),
|
||||
Map.entry("cobble", "cobblestone"),
|
||||
Map.entry("lapis", "lapis_lazuli"),
|
||||
Map.entry("redite", "redstone"),
|
||||
Map.entry("quartz", "quartz"),
|
||||
|
||||
// ores
|
||||
Map.entry("iron ore", "iron_ore"),
|
||||
Map.entry("gold ore", "gold_ore"),
|
||||
Map.entry("diamond ore", "diamond_ore"),
|
||||
Map.entry("coal ore", "coal_ore"),
|
||||
Map.entry("copper ore", "copper_ore"),
|
||||
Map.entry("emerald ore", "emerald_ore"),
|
||||
Map.entry("lapis ore", "lapis_ore"),
|
||||
Map.entry("redstone ore", "redstone_ore"),
|
||||
|
||||
// tools - pickaxes
|
||||
Map.entry("diamond pick", "diamond_pickaxe"),
|
||||
Map.entry("diamond pickaxe", "diamond_pickaxe"),
|
||||
Map.entry("iron pick", "iron_pickaxe"),
|
||||
Map.entry("iron pickaxe", "iron_pickaxe"),
|
||||
Map.entry("stone pick", "stone_pickaxe"),
|
||||
Map.entry("stone pickaxe", "stone_pickaxe"),
|
||||
Map.entry("wooden pick", "wooden_pickaxe"),
|
||||
Map.entry("wooden pickaxe", "wooden_pickaxe"),
|
||||
Map.entry("gold pick", "golden_pickaxe"),
|
||||
Map.entry("golden pick", "golden_pickaxe"),
|
||||
Map.entry("golden pickaxe", "golden_pickaxe"),
|
||||
Map.entry("netherite pick", "netherite_pickaxe"),
|
||||
Map.entry("netherite pickaxe", "netherite_pickaxe"),
|
||||
|
||||
// tools - swords
|
||||
Map.entry("diamond sword", "diamond_sword"),
|
||||
Map.entry("iron sword", "iron_sword"),
|
||||
Map.entry("stone sword", "stone_sword"),
|
||||
Map.entry("wooden sword", "wooden_sword"),
|
||||
Map.entry("golden sword", "golden_sword"),
|
||||
Map.entry("gold sword", "golden_sword"),
|
||||
Map.entry("netherite sword", "netherite_sword"),
|
||||
|
||||
// tools - axes
|
||||
Map.entry("diamond axe", "diamond_axe"),
|
||||
Map.entry("iron axe", "iron_axe"),
|
||||
Map.entry("stone axe", "stone_axe"),
|
||||
Map.entry("wooden axe", "wooden_axe"),
|
||||
Map.entry("golden axe", "golden_axe"),
|
||||
Map.entry("gold axe", "golden_axe"),
|
||||
Map.entry("netherite axe", "netherite_axe"),
|
||||
|
||||
// tools - shovels
|
||||
Map.entry("diamond shovel", "diamond_shovel"),
|
||||
Map.entry("iron shovel", "iron_shovel"),
|
||||
Map.entry("stone shovel", "stone_shovel"),
|
||||
Map.entry("wooden shovel", "wooden_shovel"),
|
||||
Map.entry("golden shovel", "golden_shovel"),
|
||||
Map.entry("gold shovel", "golden_shovel"),
|
||||
Map.entry("netherite shovel", "netherite_shovel"),
|
||||
|
||||
// tools - hoes
|
||||
Map.entry("diamond hoe", "diamond_hoe"),
|
||||
Map.entry("iron hoe", "iron_hoe"),
|
||||
Map.entry("stone hoe", "stone_hoe"),
|
||||
Map.entry("wooden hoe", "wooden_hoe"),
|
||||
Map.entry("golden hoe", "golden_hoe"),
|
||||
Map.entry("gold hoe", "golden_hoe"),
|
||||
Map.entry("netherite hoe", "netherite_hoe"),
|
||||
|
||||
// armor
|
||||
Map.entry("diamond helmet", "diamond_helmet"),
|
||||
Map.entry("diamond chestplate", "diamond_chestplate"),
|
||||
Map.entry("diamond leggings", "diamond_leggings"),
|
||||
Map.entry("diamond boots", "diamond_boots"),
|
||||
Map.entry("iron helmet", "iron_helmet"),
|
||||
Map.entry("iron chestplate", "iron_chestplate"),
|
||||
Map.entry("iron leggings", "iron_leggings"),
|
||||
Map.entry("iron boots", "iron_boots"),
|
||||
Map.entry("netherite helmet", "netherite_helmet"),
|
||||
Map.entry("netherite chestplate", "netherite_chestplate"),
|
||||
Map.entry("netherite leggings", "netherite_leggings"),
|
||||
Map.entry("netherite boots", "netherite_boots"),
|
||||
|
||||
// common blocks
|
||||
Map.entry("oak log", "oak_log"),
|
||||
Map.entry("oak plank", "oak_planks"),
|
||||
Map.entry("oak planks", "oak_planks"),
|
||||
Map.entry("oak wood", "oak_log"),
|
||||
Map.entry("spruce log", "spruce_log"),
|
||||
Map.entry("birch log", "birch_log"),
|
||||
Map.entry("jungle log", "jungle_log"),
|
||||
Map.entry("acacia log", "acacia_log"),
|
||||
Map.entry("dark oak log", "dark_oak_log"),
|
||||
Map.entry("glass", "glass"),
|
||||
Map.entry("sand", "sand"),
|
||||
Map.entry("gravel", "gravel"),
|
||||
Map.entry("obsidian", "obsidian"),
|
||||
Map.entry("glowstone", "glowstone"),
|
||||
Map.entry("netherrack", "netherrack"),
|
||||
Map.entry("endstone", "end_stone"),
|
||||
Map.entry("end stone", "end_stone"),
|
||||
|
||||
// food
|
||||
Map.entry("steak", "cooked_beef"),
|
||||
Map.entry("cooked steak", "cooked_beef"),
|
||||
Map.entry("porkchop", "cooked_porkchop"),
|
||||
Map.entry("cooked porkchop", "cooked_porkchop"),
|
||||
Map.entry("chicken", "cooked_chicken"),
|
||||
Map.entry("cooked chicken", "cooked_chicken"),
|
||||
Map.entry("bread", "bread"),
|
||||
Map.entry("apple", "apple"),
|
||||
Map.entry("golden apple", "golden_apple"),
|
||||
Map.entry("gapple", "golden_apple"),
|
||||
Map.entry("notch apple", "enchanted_golden_apple"),
|
||||
Map.entry("enchanted golden apple", "enchanted_golden_apple"),
|
||||
Map.entry("god apple", "enchanted_golden_apple"),
|
||||
|
||||
// misc
|
||||
Map.entry("ender pearl", "ender_pearl"),
|
||||
Map.entry("pearl", "ender_pearl"),
|
||||
Map.entry("enderpearl", "ender_pearl"),
|
||||
Map.entry("blaze rod", "blaze_rod"),
|
||||
Map.entry("slime ball", "slime_ball"),
|
||||
Map.entry("slimeball", "slime_ball"),
|
||||
Map.entry("ghast tear", "ghast_tear"),
|
||||
Map.entry("nether star", "nether_star"),
|
||||
Map.entry("totem", "totem_of_undying"),
|
||||
Map.entry("elytra", "elytra"),
|
||||
Map.entry("shulker", "shulker_box"),
|
||||
Map.entry("shulker box", "shulker_box"),
|
||||
Map.entry("book", "book"),
|
||||
Map.entry("exp bottle", "experience_bottle"),
|
||||
Map.entry("xp bottle", "experience_bottle"),
|
||||
Map.entry("bottle o enchanting", "experience_bottle"));
|
||||
|
||||
public MaterialAliasRegistry(ConfigurationSection aliasSection) {
|
||||
// load built-in aliases first
|
||||
for (Map.Entry<String, String> entry : COMMON_ALIASES.entrySet()) {
|
||||
try {
|
||||
Material material = Material.valueOf(entry.getValue().toUpperCase());
|
||||
aliases.put(entry.getKey().toLowerCase(), material);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignore invalid built-in aliases
|
||||
}
|
||||
}
|
||||
|
||||
// then load config aliases (override built-ins)
|
||||
loadAliases(aliasSection);
|
||||
}
|
||||
|
||||
private void loadAliases(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String alias : section.getKeys(false)) {
|
||||
String materialName = section.getString(alias);
|
||||
if (materialName != null) {
|
||||
try {
|
||||
Material material = Material.valueOf(materialName.toUpperCase());
|
||||
aliases.put(alias.toLowerCase(), material);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// invalid material name in config, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public Material resolve(String text) {
|
||||
text = text.toLowerCase().trim();
|
||||
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. try longest alias match 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)) {
|
||||
if (longestMatch == null || alias.length() > longestMatch.length()) {
|
||||
longestMatch = alias;
|
||||
longestMaterial = entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (longestMaterial != null) {
|
||||
return longestMaterial;
|
||||
}
|
||||
|
||||
// 2. try word-by-word alias match
|
||||
String[] words = text.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
|
||||
try {
|
||||
return Material.valueOf(word.toUpperCase() + "_INGOT");
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return Material.valueOf(word.toUpperCase() + "_BLOCK");
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
// 6. try stripping trailing 's' for plurals
|
||||
if (word.endsWith("s") && word.length() > 1) {
|
||||
String singular = word.substring(0, word.length() - 1);
|
||||
try {
|
||||
return Material.valueOf(singular.toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. try the whole text with underscores for complex names
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longestMaterial;
|
||||
}
|
||||
}
|
||||
262
src/main/java/party/cybsec/oyeshops/parser/SignParser.java
Normal file
262
src/main/java/party/cybsec/oyeshops/parser/SignParser.java
Normal file
@@ -0,0 +1,262 @@
|
||||
package party.cybsec.oyeshops.parser;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import party.cybsec.oyeshops.model.Trade;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* deterministic sign text parser
|
||||
* requires "." in sign text
|
||||
* ambiguity equals no shop
|
||||
*/
|
||||
public class SignParser {
|
||||
private final MaterialAliasRegistry aliasRegistry;
|
||||
|
||||
// directional keywords - price comes before, product comes after
|
||||
private static final Set<String> COST_INDICATORS = Set.of(
|
||||
"for", "per", "costs", "cost", "price", "=", "->", "=>", ">", "→");
|
||||
private static final Set<String> SELL_INDICATORS = Set.of(
|
||||
"get", "gets", "gives", "buy", "buys", "selling", "trades", "exchanges");
|
||||
|
||||
// quantity patterns
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");
|
||||
private static final Map<String, Integer> WORD_QUANTITIES = Map.ofEntries(
|
||||
Map.entry("a", 1),
|
||||
Map.entry("an", 1),
|
||||
Map.entry("one", 1),
|
||||
Map.entry("each", 1),
|
||||
Map.entry("two", 2),
|
||||
Map.entry("three", 3),
|
||||
Map.entry("four", 4),
|
||||
Map.entry("five", 5),
|
||||
Map.entry("six", 6),
|
||||
Map.entry("seven", 7),
|
||||
Map.entry("eight", 8),
|
||||
Map.entry("nine", 9),
|
||||
Map.entry("ten", 10),
|
||||
Map.entry("dozen", 12),
|
||||
Map.entry("half", 32),
|
||||
Map.entry("stack", 64));
|
||||
|
||||
// special keyword for auto-detection
|
||||
public static final String AUTO_KEYWORD = "auto";
|
||||
|
||||
public SignParser(MaterialAliasRegistry aliasRegistry) {
|
||||
this.aliasRegistry = aliasRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse sign lines into a trade
|
||||
*
|
||||
* @return trade if valid, null if invalid or ambiguous
|
||||
* 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);
|
||||
|
||||
// find directional keywords
|
||||
DirectionalInfo directional = findDirectionalKeywords(normalized);
|
||||
|
||||
if (directional != null) {
|
||||
// section-based parsing (original logic)
|
||||
String priceSection = normalized.substring(0, directional.keywordStart).trim();
|
||||
String productSection = normalized.substring(directional.keywordEnd).trim();
|
||||
|
||||
ItemQuantity price = parseItemQuantity(priceSection);
|
||||
if (price == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean isAuto = productSection.toLowerCase().contains(AUTO_KEYWORD);
|
||||
ItemQuantity product;
|
||||
if (isAuto) {
|
||||
Material productMaterial = aliasRegistry.resolve(productSection.replace(AUTO_KEYWORD, "").trim());
|
||||
product = new ItemQuantity(productMaterial, -1);
|
||||
} else {
|
||||
product = parseItemQuantity(productSection);
|
||||
if (product == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (product.material != null && price.material == product.material) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Trade(price.material, price.quantity, product.material != null ? product.material : Material.AIR,
|
||||
product.quantity);
|
||||
} else {
|
||||
// fallback: line-by-line parsing for keyword-less format (e.g. "1 dirt" \n "1
|
||||
// diamond")
|
||||
// we look for two distinct item-quantity pairs in the normalized text
|
||||
List<ItemQuantity> pairs = findMultipleItemQuantities(normalized);
|
||||
|
||||
if (pairs.size() == 2) {
|
||||
// assume first is price, second is product
|
||||
ItemQuantity price = pairs.get(0);
|
||||
ItemQuantity product = pairs.get(1);
|
||||
|
||||
if (price.material != null && product.material != null && price.material != product.material) {
|
||||
return new Trade(price.material, price.quantity, product.material, product.quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* find multiple item-quantity pairs in a string
|
||||
*/
|
||||
private List<ItemQuantity> findMultipleItemQuantities(String text) {
|
||||
List<ItemQuantity> results = new ArrayList<>();
|
||||
String[] words = text.split("\\s+");
|
||||
|
||||
// simple heuristic: find clusters of [quantity] [material]
|
||||
for (int i = 0; i < words.length; i++) {
|
||||
String word = words[i];
|
||||
|
||||
// is this a quantity?
|
||||
Integer qty = null;
|
||||
if (NUMBER_PATTERN.matcher(word).matches()) {
|
||||
qty = Integer.parseInt(word);
|
||||
} else if (WORD_QUANTITIES.containsKey(word)) {
|
||||
qty = WORD_QUANTITIES.get(word);
|
||||
}
|
||||
|
||||
if (qty != null && i + 1 < words.length) {
|
||||
// check if next word is a material
|
||||
Material mat = aliasRegistry.resolve(words[i + 1]);
|
||||
if (mat != null) {
|
||||
results.add(new ItemQuantity(mat, qty));
|
||||
i++; // skip material word
|
||||
}
|
||||
} else {
|
||||
// maybe it's just a material (quantity 1)
|
||||
Material mat = aliasRegistry.resolve(word);
|
||||
if (mat != null) {
|
||||
// avoid duplicates/overlaps
|
||||
results.add(new ItemQuantity(mat, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* normalize sign text
|
||||
* removes punctuation except underscores (for material names)
|
||||
* keeps "." check before calling this
|
||||
*/
|
||||
private String normalize(String text) {
|
||||
text = text.toLowerCase();
|
||||
// replace common arrow symbols with spaces
|
||||
text = text.replace("->", " for ");
|
||||
text = text.replace("=>", " for ");
|
||||
text = text.replace("→", " for ");
|
||||
text = text.replace(">", " for ");
|
||||
// replace punctuation with spaces (keep underscores)
|
||||
text = text.replaceAll("[^a-z0-9_\\s]", " ");
|
||||
// normalize whitespace
|
||||
text = text.replaceAll("\\s+", " ").trim();
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* find directional keywords and split point
|
||||
*/
|
||||
private DirectionalInfo findDirectionalKeywords(String text) {
|
||||
String[] tokens = text.split("\\s+");
|
||||
int currentPos = 0;
|
||||
|
||||
// first pass: look for cost indicators
|
||||
for (String token : tokens) {
|
||||
if (COST_INDICATORS.contains(token)) {
|
||||
int keywordEnd = currentPos + token.length();
|
||||
if (keywordEnd < text.length() && text.charAt(keywordEnd) == ' ') {
|
||||
keywordEnd++;
|
||||
}
|
||||
return new DirectionalInfo(currentPos, keywordEnd, token.equals("per") || token.equals("each"), token);
|
||||
}
|
||||
currentPos += token.length() + 1;
|
||||
}
|
||||
|
||||
// second pass: look for sell indicators
|
||||
currentPos = 0;
|
||||
for (String token : tokens) {
|
||||
if (SELL_INDICATORS.contains(token)) {
|
||||
int keywordEnd = currentPos + token.length();
|
||||
if (keywordEnd < text.length() && text.charAt(keywordEnd) == ' ') {
|
||||
keywordEnd++;
|
||||
}
|
||||
return new DirectionalInfo(currentPos, keywordEnd, false, token);
|
||||
}
|
||||
currentPos += token.length() + 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse item and quantity from text section
|
||||
*/
|
||||
private ItemQuantity parseItemQuantity(String section) {
|
||||
section = section.trim();
|
||||
|
||||
if (section.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// find quantity
|
||||
Integer quantity = null;
|
||||
|
||||
// look for explicit number first
|
||||
Matcher matcher = NUMBER_PATTERN.matcher(section);
|
||||
if (matcher.find()) {
|
||||
quantity = Integer.parseInt(matcher.group());
|
||||
}
|
||||
|
||||
// look for word quantities if no explicit number
|
||||
if (quantity == null) {
|
||||
String[] words = section.split("\\s+");
|
||||
for (String word : words) {
|
||||
if (WORD_QUANTITIES.containsKey(word)) {
|
||||
quantity = WORD_QUANTITIES.get(word);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to 1 if no quantity specified
|
||||
if (quantity == null) {
|
||||
quantity = 1;
|
||||
}
|
||||
|
||||
// find material
|
||||
Material material = aliasRegistry.resolve(section);
|
||||
if (material == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ItemQuantity(material, quantity);
|
||||
}
|
||||
|
||||
private record DirectionalInfo(int keywordStart, int keywordEnd, boolean isPerUnit, String keyword) {
|
||||
}
|
||||
|
||||
private record ItemQuantity(Material material, int quantity) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package party.cybsec.oyeshops.permission;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* centralized permission checking with op bypass
|
||||
*/
|
||||
public class PermissionManager {
|
||||
|
||||
public static final String CREATE = "oyeshops.create";
|
||||
public static final String USE = "oyeshops.use";
|
||||
public static final String INSPECT = "oyeshops.inspect";
|
||||
public static final String ADMIN = "oyeshops.admin";
|
||||
public static final String BREAK_OVERRIDE = "oyeshops.break.override";
|
||||
|
||||
/**
|
||||
* check if player has permission or is op
|
||||
*/
|
||||
public static boolean has(Player player, String permission) {
|
||||
return player.isOp() || player.hasPermission(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if sender has permission (for non-player senders)
|
||||
*/
|
||||
public static boolean has(CommandSender sender, String permission) {
|
||||
if (sender instanceof Player player) {
|
||||
return has(player, permission);
|
||||
}
|
||||
return sender.hasPermission(permission);
|
||||
}
|
||||
|
||||
public static boolean canCreate(Player player) {
|
||||
return has(player, CREATE);
|
||||
}
|
||||
|
||||
public static boolean canUse(Player player) {
|
||||
return has(player, USE);
|
||||
}
|
||||
|
||||
public static boolean canInspect(Player player) {
|
||||
return has(player, INSPECT) || has(player, ADMIN);
|
||||
}
|
||||
|
||||
public static boolean isAdmin(Player player) {
|
||||
return has(player, ADMIN);
|
||||
}
|
||||
|
||||
public static boolean isAdmin(CommandSender sender) {
|
||||
if (sender instanceof Player player) {
|
||||
return isAdmin(player);
|
||||
}
|
||||
return sender.hasPermission(ADMIN);
|
||||
}
|
||||
|
||||
public static boolean canBreakOverride(Player player) {
|
||||
return has(player, BREAK_OVERRIDE) || has(player, ADMIN);
|
||||
}
|
||||
}
|
||||
106
src/main/java/party/cybsec/oyeshops/registry/ShopRegistry.java
Normal file
106
src/main/java/party/cybsec/oyeshops/registry/ShopRegistry.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package party.cybsec.oyeshops.registry;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* in-memory shop cache
|
||||
* synchronized access, backed by sqlite
|
||||
*/
|
||||
public class ShopRegistry {
|
||||
private final Map<String, Shop> shopsByLocation = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, Shop> shopsById = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* register a shop
|
||||
*/
|
||||
public void register(Shop shop) {
|
||||
String key = locationKey(shop.getSignLocation());
|
||||
shopsByLocation.put(key, shop);
|
||||
shopsById.put(shop.getId(), shop);
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister a shop by location
|
||||
*/
|
||||
public void unregister(Location location) {
|
||||
String key = locationKey(location);
|
||||
Shop shop = shopsByLocation.remove(key);
|
||||
if (shop != null) {
|
||||
shopsById.remove(shop.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister a shop by id
|
||||
*/
|
||||
public void unregister(int shopId) {
|
||||
Shop shop = shopsById.remove(shopId);
|
||||
if (shop != null) {
|
||||
String key = locationKey(shop.getSignLocation());
|
||||
shopsByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister a shop
|
||||
*/
|
||||
public void unregister(Shop shop) {
|
||||
unregister(shop.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* get shop by location
|
||||
*/
|
||||
public Shop getShop(Location location) {
|
||||
String key = locationKey(location);
|
||||
return shopsByLocation.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* get shop by id
|
||||
*/
|
||||
public Shop getShop(int shopId) {
|
||||
return shopsById.get(shopId);
|
||||
}
|
||||
|
||||
/**
|
||||
* get shop by id (alias)
|
||||
*/
|
||||
public Shop getShopById(int shopId) {
|
||||
return shopsById.get(shopId);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all shops
|
||||
*/
|
||||
public Collection<Shop> getAllShops() {
|
||||
return shopsById.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* check if location has a shop
|
||||
*/
|
||||
public boolean hasShop(Location location) {
|
||||
return shopsByLocation.containsKey(locationKey(location));
|
||||
}
|
||||
|
||||
/**
|
||||
* clear all shops
|
||||
*/
|
||||
public void clear() {
|
||||
shopsByLocation.clear();
|
||||
shopsById.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* create location key for map lookup
|
||||
*/
|
||||
private String locationKey(Location loc) {
|
||||
return loc.getWorld().getUID() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package party.cybsec.oyeshops.transaction;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Barrel;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.model.Trade;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* handles atomic shop transactions with rollback
|
||||
*/
|
||||
public class TransactionManager {
|
||||
private final OyeShopsPlugin plugin;
|
||||
|
||||
public TransactionManager(OyeShopsPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public enum Result {
|
||||
SUCCESS,
|
||||
INSUFFICIENT_FUNDS,
|
||||
INSUFFICIENT_STOCK,
|
||||
INVENTORY_FULL,
|
||||
DATABASE_ERROR,
|
||||
SHOP_DISABLED
|
||||
}
|
||||
|
||||
/**
|
||||
* execute a transaction for the given number of units
|
||||
*/
|
||||
public Result execute(Player buyer, Shop shop, int units) {
|
||||
if (!shop.isEnabled()) {
|
||||
return Result.SHOP_DISABLED;
|
||||
}
|
||||
|
||||
Trade trade = shop.getTrade();
|
||||
int totalPrice = trade.priceQuantity() * units;
|
||||
int totalProduct = trade.productQuantity() * units;
|
||||
|
||||
// get chest inventory
|
||||
Inventory chestInventory = getShopInventory(shop);
|
||||
if (chestInventory == null) {
|
||||
return Result.DATABASE_ERROR;
|
||||
}
|
||||
|
||||
// verify buyer has required items
|
||||
if (!hasItems(buyer.getInventory(), trade.priceItem(), totalPrice)) {
|
||||
return Result.INSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// verify chest has required stock
|
||||
if (!hasItems(chestInventory, trade.productItem(), totalProduct)) {
|
||||
return Result.INSUFFICIENT_STOCK;
|
||||
}
|
||||
|
||||
// take snapshots for rollback
|
||||
ItemStack[] buyerSnapshot = buyer.getInventory().getContents().clone();
|
||||
for (int i = 0; i < buyerSnapshot.length; i++) {
|
||||
if (buyerSnapshot[i] != null) {
|
||||
buyerSnapshot[i] = buyerSnapshot[i].clone();
|
||||
}
|
||||
}
|
||||
|
||||
ItemStack[] chestSnapshot = chestInventory.getContents().clone();
|
||||
for (int i = 0; i < chestSnapshot.length; i++) {
|
||||
if (chestSnapshot[i] != null) {
|
||||
chestSnapshot[i] = chestSnapshot[i].clone();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// remove price items from buyer
|
||||
removeItems(buyer.getInventory(), trade.priceItem(), totalPrice);
|
||||
|
||||
// remove product items from chest
|
||||
removeItems(chestInventory, trade.productItem(), totalProduct);
|
||||
|
||||
// add product items to buyer
|
||||
HashMap<Integer, ItemStack> overflow = buyer.getInventory().addItem(
|
||||
createItemStacks(trade.productItem(), totalProduct));
|
||||
|
||||
if (!overflow.isEmpty()) {
|
||||
// rollback
|
||||
buyer.getInventory().setContents(buyerSnapshot);
|
||||
chestInventory.setContents(chestSnapshot);
|
||||
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);
|
||||
}
|
||||
|
||||
return Result.SUCCESS;
|
||||
|
||||
} catch (SQLException e) {
|
||||
// rollback on database error
|
||||
buyer.getInventory().setContents(buyerSnapshot);
|
||||
chestInventory.setContents(chestSnapshot);
|
||||
e.printStackTrace();
|
||||
return Result.DATABASE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if inventory has required items
|
||||
*/
|
||||
private boolean hasItems(Inventory inventory, Material material, int amount) {
|
||||
int count = 0;
|
||||
for (ItemStack item : inventory.getContents()) {
|
||||
if (item != null && item.getType() == material) {
|
||||
count += item.getAmount();
|
||||
if (count >= amount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove items from inventory
|
||||
*/
|
||||
private void removeItems(Inventory inventory, Material material, int amount) {
|
||||
int remaining = amount;
|
||||
|
||||
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);
|
||||
if (toRemove == item.getAmount()) {
|
||||
inventory.setItem(i, null);
|
||||
} else {
|
||||
item.setAmount(item.getAmount() - toRemove);
|
||||
}
|
||||
remaining -= toRemove;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create item stacks for given total amount
|
||||
*/
|
||||
private ItemStack[] createItemStacks(Material material, int totalAmount) {
|
||||
int maxStack = material.getMaxStackSize();
|
||||
int fullStacks = totalAmount / maxStack;
|
||||
int remainder = totalAmount % maxStack;
|
||||
|
||||
int arraySize = fullStacks + (remainder > 0 ? 1 : 0);
|
||||
ItemStack[] stacks = new ItemStack[arraySize];
|
||||
|
||||
for (int i = 0; i < fullStacks; i++) {
|
||||
stacks[i] = new ItemStack(material, maxStack);
|
||||
}
|
||||
|
||||
if (remainder > 0) {
|
||||
stacks[fullStacks] = new ItemStack(material, remainder);
|
||||
}
|
||||
|
||||
return stacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the shop's container inventory
|
||||
*/
|
||||
private Inventory getShopInventory(Shop shop) {
|
||||
Location signLoc = shop.getSignLocation();
|
||||
Block signBlock = signLoc.getBlock();
|
||||
|
||||
if (!(signBlock.getBlockData() instanceof WallSign wallSign)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockFace attachedFace = wallSign.getFacing().getOppositeFace();
|
||||
Block containerBlock = signBlock.getRelative(attachedFace);
|
||||
|
||||
if (containerBlock.getState() instanceof Chest chest) {
|
||||
return chest.getInventory();
|
||||
} else if (containerBlock.getState() instanceof Barrel barrel) {
|
||||
return barrel.getInventory();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
29
src/main/resources/config.yml
Normal file
29
src/main/resources/config.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# hopper protection
|
||||
hoppers:
|
||||
allow-product-output: true
|
||||
block-price-input: true
|
||||
|
||||
# transaction history
|
||||
history:
|
||||
max-transactions-per-shop: 100
|
||||
auto-prune: true
|
||||
|
||||
# material aliases
|
||||
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
|
||||
34
src/main/resources/plugin.yml
Normal file
34
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
name: oyeShops
|
||||
version: 1.0.0
|
||||
main: party.cybsec.oyeshops.OyeShopsPlugin
|
||||
api-version: '1.21'
|
||||
description: deterministic item-for-item chest barter
|
||||
author: cybsec
|
||||
website: https://party.cybsec
|
||||
|
||||
commands:
|
||||
oyeshops:
|
||||
description: oyeShops commands
|
||||
usage: /oyeshops <on|off|toggle|reload|inspect|spoof|enable|disable|unregister>
|
||||
aliases: [oyes, oshop]
|
||||
|
||||
permissions:
|
||||
oyeshops.create:
|
||||
description: allows placing valid shop signs (requires opt-in)
|
||||
default: true
|
||||
|
||||
oyeshops.use:
|
||||
description: allows buying from shops
|
||||
default: true
|
||||
|
||||
oyeshops.inspect:
|
||||
description: allows admin inspection via inspect mode
|
||||
default: op
|
||||
|
||||
oyeshops.admin:
|
||||
description: full control including spoof, reload, shop management
|
||||
default: op
|
||||
|
||||
oyeshops.break.override:
|
||||
description: allows breaking any shop sign or chest/barrel
|
||||
default: op
|
||||
Reference in New Issue
Block a user