diff --git a/src/main/kotlin/net/hareworks/hcu/shop/ShopVisuals.kt b/src/main/kotlin/net/hareworks/hcu/shop/ShopVisuals.kt new file mode 100644 index 0000000..e153ba0 --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/shop/ShopVisuals.kt @@ -0,0 +1,63 @@ +package net.hareworks.hcu.shop + +import net.hareworks.hcu.core.player.PlayerIdServiceImpl +import net.hareworks.hcu.shop.database.ShopData +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.block.Sign +import org.bukkit.block.sign.Side +import org.bukkit.plugin.java.JavaPlugin + +object ShopVisuals { + fun updateSign(plugin: JavaPlugin, block: org.bukkit.block.Block, shopData: ShopData, side: Side) { + val state = block.state as? Sign ?: return + val signSide = state.getSide(side) + + // Fetch item name + val itemStack = shopData.item + val itemName = if (itemStack != null) { + itemStack.type.name.lowercase().split("_") + .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + .take(15) // Limit length to fit on sign if needed? + } else { + "Not Set" + } + + // Fetch Owner Name + val playerIdService = PlayerIdServiceImpl(plugin.logger) + val ownerName = try { + val entry = playerIdService.find(shopData.actorId) + if (entry != null) { + org.bukkit.Bukkit.getOfflinePlayer(entry.uuid).name ?: "Unknown" + } else { + "Unknown" + } + } catch (e: Exception) { + "Unknown" + } + + val amount = itemStack?.amount ?: 0 + val price = shopData.price.toInt() + val priceStr = net.hareworks.hcu.economy.api.EconomyService.format(price) + + // Format: + // L1: (Aqua) + // L2: x - (Black) + // L3: (Empty) + // L4: (Gray) + + signSide.line(0, Component.text(itemName, NamedTextColor.AQUA)) + signSide.line(1, Component.text("x$amount - $priceStr", NamedTextColor.BLACK)) + signSide.line(2, Component.empty()) + signSide.line(3, Component.text(ownerName, NamedTextColor.GRAY)) + + // Update the sign one tick later to ensure persistence compatibility + // Or if called from command, update immediately? + // Safer to always schedule if in doubt, or check constraints. + // For command, immediate update is fine, but for event, delay is needed. + // Let's us runTask always for safety. + plugin.server.scheduler.runTask(plugin, Runnable { + state.update() + }) + } +} diff --git a/src/main/kotlin/net/hareworks/hcu/shop/command/ShopCommands.kt b/src/main/kotlin/net/hareworks/hcu/shop/command/ShopCommands.kt index 21003b6..bc710a0 100644 --- a/src/main/kotlin/net/hareworks/hcu/shop/command/ShopCommands.kt +++ b/src/main/kotlin/net/hareworks/hcu/shop/command/ShopCommands.kt @@ -50,6 +50,17 @@ object ShopCommands { } } } + + literal("update-signs") { + permission { + description = "Force update all shop signs" + defaultValue = org.bukkit.permissions.PermissionDefault.OP + } + executes { + val count = updateAllSigns(plugin) + sender.sendMessage(Component.text("Updated $count shop signs.", NamedTextColor.GREEN)) + } + } } } } @@ -71,4 +82,21 @@ object ShopCommands { sender.inventory.addItem(signItem) sender.sendMessage(Component.text("Given Shop Sign Tier $tier", NamedTextColor.GREEN)) } + + private fun updateAllSigns(plugin: ShopPlugin): Int { + val shops = net.hareworks.hcu.shop.database.ShopRepository.findAll() + var count = 0 + for (shop in shops) { + val world = org.bukkit.Bukkit.getWorld(shop.worldUid) ?: continue + val block = world.getBlockAt(shop.x, shop.y, shop.z) + // Ideally we check if loaded, but getBlockAt loads chunk if not async? + // In main thread command, it's fine for now, but beware of lag with thousands of shops. + if (block.type == Material.OAK_SIGN) { + val side = if (shop.face == "FRONT") org.bukkit.block.sign.Side.FRONT else org.bukkit.block.sign.Side.BACK + net.hareworks.hcu.shop.ShopVisuals.updateSign(plugin, block, shop, side) + count++ + } + } + return count + } } diff --git a/src/main/kotlin/net/hareworks/hcu/shop/database/ShopRepository.kt b/src/main/kotlin/net/hareworks/hcu/shop/database/ShopRepository.kt index d997844..66f1859 100644 --- a/src/main/kotlin/net/hareworks/hcu/shop/database/ShopRepository.kt +++ b/src/main/kotlin/net/hareworks/hcu/shop/database/ShopRepository.kt @@ -77,6 +77,13 @@ object ShopRepository { } } + fun findAll(): List { + return HcuDatabase.transaction { + Shops.selectAll() + .map { toShopData(it) } + } + } + fun updateStock(id: Int, newStock: Int) { HcuDatabase.transaction { Shops.update({ Shops.id eq id }) { diff --git a/src/main/kotlin/net/hareworks/hcu/shop/listeners/ShopListener.kt b/src/main/kotlin/net/hareworks/hcu/shop/listeners/ShopListener.kt index c8717eb..457098d 100644 --- a/src/main/kotlin/net/hareworks/hcu/shop/listeners/ShopListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/shop/listeners/ShopListener.kt @@ -1,5 +1,6 @@ package net.hareworks.hcu.shop.listeners import net.hareworks.hcu.shop.ShopPlugin +import net.hareworks.hcu.shop.ShopVisuals import net.hareworks.hcu.shop.database.ShopRepository import net.hareworks.hcu.core.player.PlayerIdServiceImpl import net.kyori.adventure.text.Component @@ -57,16 +58,16 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { val signState = event.blockPlaced.state as? Sign ?: return // Front Text + // Handled by ShopVisuals later + // backSide.line(0, Component.text("Back")) ... + + // Just ensure color is black initially if needed, but Visuals will overwrite. val frontSide = signState.getSide(Side.FRONT) - frontSide.line(0, Component.text("Front")) - frontSide.line(1, Component.text("oak_log")) frontSide.color = DyeColor.BLACK frontSide.isGlowingText = false // Back Text val backSide = signState.getSide(Side.BACK) - backSide.line(0, Component.text("Back")) - backSide.line(1, Component.text("gunpowder")) backSide.color = DyeColor.BLACK backSide.isGlowingText = false @@ -88,18 +89,24 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { val actorId = playerEntry.actorId // Create FRONT shop - ShopRepository.create( + val frontShop = ShopRepository.create( actorId = actorId, location = event.blockPlaced.location, face = "FRONT" ) + if (frontShop != null) { + ShopVisuals.updateSign(plugin, event.blockPlaced, frontShop, Side.FRONT) + } // Create BACK shop - ShopRepository.create( + val backShop = ShopRepository.create( actorId = actorId, location = event.blockPlaced.location, face = "BACK" ) + if (backShop != null) { + ShopVisuals.updateSign(plugin, event.blockPlaced, backShop, Side.BACK) + } } // Spawn Displays @@ -223,22 +230,14 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { player.sendMessage(Component.text("Shop price set to $$price", NamedTextColor.GREEN)) // Update Actual Sign Visuals - updateShopSignVisuals(event.block, shopData.copy(price = price), event.side) + ShopVisuals.updateSign(plugin, event.block, shopData.copy(price = price), event.side) } else { player.sendMessage(Component.text("Invalid price format. Please enter a valid number.", NamedTextColor.RED)) - // Optionally reopen? For now just fail. } - // Clean up + // Clean up Map ShopInputMap.pendingInputs.remove(player.uniqueId) - // Cancel the raw update so we don't just paste "100" on the sign - // But we DO want to update it with the visual method above. - // Since we called updateShopSignVisuals which does state.update(), - // verifying if cancelling event interferes. - // Usually, event changes the state *after* this handler. - // If we cancel, no change happens. - // So we cancel, and manually set the text we want. event.isCancelled = true } else { @@ -251,34 +250,7 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { } } - private fun updateShopSignVisuals(block: org.bukkit.block.Block, shopData: net.hareworks.hcu.shop.database.ShopData, side: Side) { - val state = block.state as? Sign ?: return - val signSide = state.getSide(side) // Currently updating the side being edited - - // Fetch item name - val itemName = shopData.item?.type?.name?.lowercase()?.replace("_", " ") ?: "Unknown" - // Format: - // L1: - // L2: Buy: - // L3: Stock: - - signSide.line(0, Component.text(itemName, NamedTextColor.BLACK)) - signSide.line(1, Component.text("Buy: $${shopData.price}", NamedTextColor.BLACK)) - signSide.line(2, Component.text("Stock: ${shopData.stock}", NamedTextColor.BLACK)) - signSide.line(3, Component.empty()) - - // If updating the sign block during event cancellation, we might need a scheduler or force update - // But since we are cancelling the event, the state remains as is. - // We need to apply THESE changes. - // state.update() pushes to world. - - // Wait, if we are in SignChangeEvent, the block is currently processing update. - // It's safer to run a task to update it 1 tick later if we cancel the event. - plugin.server.scheduler.runTask(plugin, Runnable { - state.update() - }) - } @EventHandler fun onShopInteract(event: PlayerInteractEvent) { @@ -415,10 +387,16 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { } inventory.setItem(2, barrier) - // Update Display + // Update Display and Sign val updatedShop = ShopRepository.findById(shopId) if (updatedShop != null) { updateShopDisplay(updatedShop) + val world = org.bukkit.Bukkit.getWorld(updatedShop.worldUid) + val block = world?.getBlockAt(updatedShop.x, updatedShop.y, updatedShop.z) + if (block != null && block.type == Material.OAK_SIGN) { + val side = if (updatedShop.face == "FRONT") Side.FRONT else Side.BACK + ShopVisuals.updateSign(plugin, block, updatedShop, side) + } } } @@ -439,6 +417,12 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { val updatedShop = ShopRepository.findById(shopId) if (updatedShop != null) { updateShopDisplay(updatedShop) + val world = org.bukkit.Bukkit.getWorld(updatedShop.worldUid) + val block = world?.getBlockAt(updatedShop.x, updatedShop.y, updatedShop.z) + if (block != null && block.type == Material.OAK_SIGN) { + val side = if (updatedShop.face == "FRONT") Side.FRONT else Side.BACK + ShopVisuals.updateSign(plugin, block, updatedShop, side) + } } // Prompt for Price via Sign @@ -484,13 +468,6 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { // Determine side based on ShopData face val side = if (shopData.face == "FRONT") Side.FRONT else Side.BACK - val signSide = sign.getSide(side) - - // Send client-side update with prompt - // Using Paper API: player.sendBlockUpdate(location, blockData) doesnt allow setting text easily - // We can modify the sign state locally and send update, then revert? - // Or actually, `player.sendBlockUpdate` takes a `TileState` in recent Paper versions? - // If not, we can use `player.sendSignChange`. val promptLines = listOf( Component.text("Price : ", NamedTextColor.BLACK), @@ -499,7 +476,7 @@ class ShopListener(private val plugin: ShopPlugin) : Listener { Component.empty() ) - player.sendSignChange(location, promptLines, DyeColor.BLACK, true) // hasGlowingText=true/false? + player.sendSignChange(location, promptLines, DyeColor.BLACK, true) // Register pending input ShopInputMap.pendingInputs[player.uniqueId] = shopId