feat: 表示のUpdate

This commit is contained in:
Keisuke Hirata 2025-12-12 06:28:23 +09:00
parent 79d34ee2dc
commit bbb88cea8f
4 changed files with 127 additions and 52 deletions

View File

@ -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: <Item Name> (Aqua)
// L2: x<Amount> - <Price Formatted> (Black)
// L3: (Empty)
// L4: <Owner Name> (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()
})
}
}

View File

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

View File

@ -77,6 +77,13 @@ object ShopRepository {
}
}
fun findAll(): List<ShopData> {
return HcuDatabase.transaction {
Shops.selectAll()
.map { toShopData(it) }
}
}
fun updateStock(id: Int, newStock: Int) {
HcuDatabase.transaction {
Shops.update({ Shops.id eq id }) {

View File

@ -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: <Item Name>
// L2: Buy: <Price>
// L3: Stock: <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