Добавление покупки по табличке.

This commit is contained in:
justuser-31 2025-11-30 15:40:38 +03:00
parent 79d21942bb
commit 5bd184ec2a
2 changed files with 841 additions and 563 deletions

View File

@ -0,0 +1,163 @@
package main.VpcSpigotIntegration
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.INVOICE_CREATION_TIMES
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.LOGGER
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PAY_BY_SIGN
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PLUGIN
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PREFIX
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.SIGN_PAYMENT_INFO
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.TO_PAY_SIGN_INVOICES
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USERNAME
import net.md_5.bungee.api.chat.ClickEvent
import net.md_5.bungee.api.chat.ComponentBuilder
import net.md_5.bungee.api.chat.HoverEvent
import net.md_5.bungee.api.chat.TextComponent
import org.bukkit.Bukkit
import org.bukkit.Bukkit.getWorld
import org.bukkit.ChatColor
import org.bukkit.Location
import org.bukkit.event.EventHandler
import org.bukkit.event.block.BlockEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.Material
import org.bukkit.block.Chest
import org.bukkit.block.Container
import org.bukkit.block.Sign
import org.bukkit.entity.Player
import org.bukkit.event.Listener
import org.bukkit.event.block.Action
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.block.SignChangeEvent
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.java.JavaPlugin
import java.util.Locale
import java.util.Locale.getDefault
class SignPaymentInfo(
public val amount: Int,
public val type: Material,
public val container: Chest,
public val player: Player,
public val cost: Double,
public val dst_username: String
)
class SignHandler: Listener {
@EventHandler
fun onClick(event: PlayerInteractEvent) {
// Check if feature enabled
if (!PAY_BY_SIGN) return
// Check if clicked block is a sign
val block = event.clickedBlock!!
if (block.type != Material.SIGN_POST && block.type != Material.WALL_SIGN) return
// Verify it's actually a sign state
val state = block.state
if (state !is Sign) return
// Check if it is RIGHT CLICK
if (event.action != Action.RIGHT_CLICK_BLOCK) return
// Check if this is good formatted sign
val sign = block.state as Sign
val clearFirstLine = ChatColor.stripColor(sign.getLine(0)).trim()
val clearPrefix = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', PREFIX)).trim()
if (clearFirstLine != clearPrefix) return
// Check if player already have open invoices
if (TO_PAY_SIGN_INVOICES.contains(event.player.name)) {
Utils.send(event.player, "&cУ вас уже есть открытый счёт, сначала оплатите его.")
return
}
val blockFace = event.blockFace.toString()
LOGGER.info("Block face: $blockFace")
var cordX: Int = event.clickedBlock.location.x.toInt()
var cordY: Int = event.clickedBlock.location.y.toInt()
var cordZ: Int = event.clickedBlock.location.z.toInt()
LOGGER.info("Cords: $cordX $cordY $cordZ")
when {
blockFace == "EAST" -> {
cordX -= 1
}
blockFace == "NORTH" -> {
cordZ += 1
}
blockFace == "WEST" -> {
cordX += 1
}
blockFace == "SOUTH" -> {
cordZ -= 1
}
}
val containerBlock = getWorld(event.clickedBlock.world.uid).getBlockAt(cordX, cordY, cordZ)
LOGGER.info("Container block: $cordX $cordY $cordZ")
val container: Chest = containerBlock.state as Chest
val firstItem = container.blockInventory.contents[0]
LOGGER.info("firstItem: ${firstItem.type} | amount: ${firstItem.amount}")
// Get info
val vpcUsername = sign.getLine(1)
val costPerItem: Double? = sign.getLine(2).toDoubleOrNull()
val amount: Int? = sign.getLine(3).toIntOrNull()
if (vpcUsername.isEmpty() || costPerItem == null || amount == null) return
LOGGER.info("vpcUsername: $vpcUsername")
LOGGER.info("costPerItem: $costPerItem")
LOGGER.info("amount: $amount")
// Fixed: Properly check if there are enough items of the required type
val availableAmount = container.inventory.all(firstItem.type).values.sumOf { it.amount }
if (availableAmount < amount) {
LOGGER.info("Not enough items: [type: ${firstItem.type}, requested: $amount, available: $availableAmount]")
Utils.send(event.player,"&cВ контейнере не хватает предметов для покупки.")
return
}
// Generate invoice etc.
Bukkit.getScheduler().runTaskAsynchronously(PLUGIN, Runnable {
val cost = costPerItem*amount
val invoiceId = VpcApi.create_invoice(cost).toString()
LOGGER.info("Invoice id: $invoiceId")
if (invoiceId == "null") {
Utils.send(event.player,"&cПроизошла ошибка при генерации счёта. Обратитесь к администратору или повторите позже.")
return@Runnable
}
// Add data to map
TO_PAY_SIGN_INVOICES[event.player.name] = invoiceId
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
SIGN_PAYMENT_INFO[invoiceId] = SignPaymentInfo(amount, firstItem.type, container, event.player, cost, vpcUsername)
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите здесь, оплатить &6$cost VPC")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpc pay $USERNAME $cost $invoiceId")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpc pay $USERNAME $cost $invoiceId").create())
event.player.spigot().sendMessage(message)
})
}
@EventHandler
fun onSignChange(event: SignChangeEvent) {
// Check if feature enabled
if (!PAY_BY_SIGN) return
if (event.getLine(0).contains("[VPC]")) {
val vpcUsername = event.getLine(1)
val cost: Double? = event.getLine(2).toDoubleOrNull()
val amount: Int? = event.getLine(3).toIntOrNull()
var coloredLine: String
// Check if some data is missing
if (vpcUsername.isEmpty() || cost == null || amount == null) {
coloredLine = ChatColor.translateAlternateColorCodes('&', "$PREFIX&cError")
event.setLine(0, coloredLine)
return
}
coloredLine = ChatColor.translateAlternateColorCodes('&', PREFIX)
event.setLine(0, coloredLine)
}
}
}

