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

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

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