feat: setup command, help book updates, and db migration
This commit is contained in:
@@ -9,10 +9,11 @@ description = "deterministic item-for-item chest barter"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
maven("https://repo.papermc.io/repository/maven-snapshots/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
|
||||
implementation("org.xerial:sqlite-jdbc:3.47.1.0")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ import party.cybsec.oyeshops.model.PendingActivation;
|
||||
import party.cybsec.oyeshops.model.Shop;
|
||||
import party.cybsec.oyeshops.permission.PermissionManager;
|
||||
import party.cybsec.oyeshops.gui.HelpBook;
|
||||
import party.cybsec.oyeshops.gui.SetupDialog;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.type.WallSign;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
@@ -43,6 +47,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
case "notify" -> handleNotifyToggle(sender);
|
||||
case "info" -> handleInfo(sender);
|
||||
case "help" -> handleHelp(sender);
|
||||
case "setup" -> handleSetup(sender);
|
||||
case "reload" -> handleReload(sender);
|
||||
case "inspect", "i" -> handleInspect(sender);
|
||||
case "spoof", "s" -> handleSpoof(sender);
|
||||
@@ -58,6 +63,8 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
sender.sendMessage(Component.text("=== oyeshops commands ===", NamedTextColor.GOLD));
|
||||
sender.sendMessage(Component.text("/oyeshops help", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - open interactive guide", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops setup", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - open shop wizard", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops on", NamedTextColor.YELLOW)
|
||||
.append(Component.text(" - enable shop creation", NamedTextColor.GRAY)));
|
||||
sender.sendMessage(Component.text("/oyeshops off", NamedTextColor.YELLOW)
|
||||
@@ -247,6 +254,18 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
plugin.getPlayerPreferenceRepository().enableShops(player.getUniqueId());
|
||||
player.sendMessage(Component.text("shop creation enabled. you can now make chest shops!",
|
||||
NamedTextColor.GREEN));
|
||||
|
||||
// show help book on first enable
|
||||
if (!plugin.getPlayerPreferenceRepository().hasSeenIntro(player.getUniqueId())) {
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
HelpBook.open(player);
|
||||
try {
|
||||
plugin.getPlayerPreferenceRepository().setSeenIntro(player.getUniqueId());
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, 20L); // delay slightly
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
player.sendMessage(Component.text("database error", NamedTextColor.RED));
|
||||
e.printStackTrace();
|
||||
@@ -354,7 +373,7 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (args.length == 1) {
|
||||
List<String> subCommands = new ArrayList<>(
|
||||
List.of("help", "on", "off", "toggle", "notify", "info", "enable", "disable"));
|
||||
List.of("help", "setup", "on", "off", "toggle", "notify", "info", "enable", "disable"));
|
||||
if (PermissionManager.isAdmin(sender)) {
|
||||
subCommands.addAll(List.of("reload", "inspect", "spoof", "unregister"));
|
||||
}
|
||||
@@ -377,4 +396,43 @@ public class AdminCommands implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
private void handleSetup(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("players only", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.canCreate(player)) {
|
||||
sender.sendMessage(Component.text("no permission", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
Block block = player.getTargetBlockExact(5);
|
||||
if (block == null || !(block.getBlockData() instanceof WallSign wallSign)) {
|
||||
player.sendMessage(Component.text("you must look at a wall sign to use the wizard", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
// duplicate validation logic from ShopActivationListener for safety
|
||||
BlockFace attachedFace = wallSign.getFacing().getOppositeFace();
|
||||
Block attachedBlock = block.getRelative(attachedFace);
|
||||
|
||||
if (!party.cybsec.oyeshops.listener.ShopActivationListener.isContainer(attachedBlock.getType())) {
|
||||
player.sendMessage(Component.text("sign must be on a chest or barrel", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PermissionManager.isAdmin(player)) {
|
||||
java.util.UUID sessionOwner = plugin.getContainerMemoryManager().getOwner(attachedBlock.getLocation());
|
||||
if (sessionOwner == null || !sessionOwner.equals(player.getUniqueId())) {
|
||||
player.sendMessage(
|
||||
Component.text("you can only create shops on containers you placed this session",
|
||||
NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SetupDialog.open(player, block, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,18 @@ public class DatabaseManager {
|
||||
player_uuid text primary key,
|
||||
shops_enabled boolean not null default false,
|
||||
notify_low_stock boolean not null default false,
|
||||
seen_intro boolean not null default false,
|
||||
enabled_at integer
|
||||
)
|
||||
""");
|
||||
|
||||
// migration: add seen_intro if missing
|
||||
try {
|
||||
stmt.execute("alter table player_preferences add column seen_intro boolean not null default false");
|
||||
} catch (SQLException ignored) {
|
||||
// column likely exists
|
||||
}
|
||||
|
||||
// indexes
|
||||
stmt.execute("""
|
||||
create index if not exists idx_shop_location
|
||||
|
||||
@@ -15,9 +15,9 @@ import java.util.UUID;
|
||||
public class PlayerPreferenceRepository {
|
||||
private final DatabaseManager dbManager;
|
||||
|
||||
// in-memory cache for performance
|
||||
private final Set<UUID> enabledPlayers = new HashSet<>();
|
||||
private final Set<UUID> notifyPlayers = new HashSet<>();
|
||||
private final Set<UUID> seenIntroPlayers = new HashSet<>();
|
||||
|
||||
public PlayerPreferenceRepository(DatabaseManager dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
@@ -27,7 +27,7 @@ public class PlayerPreferenceRepository {
|
||||
* load all player preferences into cache
|
||||
*/
|
||||
public void loadPreferences() throws SQLException {
|
||||
String sql = "select player_uuid, shops_enabled, notify_low_stock from player_preferences";
|
||||
String sql = "select player_uuid, shops_enabled, notify_low_stock, seen_intro from player_preferences";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
@@ -39,6 +39,9 @@ public class PlayerPreferenceRepository {
|
||||
if (rs.getBoolean("notify_low_stock")) {
|
||||
notifyPlayers.add(uuid);
|
||||
}
|
||||
if (rs.getBoolean("seen_intro")) {
|
||||
seenIntroPlayers.add(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +60,31 @@ public class PlayerPreferenceRepository {
|
||||
return notifyPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if player has seen the intro
|
||||
*/
|
||||
public boolean hasSeenIntro(UUID playerUuid) {
|
||||
return seenIntroPlayers.contains(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* set seen intro flag
|
||||
*/
|
||||
public void setSeenIntro(UUID playerUuid) throws SQLException {
|
||||
String sql = """
|
||||
insert into player_preferences (player_uuid, seen_intro)
|
||||
values (?, true)
|
||||
on conflict(player_uuid) do update set seen_intro = true
|
||||
""";
|
||||
|
||||
try (PreparedStatement stmt = dbManager.getConnection().prepareStatement(sql)) {
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
seenIntroPlayers.add(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* enable shops for player
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,8 @@ public class HelpBook {
|
||||
.append(Component.text("oyeshops guide", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("welcome to the simple item barter system.", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("welcome to the simple item barter system.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("steps to start:", NamedTextColor.GRAY))
|
||||
@@ -31,17 +32,40 @@ public class HelpBook {
|
||||
.append(Component.text("1. type ", NamedTextColor.BLACK))
|
||||
.append(Component.text("/oyes on", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("2. place a container", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("3. place a wall sign", NamedTextColor.BLACK))
|
||||
.build());
|
||||
|
||||
// page 2: setup wizard
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("setup wizard", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("hate typing?", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("1. look at a sign", NamedTextColor.GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("2. type ", NamedTextColor.GRAY))
|
||||
.append(Component.text("/oyes setup", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("or just write ", NamedTextColor.GRAY))
|
||||
.append(Component.text("setup", NamedTextColor.BLUE, TextDecoration.BOLD))
|
||||
.append(Component.text(" on the first line of the sign.", NamedTextColor.GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text(
|
||||
"this opens a fancy menu where you can click to create your shop.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 2: the sign format
|
||||
pages.add(Component.text()
|
||||
.append(Component.text("creating a shop", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("write your trade on the sign like this:", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("write your trade on the sign like this:",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("line 1: ", NamedTextColor.GRAY))
|
||||
@@ -54,7 +78,8 @@ public class HelpBook {
|
||||
.append(Component.text("64 dirt", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("(order doesn't matter, it shows a confirmation)", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("(order doesn't matter, it shows a confirmation)",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
// page 3: auto detection
|
||||
@@ -62,7 +87,8 @@ public class HelpBook {
|
||||
.append(Component.text("auto detection", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("lazy? just put your items in the chest and write:", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("lazy? just put your items in the chest and write:",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("auto", NamedTextColor.BLUE, TextDecoration.BOLD))
|
||||
@@ -70,7 +96,8 @@ public class HelpBook {
|
||||
.append(Component.text("for 10 gold", NamedTextColor.BLACK))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("the plugin will see what's in the chest and fill it in for you.",
|
||||
.append(Component.text(
|
||||
"the plugin will see what's in the chest and fill it in for you.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.build());
|
||||
|
||||
@@ -79,7 +106,8 @@ public class HelpBook {
|
||||
.append(Component.text("ownership rules", NamedTextColor.GOLD, TextDecoration.BOLD))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("to prevent stealing, you can only make shops on containers you placed ",
|
||||
.append(Component.text(
|
||||
"to prevent stealing, you can only make shops on containers you placed ",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("this session", NamedTextColor.BLACK, TextDecoration.ITALIC))
|
||||
.append(Component.text(".", NamedTextColor.DARK_GRAY))
|
||||
@@ -113,7 +141,8 @@ public class HelpBook {
|
||||
.append(Component.newline())
|
||||
.append(Component.text("/oyes notify", NamedTextColor.BLUE))
|
||||
.append(Component.newline())
|
||||
.append(Component.text("get alerts when your shops run out of items.", NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text("get alerts when your shops run out of items.",
|
||||
NamedTextColor.DARK_GRAY))
|
||||
.append(Component.newline())
|
||||
.append(Component.newline())
|
||||
.append(Component.text("/oyes info", NamedTextColor.BLUE))
|
||||
|
||||
112
src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java
Normal file
112
src/main/java/party/cybsec/oyeshops/gui/SetupDialog.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package party.cybsec.oyeshops.gui;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import party.cybsec.oyeshops.OyeShopsPlugin;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
// import io.papermc.paper.dialog.Dialog;
|
||||
// import io.papermc.paper.dialog.DialogBase;
|
||||
// import io.papermc.paper.dialog.DialogType;
|
||||
// import io.papermc.paper.dialog.action.ActionButton;
|
||||
// import io.papermc.paper.dialog.action.DialogAction;
|
||||
// import io.papermc.paper.dialog.component.DialogInput;
|
||||
// import io.papermc.paper.dialog.event.DialogResponseView;
|
||||
|
||||
/**
|
||||
* opens a setup wizard for creating shops using paper's dialog api
|
||||
*
|
||||
* TODO: Uncomment and fix imports once Paper 1.21.8 API is available
|
||||
*/
|
||||
public class SetupDialog {
|
||||
|
||||
public static void open(Player player, Block signBlock, OyeShopsPlugin plugin) {
|
||||
player.sendMessage(Component.text("setup wizard is coming soon (waiting for paper 1.21.8 update)",
|
||||
NamedTextColor.YELLOW));
|
||||
|
||||
/*
|
||||
* Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||
* .base(DialogBase.builder(Component.text("shop setup wizard",
|
||||
* NamedTextColor.GOLD))
|
||||
* .inputs(List.of(
|
||||
* // product (selling)
|
||||
* DialogInput.text("product_item",
|
||||
* Component.text("what are you selling? (item name)", NamedTextColor.YELLOW))
|
||||
* .placeholder(Component.text("e.g. diamond"))
|
||||
* .build(),
|
||||
* DialogInput.numberRange("product_qty",
|
||||
* Component.text("how many per purchase?", NamedTextColor.YELLOW), 1, 64)
|
||||
* .step(1)
|
||||
* .initial(1)
|
||||
* .build(),
|
||||
*
|
||||
* // price (buying)
|
||||
* DialogInput.text("price_item",
|
||||
* Component.text("what do you want? (payment item)", NamedTextColor.GREEN))
|
||||
* .placeholder(Component.text("e.g. gold ingot"))
|
||||
* .build(),
|
||||
* DialogInput.numberRange("price_qty",
|
||||
* Component.text("how many?", NamedTextColor.GREEN), 1, 64)
|
||||
* .step(1)
|
||||
* .initial(1)
|
||||
* .build()))
|
||||
* .build())
|
||||
* .type(DialogType.confirmation(
|
||||
* ActionButton.create(
|
||||
* Component.text("create shop", TextColor.color(0xAEFFC1)),
|
||||
* Component.text("click to confirm trade details"),
|
||||
* 100,
|
||||
* DialogAction.customClick(
|
||||
* (view, audience) -> handleCallback(view, (Player) audience, signBlock,
|
||||
* plugin),
|
||||
* io.papermc.paper.dialog.action.ClickCallback.Options.builder().uses(1).build(
|
||||
* ))),
|
||||
* ActionButton.create(
|
||||
* Component.text("cancel", TextColor.color(0xFFA0B1)),
|
||||
* Component.text("discard changes"),
|
||||
* 100,
|
||||
* null // closes dialog
|
||||
* ))));
|
||||
*
|
||||
* player.showDialog(dialog);
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
* private static void handleCallback(DialogResponseView view, Player player,
|
||||
* Block signBlock, OyeShopsPlugin plugin) {
|
||||
* String productStr = view.getString("product_item");
|
||||
* int productQty = view.getFloat("product_qty").intValue();
|
||||
* String priceStr = view.getString("price_item");
|
||||
* int priceQty = view.getFloat("price_qty").intValue();
|
||||
*
|
||||
* // 1. parse materials
|
||||
* Material productMat = plugin.getSignParser().parseMaterial(productStr);
|
||||
* if (productMat == null) {
|
||||
* player.sendMessage(Component.text("invalid product item: " + productStr,
|
||||
* NamedTextColor.RED));
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* Material priceMat = plugin.getSignParser().parseMaterial(priceStr);
|
||||
* if (priceMat == null) {
|
||||
* player.sendMessage(Component.text("invalid payment item: " + priceStr,
|
||||
* NamedTextColor.RED));
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* // 2. create trade & activation
|
||||
* Trade trade = new Trade(priceMat, priceQty, productMat, productQty);
|
||||
* PendingActivation activation = new PendingActivation(
|
||||
* player.getUniqueId(),
|
||||
* signBlock.getLocation(),
|
||||
* trade,
|
||||
* System.currentTimeMillis());
|
||||
*
|
||||
* // 3. finalize shop immediately (skipping chat confirmation since dialog IS
|
||||
* confirmation)
|
||||
* plugin.getShopActivationListener().finalizeShop(player, activation);
|
||||
* }
|
||||
*/
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package party.cybsec.oyeshops.listener;
|
||||
|
||||
import party.cybsec.oyeshops.gui.SetupDialog;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
@@ -114,6 +116,14 @@ public class ShopActivationListener implements Listener {
|
||||
: "";
|
||||
}
|
||||
|
||||
// 3. check for setup wizard
|
||||
if (lines[0].equalsIgnoreCase("setup") && lines[1].isEmpty() && lines[2].isEmpty() && lines[3].isEmpty()) {
|
||||
// clear sign text to avoid "setup" staying on the sign
|
||||
event.line(0, Component.text(""));
|
||||
SetupDialog.open(player, block, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
// parse trade
|
||||
Trade trade = parser.parse(lines);
|
||||
if (trade == null) {
|
||||
@@ -339,7 +349,7 @@ public class ShopActivationListener implements Listener {
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isContainer(Material material) {
|
||||
public static boolean isContainer(Material material) {
|
||||
return material == Material.CHEST || material == Material.TRAPPED_CHEST || material == Material.BARREL;
|
||||
}
|
||||
|
||||
|
||||
@@ -259,4 +259,8 @@ public class SignParser {
|
||||
|
||||
private record ItemQuantity(Material material, int quantity) {
|
||||
}
|
||||
|
||||
public Material parseMaterial(String name) {
|
||||
return aliasRegistry.resolve(name);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user