View File

@ -4,6 +4,7 @@ package main.VpcSpigotIntegration
// Here you will see the main logic // Here you will see the main logic
// ---------------------------------------------- // ----------------------------------------------
import com.sun.org.apache.xpath.internal.operations.Bool
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.command.Command import org.bukkit.command.Command
@ -15,6 +16,8 @@ import net.md_5.bungee.api.chat.HoverEvent
import net.md_5.bungee.api.chat.ClickEvent import net.md_5.bungee.api.chat.ClickEvent
import net.md_5.bungee.api.chat.TextComponent import net.md_5.bungee.api.chat.TextComponent
import org.bukkit.ChatColor import org.bukkit.ChatColor
import org.bukkit.Material
import org.bukkit.inventory.ItemStack
import org.bukkit.scheduler.BukkitRunnable import org.bukkit.scheduler.BukkitRunnable
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -40,6 +43,7 @@ class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
lateinit var COURSE_DYNAMIC_COMMAND: String lateinit var COURSE_DYNAMIC_COMMAND: String
var COURSE_COMMISSION: Float = DEFAULT_COURSE_COMMISSION var COURSE_COMMISSION: Float = DEFAULT_COURSE_COMMISSION
var INVOICE_TIMEOUT_SECONDS: Long = DEFAULT_INVOICE_TIMEOUT_SECONDS var INVOICE_TIMEOUT_SECONDS: Long = DEFAULT_INVOICE_TIMEOUT_SECONDS
var PAY_BY_SIGN: Boolean = true
// Default configuration values // Default configuration values
const val DEFAULT_USERNAME: String = "test" const val DEFAULT_USERNAME: String = "test"
@ -55,6 +59,7 @@ class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
const val DEFAULT_COURSE_DYNAMIC_COMMAND: String = "baltop force" const val DEFAULT_COURSE_DYNAMIC_COMMAND: String = "baltop force"
const val DEFAULT_COURSE_COMMISSION: Float = 5.0f const val DEFAULT_COURSE_COMMISSION: Float = 5.0f
const val DEFAULT_INVOICE_TIMEOUT_SECONDS: Long = 300 // 5 minutes const val DEFAULT_INVOICE_TIMEOUT_SECONDS: Long = 300 // 5 minutes
const val DEFAULT_PAY_BY_SIGN: Boolean = true
// Static configurations // Static configurations
const val PREFIX = "&9[&bVPC&7-&6I&9] &3" const val PREFIX = "&9[&bVPC&7-&6I&9] &3"
@ -70,11 +75,17 @@ class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
var TO_AUTH_PLAYERS: MutableMap<String, String> = mutableMapOf() // Pair: {player: vpc_username} var TO_AUTH_PLAYERS: MutableMap<String, String> = mutableMapOf() // Pair: {player: vpc_username}
@JvmStatic @JvmStatic
var TO_AUTH_PLAYERS_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id} var TO_AUTH_PLAYERS_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic @JvmStatic
var TO_PAY_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id} var TO_PAY_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic @JvmStatic
var INVOICES_AMOUNT: MutableMap<String, Double> = mutableMapOf() // How many should we pay to player after invoice? var INVOICES_AMOUNT: MutableMap<String, Double> = mutableMapOf() // How many should we pay to player after invoice?
@JvmStatic
var TO_PAY_SIGN_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic
var SIGN_PAYMENT_INFO: MutableMap<String, SignPaymentInfo> = mutableMapOf() // Information about amount, type and block
// Track invoice creation times for timeout handling // Track invoice creation times for timeout handling
@JvmStatic @JvmStatic
var INVOICE_CREATION_TIMES: MutableMap<String, Long> = mutableMapOf() // Pair: {invoice_id: creation_timestamp} var INVOICE_CREATION_TIMES: MutableMap<String, Long> = mutableMapOf() // Pair: {invoice_id: creation_timestamp}
@ -115,6 +126,7 @@ class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
COURSE_DYNAMIC_COMMAND = properties.getProperty("course_dynamic_command", DEFAULT_COURSE_DYNAMIC_COMMAND).replace("'", "") COURSE_DYNAMIC_COMMAND = properties.getProperty("course_dynamic_command", DEFAULT_COURSE_DYNAMIC_COMMAND).replace("'", "")
COURSE_COMMISSION = properties.getProperty("course_commission", DEFAULT_COURSE_COMMISSION.toString()).replace("'", "").toFloat() COURSE_COMMISSION = properties.getProperty("course_commission", DEFAULT_COURSE_COMMISSION.toString()).replace("'", "").toFloat()
INVOICE_TIMEOUT_SECONDS = properties.getProperty("invoice_timeout_seconds", DEFAULT_INVOICE_TIMEOUT_SECONDS.toString()).replace("'", "").toLong() INVOICE_TIMEOUT_SECONDS = properties.getProperty("invoice_timeout_seconds", DEFAULT_INVOICE_TIMEOUT_SECONDS.toString()).replace("'", "").toLong()
PAY_BY_SIGN = properties.getProperty("pay_by_sign", DEFAULT_PAY_BY_SIGN.toString()).replace("'", "").toBoolean()
logger.info("Configuration loaded successfully - Username: $USERNAME, API URL: $USER_API_URL") logger.info("Configuration loaded successfully - Username: $USERNAME, API URL: $USER_API_URL")
} catch (e: Exception) { } catch (e: Exception) {
@ -184,6 +196,11 @@ course_commission=$DEFAULT_COURSE_COMMISSION
# After how many seconds unpaid invoices will be deleted (default: 300 seconds = 5 minutes) # After how many seconds unpaid invoices will be deleted (default: 300 seconds = 5 minutes)
invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
# -------------------- END ------------------------ # -------------------- END ------------------------
# ------------------ Features ---------------------
# Should we enable pay by signs from containers (chest/etc.)?
pay_by_sign=${DEFAULT_PAY_BY_SIGN}
# -------------------- END ------------------------
""".trimIndent() """.trimIndent()
) )
} }
@ -201,6 +218,8 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
// Register command executor // Register command executor
getCommand("vpi")?.setExecutor(this) getCommand("vpi")?.setExecutor(this)
// Register clicking on sign
server.pluginManager.registerEvents(SignHandler(), this)
// Background checks such a auth, invoice check, ... // Background checks such a auth, invoice check, ...
startBackgroundChecks() startBackgroundChecks()
@ -563,6 +582,8 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
override fun run() { override fun run() {
processAuthenticationInvoices() processAuthenticationInvoices()
processPaymentInvoices() processPaymentInvoices()
processSignInvoices()
cleanupExpiredInvoices() cleanupExpiredInvoices()
} }
}.runTaskTimerAsynchronously(this, 0L, 20L) }.runTaskTimerAsynchronously(this, 0L, 20L)
@ -661,6 +682,69 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
} }
} }
private fun processSignInvoices() {
synchronized(TO_PAY_SIGN_INVOICES) {
if (TO_PAY_SIGN_INVOICES.isNotEmpty()) {
val iterator = TO_PAY_SIGN_INVOICES.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
try {
val result = VpcApi.get_invoice(entry.value) as Map<*, *>?
if (result != null) {
val status = result["status"]
LOGGER.info("Payment invoice ${entry.value} status: $status")
if (status != null && status.toString() == "true") {
LOGGER.info("Processing successful payment")
VpcApi.delete_invoice(entry.value)
// Check if we have payment info for this entry
val paymentInfo = SIGN_PAYMENT_INFO[entry.value]
if (paymentInfo == null) {
LOGGER.error("No payment info found for key: ${entry.value}")
return
}
val itemStack = ItemStack(paymentInfo.type, paymentInfo.amount)
val type = paymentInfo.type
// Check if container have needed items
val availableAmount = paymentInfo.container.inventory.all(type).values.sumOf { it.amount }
if (availableAmount < paymentInfo.amount) {
LOGGER.error("Not enough items after pay invoice, try to return VPC...")
val vpcUsername = DataManager.getPlayerVPCUsername(paymentInfo.player.name).toString()
val transferResult = VpcApi.transfer_coins(vpcUsername, paymentInfo.cost).toString()
if (transferResult != "OK") {
LOGGER.error("Can't return VPC, result: $transferResult")
}
} else {
// If we have needed items
paymentInfo.player.inventory.addItem(itemStack)
paymentInfo.container.inventory.removeItem(itemStack)
val transferResult = VpcApi.transfer_coins(paymentInfo.dst_username, paymentInfo.cost).toString()
if (transferResult != "OK") {
LOGGER.error("Can't transfer VPC, result: $transferResult")
}
}
// Clean up tracking maps
INVOICE_CREATION_TIMES.remove(entry.value)
SIGN_PAYMENT_INFO.remove(entry.value)
iterator.remove()
}
} else {
logger.warning("Received null result for payment invoice ${entry.value}")
}
} catch (e: Exception) {
LOGGER.error("Error processing payment invoice ${entry.value}: ${e.message}")
e.printStackTrace()
}
}
}
}
}
/** /**
* Cleanup expired invoices based on timeout configuration * Cleanup expired invoices based on timeout configuration
*/ */
@ -729,5 +813,36 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
} }
} }
} }
// Cleanup sign invoices
synchronized(TO_PAY_SIGN_INVOICES) {
val paymentIterator = TO_PAY_SIGN_INVOICES.iterator()
while (paymentIterator.hasNext()) {
val entry = paymentIterator.next()
val invoiceId = entry.value
val creationTime = INVOICE_CREATION_TIMES[invoiceId] ?: continue
LOGGER.info("invoice ( $invoiceId ) left to live: ${timeoutMillis - (currentTime - creationTime)}")
if (currentTime - creationTime > timeoutMillis) {
LOGGER.info("Deleting expired payment invoice: $invoiceId")
try {
VpcApi.delete_invoice(invoiceId)
} catch (e: Exception) {
LOGGER.error("Error deleting expired payment invoice $invoiceId: ${e.message}")
}
// Clean up tracking maps
SIGN_PAYMENT_INFO.remove(invoiceId)
INVOICE_CREATION_TIMES.remove(invoiceId)
paymentIterator.remove()
// Notify player if online
val player = Bukkit.getPlayer(entry.key)
if (player != null && player.isOnline) {
Utils.send(player, "&cВаша оплата истекла. Нажмите ещё раз на табличку.")
}
}
}
}
} }
} }