feat: setup command, help book updates, and db migration

This commit is contained in:
2026-02-04 20:52:49 -05:00
parent e5bc3d1f14
commit 9eecc99f56
8 changed files with 372 additions and 122 deletions

View File

@@ -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")
}

View File

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

View File

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

View File

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

View File

@@ -15,129 +15,158 @@ import java.util.List;
*/
public class HelpBook {
public static void open(Player player) {
List<Component> pages = new ArrayList<>();
public static void open(Player player) {
List<Component> pages = new ArrayList<>();
// page 1: introduction
pages.add(Component.text()
.append(Component.text("oyeshops guide", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("welcome to the simple item barter system.", NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("steps to start:", NamedTextColor.GRAY))
.append(Component.newline())
.append(Component.text("1. type ", NamedTextColor.BLACK))
.append(Component.text("/oyes on", NamedTextColor.BLUE))
.append(Component.newline())
.append(Component.text("2. place a container", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("3. place a wall sign", NamedTextColor.BLACK))
.build());
// page 1: introduction
pages.add(Component.text()
.append(Component.text("oyeshops guide", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("welcome to the simple item barter system.",
NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("steps to start:", NamedTextColor.GRAY))
.append(Component.newline())
.append(Component.text("1. type ", NamedTextColor.BLACK))
.append(Component.text("/oyes on", NamedTextColor.BLUE))
.append(Component.newline())
.append(Component.text("3. place a wall sign", NamedTextColor.BLACK))
.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.newline())
.append(Component.newline())
.append(Component.text("line 1: ", NamedTextColor.GRAY))
.append(Component.text("1 diamond", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("line 2: ", NamedTextColor.GRAY))
.append(Component.text("for", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("line 3: ", NamedTextColor.GRAY))
.append(Component.text("64 dirt", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("(order doesn't matter, it shows a confirmation)", NamedTextColor.DARK_GRAY))
.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 3: auto detection
pages.add(Component.text()
.append(Component.text("auto detection", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("lazy? just put your items in the chest and write:", NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("auto", NamedTextColor.BLUE, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.text("for 10 gold", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("the plugin will see what's in the chest and fill it in for you.",
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.newline())
.append(Component.newline())
.append(Component.text("line 1: ", NamedTextColor.GRAY))
.append(Component.text("1 diamond", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("line 2: ", NamedTextColor.GRAY))
.append(Component.text("for", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("line 3: ", NamedTextColor.GRAY))
.append(Component.text("64 dirt", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("(order doesn't matter, it shows a confirmation)",
NamedTextColor.DARK_GRAY))
.build());
// page 4: protection and ownership
pages.add(Component.text()
.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 ",
NamedTextColor.DARK_GRAY))
.append(Component.text("this session", NamedTextColor.BLACK, TextDecoration.ITALIC))
.append(Component.text(".", NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text(
"if the server restarts, you can't turn old chests into shops. place a fresh one!",
NamedTextColor.DARK_GRAY))
.build());
// page 3: auto detection
pages.add(Component.text()
.append(Component.text("auto detection", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("lazy? just put your items in the chest and write:",
NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("auto", NamedTextColor.BLUE, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.text("for 10 gold", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.newline())
.append(Component.text(
"the plugin will see what's in the chest and fill it in for you.",
NamedTextColor.DARK_GRAY))
.build());
// page 5: containers
pages.add(Component.text()
.append(Component.text("supported blocks", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("- chests", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- barrels", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- trapped chests", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("double chests are fully supported. both halves are protected.",
NamedTextColor.DARK_GRAY))
.build());
// page 4: protection and ownership
pages.add(Component.text()
.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 ",
NamedTextColor.DARK_GRAY))
.append(Component.text("this session", NamedTextColor.BLACK, TextDecoration.ITALIC))
.append(Component.text(".", NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text(
"if the server restarts, you can't turn old chests into shops. place a fresh one!",
NamedTextColor.DARK_GRAY))
.build());
// page 6: utility commands
pages.add(Component.text()
.append(Component.text("handy commands", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("/oyes notify", NamedTextColor.BLUE))
.append(Component.newline())
.append(Component.text("get alerts when your shops run out of items.", NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("/oyes info", NamedTextColor.BLUE))
.append(Component.newline())
.append(Component.text("see who made this plugin.", NamedTextColor.DARK_GRAY))
.build());
// page 5: containers
pages.add(Component.text()
.append(Component.text("supported blocks", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("- chests", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- barrels", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- trapped chests", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("double chests are fully supported. both halves are protected.",
NamedTextColor.DARK_GRAY))
.build());
// page 7: tips
pages.add(Component.text()
.append(Component.text("pro tips", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("- use wall signs only.", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- use abbreviations like ", NamedTextColor.BLACK))
.append(Component.text("dia", NamedTextColor.BLUE))
.append(Component.text(" for diamond.", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- shops are ", NamedTextColor.BLACK))
.append(Component.text("off", NamedTextColor.RED))
.append(Component.text(" by default to prevent accidents.", NamedTextColor.BLACK))
.build());
// page 6: utility commands
pages.add(Component.text()
.append(Component.text("handy commands", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("/oyes notify", NamedTextColor.BLUE))
.append(Component.newline())
.append(Component.text("get alerts when your shops run out of items.",
NamedTextColor.DARK_GRAY))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("/oyes info", NamedTextColor.BLUE))
.append(Component.newline())
.append(Component.text("see who made this plugin.", NamedTextColor.DARK_GRAY))
.build());
Book book = Book.book(Component.text("oyeshops manual"), Component.text("oyeshops"), pages);
player.openBook(book);
}
// page 7: tips
pages.add(Component.text()
.append(Component.text("pro tips", NamedTextColor.GOLD, TextDecoration.BOLD))
.append(Component.newline())
.append(Component.newline())
.append(Component.text("- use wall signs only.", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- use abbreviations like ", NamedTextColor.BLACK))
.append(Component.text("dia", NamedTextColor.BLUE))
.append(Component.text(" for diamond.", NamedTextColor.BLACK))
.append(Component.newline())
.append(Component.text("- shops are ", NamedTextColor.BLACK))
.append(Component.text("off", NamedTextColor.RED))
.append(Component.text(" by default to prevent accidents.", NamedTextColor.BLACK))
.build());
Book book = Book.book(Component.text("oyeshops manual"), Component.text("oyeshops"), pages);
player.openBook(book);
}
}

View 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);
* }
*/
}

View File

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

View File

@@ -259,4 +259,8 @@ public class SignParser {
private record ItemQuantity(Material material, int quantity) {
}
public Material parseMaterial(String name) {
return aliasRegistry.resolve(name);
}
}