Compare commits

...

9 Commits

11 changed files with 1153 additions and 786 deletions

View File

@ -6,9 +6,6 @@
### Функциональность на данный момент:
* Конвертация VPC в локальную валюту и наоборот
### Разрабатывается:
* Оплата по табличке
* Покупка из сундука/... по табличке
# Как использовать?

12
pom.xml
View File

@ -5,11 +5,11 @@
<modelVersion>4.0.0</modelVersion>
<groupId>main</groupId>
<artifactId>vp_server_integration</artifactId>
<artifactId>VpcSpigotIntegration</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>vp_server_integration</name>
<name>VpcSpigotIntegration</name>
<properties>
<java.version>17</java.version>
@ -106,10 +106,10 @@
<configuration>
<target>
<echo message="UPLOADING file to ${ftps.username}@${ftps.host} ${srv.ftps.remote.directory}"/>
<echo message="curl -k --ssl-reqd --ftp-create-dirs -T target/vp_server_integration-1.0.jar ${ftps.username}:${ftps.password} ftp://${ftps.host}:${ftps.port}/${srv.ftps.remote.directory}"/>
<echo message="curl -k --ssl-reqd --ftp-create-dirs -T target/VpcSpigotIntegration-1.0.jar ${ftps.username}:${ftps.password} ftp://${ftps.host}:${ftps.port}/${srv.ftps.remote.directory}"/>
<exec executable="sh">
<arg value="-c"/>
<arg value="curl -k --ssl-reqd --ftp-create-dirs -T target/vp_server_integration-1.0.jar -u ${ftps.username}:${ftps.password} ftp://${ftps.host}:${ftps.port}/${srv.ftps.remote.directory}"/>
<arg value="curl -k --ssl-reqd --ftp-create-dirs -T target/VpcSpigotIntegration-1.0.jar -u ${ftps.username}:${ftps.password} ftp://${ftps.host}:${ftps.port}/${srv.ftps.remote.directory}"/>
</exec>
</target>
</configuration>
@ -125,10 +125,10 @@
<configuration>
<target>
<echo message="SENDING reload plugin request to ${srv.restart.endpoint}"/>-->
<echo message="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload vp_server_integration'"/>-->
<echo message="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload VpcSpigotIntegration'"/>-->
<exec executable="sh">
<arg value="-c"/>
<arg value="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload vp_server_integration'"/>
<arg value="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload VpcSpigotIntegration'"/>
</exec>
<!-- <echo message="SENDING restart request to ${srv.restart.endpoint}"/>-->
<!-- <echo message="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.restart.endpoint}"/>-->

View File

@ -1,11 +1,14 @@
package main.vp_server_integration
package main.VpcSpigotIntegration
// -------------- Capture output ----------------
// Here is implemented capture of output command (for dynamic course)
// ----------------------------------------------
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PLUGIN
import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CommandCapture {
companion object {
@ -34,7 +37,8 @@ class CommandCapture {
}
}
fun execute(command: String): List<String> {
// Deprecated stupid version for sync version which catch output not every time
fun executeOld(command: String): List<String> {
val output = mutableListOf<String>()
val customSender = createCapturingSender { message ->
@ -45,5 +49,61 @@ class CommandCapture {
return output
}
const val MAX_WAIT_TIME_MS = 5000
const val CHECK_INTERVAL_MS: Long = 300
fun execute(command: String): List<String> {
val output = mutableListOf<String>()
var isCommandFinished = false
val customSender = createCapturingSender { message ->
output.add(message)
}
Bukkit.getScheduler().runTask(PLUGIN) {
Bukkit.dispatchCommand(customSender, command)
}
// Wait for command completion with timeout
val startTime = System.currentTimeMillis()
while (!isCommandFinished && (System.currentTimeMillis() - startTime) < MAX_WAIT_TIME_MS) {
try {
Thread.sleep(CHECK_INTERVAL_MS)
if (output.isNotEmpty()) {
isCommandFinished = true
}
} catch (e: InterruptedException) {
break
}
}
return output
}
// Untested another version
fun executeV3(command: String): List<String> {
val output = mutableListOf<String>()
val latch = CountDownLatch(1)
val customSender = createCapturingSender { message ->
synchronized(output) {
output.add(message)
}
}
Bukkit.getScheduler().runTask(PLUGIN) {
try {
Bukkit.dispatchCommand(customSender, command)
} finally {
latch.countDown()
}
}
// Wait for command completion with timeout
latch.await(MAX_WAIT_TIME_MS/1000.toLong(), TimeUnit.SECONDS)
return output.toList()
}
}
}

View File

@ -1,7 +1,6 @@
package main.vp_server_integration
package main.VpcSpigotIntegration
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.plugin.java.JavaPlugin
import java.io.File
class DataManager(private val dataFolder: File) {

View File

@ -0,0 +1,164 @@
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_FEATURE
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.event.EventHandler
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.Material
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.SignChangeEvent
import org.bukkit.inventory.ItemStack
class SignPaymentInfo(
public val amount: Int,
public val type: Material,
public val container: Container,
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_FEATURE) 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)) {
val message = TextComponent(ChatColor.translateAlternateColorCodes('&',"&cУ вас уже есть открытый счёт, сначала оплатите его или &6отмените."))
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpi signPay cancel")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpi signPay cancel").create())
event.player.spigot().sendMessage(message)
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: Container = containerBlock.state as Container
LOGGER.info("Content: ${container.inventory.contents}")
var firstItem = container.inventory.contents.filterNotNull().firstOrNull()
if (firstItem == null) {
Utils.send(event.player,"&cВ контейнере нет предметов для покупки.")
}
firstItem = firstItem as ItemStack
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_FEATURE) 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 || amount < 0 || cost < 0 || cost < 0.0001) {
coloredLine = ChatColor.translateAlternateColorCodes('&', "$PREFIX&cError")
event.setLine(0, coloredLine)
return
}
coloredLine = ChatColor.translateAlternateColorCodes('&', PREFIX)
event.setLine(0, coloredLine)
}
}
}

View File

@ -1,18 +1,17 @@
package main.vp_server_integration
package main.VpcSpigotIntegration
// ----------------- VPC API --------------------
// Here is modules for check global balance (for further using in DYNAMIC course)
// ----------------------------------------------
import main.vp_server_integration.Vp_server_integration.Companion.LOGGER
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.LOGGER
class TotalBalanceModules {
companion object {
fun getEssentialsBalance(): Float {
LOGGER.info("Calculating dynamic rate using baltop")
// Force update balance top and delay to avoid empty string
CommandCapture.execute("baltop force")
val output = CommandCapture.execute("baltop")
val output = CommandCapture.execute("baltop force")
LOGGER.info("Balance top command output: $output")
var total = 0.0f

View File

@ -1,15 +1,13 @@
package main.vp_server_integration
package main.VpcSpigotIntegration
// ----------------- VPC API --------------------
// Here is utils help for working with some stuff
// ----------------------------------------------
import main.vp_server_integration.Vp_server_integration.Companion.PREFIX
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PREFIX
import org.bukkit.ChatColor
import org.bukkit.command.CommandSender
import java.util.logging.Logger
class MyLogger(debugEnabled: Boolean, private val loggerOriginal: java.util.logging.Logger) {
private val debug_enabled = debugEnabled
private val loggerSpigot = loggerOriginal
@ -22,7 +20,7 @@ class MyLogger(debugEnabled: Boolean, private val loggerOriginal: java.util.logg
fun error(message: String) {
if (debug_enabled) {
loggerSpigot.severe("[DBG] message")
loggerSpigot.severe("[DBG] $message")
}
}
}

View File

@ -1,4 +1,4 @@
package main.vp_server_integration
package main.VpcSpigotIntegration
// ----------------- VPC API --------------------
// Here is implemented calls to VPC UserAPI
@ -8,29 +8,25 @@ import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import com.google.gson.JsonSyntaxException
import main.vp_server_integration.Vp_server_integration.Companion.PREFIX
import main.vp_server_integration.Vp_server_integration.Companion.USERNAME
import main.vp_server_integration.Vp_server_integration.Companion.USER_API_URL
import main.vp_server_integration.Vp_server_integration.Companion.USER_TOKEN
import org.bukkit.ChatColor
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USERNAME
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USER_API_URL
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USER_TOKEN
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.URI
import java.net.URISyntaxException
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.math.max
class VpcApi {
companion object {
// Rate limiting: track timestamps of requests
private val requestTimestamps = ConcurrentLinkedQueue<Long>()
private const val MAX_REQUESTS_PER_MINUTE = 60
private const val MINUTE_IN_MILLIS = 60 * 1000L
// Rate limiting: lock and last execution time
private val rateLimitLock = Object()
private var lastExecutionTime = 0L
private const val MIN_INTERVAL_MILLIS = 1000L // 1 second between requests
// Using Gson for JSON serialization - much cleaner than manual string building
fun jsonify(args: List<String>): String {
@ -53,37 +49,27 @@ class VpcApi {
}
/**
* Enforces rate limiting by checking if we've exceeded the request limit.
* If so, sleeps until we're within the limit again.
* Enforces rate limiting by ensuring requests are executed one at a time
* with at least 1 second between them.
*/
private fun enforceRateLimit() {
synchronized(rateLimitLock) {
val now = System.currentTimeMillis()
val timeSinceLastExecution = now - lastExecutionTime
// Remove timestamps older than 1 minute
while (requestTimestamps.isNotEmpty() && now - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
requestTimestamps.poll()
}
// If we've hit the limit, calculate sleep time
if (requestTimestamps.size >= MAX_REQUESTS_PER_MINUTE) {
val oldestRequestTime = requestTimestamps.peek()
val sleepTime = max(0, MINUTE_IN_MILLIS - (now - oldestRequestTime)) + 100 // Add small buffer
// If less than 1 second has passed since last execution, wait
if (timeSinceLastExecution < MIN_INTERVAL_MILLIS) {
val sleepTime = MIN_INTERVAL_MILLIS - timeSinceLastExecution
try {
Thread.sleep(sleepTime)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
// After sleeping, clean up old timestamps again
val afterSleep = System.currentTimeMillis()
while (requestTimestamps.isNotEmpty() && afterSleep - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
requestTimestamps.poll()
}
}
// Add current timestamp
requestTimestamps.offer(now)
// Update last execution time to now
lastExecutionTime = System.currentTimeMillis()
}
}
fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse {

View File

@ -0,0 +1,888 @@
package main.VpcSpigotIntegration
// ---------------- MAIN FILE -------------------
// Here you will see the main logic
// ----------------------------------------------
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.Bukkit
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import net.md_5.bungee.api.chat.ComponentBuilder
import net.md_5.bungee.api.chat.HoverEvent
import net.md_5.bungee.api.chat.ClickEvent
import net.md_5.bungee.api.chat.TextComponent
import org.bukkit.ChatColor
import org.bukkit.inventory.ItemStack
import org.bukkit.scheduler.BukkitRunnable
import java.io.File
import java.io.FileInputStream
import java.util.Properties
import kotlin.collections.mutableMapOf
import kotlin.math.abs
class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
companion object {
// Configuration settings
var DEBUG_FLAG: Boolean = false
lateinit var USERNAME: String
lateinit var USER_TOKEN: String
lateinit var USER_API_URL: String
lateinit var COMMAND_ADD_COINS: String
lateinit var COMMAND_REMOVE_MODE: String
lateinit var COMMAND_REMOVE_COINS: String
lateinit var COMMAND_REMOVE_ERROR: String
var LOCAL_CURRENCY_MINIMUM: Double = DEFAULT_LOCAL_CURRENCY_MINIMUM
lateinit var COURSE_MODE: String
var COURSE_STATIC_VALUE: Double = DEFAULT_COURSE_STATIC_VALUE
lateinit var COURSE_DYNAMIC_COMMAND: String
var COURSE_COMMISSION: Float = DEFAULT_COURSE_COMMISSION
var INVOICE_TIMEOUT_SECONDS: Long = DEFAULT_INVOICE_TIMEOUT_SECONDS
// Features
var CURRENCY_CONVERT_FEATURE: Boolean = true
var PAY_BY_SIGN_FEATURE: Boolean = true
// Default configuration values
const val DEFAULT_USERNAME: String = "test"
const val DEFAULT_USER_TOKEN: String = "test"
const val DEFAULT_USER_API_URL: String = "http://127.0.0.1:8010/api/"
const val DEFAULT_COMMAND_ADD_COINS: String = "eco give %player% %amount%"
const val DEFAULT_COMMAND_REMOVE_MODE: String = "console"
const val DEFAULT_COMMAND_REMOVE_COINS: String = "eco take %player% %amount%"
const val DEFAULT_COMMAND_REMOVE_ERROR: String = "Error:"
const val DEFAULT_LOCAL_CURRENCY_MINIMUM: Double = 1.00
const val DEFAULT_COURSE_MODE: String = "dynamic"
const val DEFAULT_COURSE_STATIC_VALUE: Double = 1000.0
const val DEFAULT_COURSE_DYNAMIC_COMMAND: String = "baltop force"
const val DEFAULT_COURSE_COMMISSION: Float = 5.0f
const val DEFAULT_INVOICE_TIMEOUT_SECONDS: Long = 300 // 5 minutes
// Features
const val DEFAULT_PAY_BY_SIGN_FEATURE: Boolean = true
const val DEFAULT_CURRENCY_CONVERT_FEATURE: Boolean = true
// Static configurations
const val PREFIX = "&9[&bVPC&7-&6I&9] &3"
const val NUM_AFTER_DOT = 4
// Utility instances
lateinit var LOGGER: MyLogger
lateinit var SERVER: org.bukkit.Server
lateinit var PLUGIN: org.bukkit.plugin.Plugin
// For background checks
@JvmStatic
var TO_AUTH_PLAYERS: MutableMap<String, String> = mutableMapOf() // Pair: {player: vpc_username}
@JvmStatic
var TO_AUTH_PLAYERS_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic
var TO_PAY_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic
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
@JvmStatic
var INVOICE_CREATION_TIMES: MutableMap<String, Long> = mutableMapOf() // Pair: {invoice_id: creation_timestamp}
}
/**
* Load configuration from config.properties file
*/
private fun loadConfiguration() {
try {
// In Spigot, we use getDataFolder() instead of dataDirectory
val dataFolder = getDataFolder()
if (!dataFolder.exists()) {
dataFolder.mkdirs()
}
val configFile = File(dataFolder, "config.properties")
if (!configFile.exists()) {
createDefaultConfig(configFile)
}
// Load properties from file
val properties = Properties()
FileInputStream(configFile).use { properties.load(it) }
// Parse configuration values
DEBUG_FLAG = properties.getProperty("debug", false.toString()).replace("'", "").toBoolean()
USERNAME = properties.getProperty("username", DEFAULT_USERNAME).replace("'", "")
USER_TOKEN = properties.getProperty("user_token", DEFAULT_USER_TOKEN).replace("'", "")
USER_API_URL = properties.getProperty("user_api_url", DEFAULT_USER_API_URL).replace("'", "")
COMMAND_ADD_COINS = properties.getProperty("command_add_coins", DEFAULT_COMMAND_ADD_COINS).replace("'", "")
COMMAND_REMOVE_MODE = properties.getProperty("command_remove_mode", DEFAULT_COMMAND_REMOVE_MODE).replace("'", "")
COMMAND_REMOVE_COINS = properties.getProperty("command_remove_coins", DEFAULT_COMMAND_REMOVE_COINS).replace("'", "")
COMMAND_REMOVE_ERROR = properties.getProperty("command_remove_error", DEFAULT_COMMAND_REMOVE_ERROR).replace("'", "")
LOCAL_CURRENCY_MINIMUM = properties.getProperty("local_currency_minimum", DEFAULT_LOCAL_CURRENCY_MINIMUM.toString()).replace("'", "").toDouble()
COURSE_MODE = properties.getProperty("course_mode", DEFAULT_COURSE_MODE).replace("'", "")
COURSE_STATIC_VALUE = properties.getProperty("course_static_value", DEFAULT_COURSE_STATIC_VALUE.toString()).replace("'", "").toDouble()
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()
INVOICE_TIMEOUT_SECONDS = properties.getProperty("invoice_timeout_seconds", DEFAULT_INVOICE_TIMEOUT_SECONDS.toString()).replace("'", "").toLong()
// Features
CURRENCY_CONVERT_FEATURE = properties.getProperty("currency_convert", DEFAULT_CURRENCY_CONVERT_FEATURE.toString()).replace("'", "").toBoolean()
PAY_BY_SIGN_FEATURE = properties.getProperty("pay_by_sign", DEFAULT_PAY_BY_SIGN_FEATURE.toString()).replace("'", "").toBoolean()
logger.info("Configuration loaded successfully - Username: $USERNAME, API URL: $USER_API_URL")
} catch (e: Exception) {
logger.severe("Failed to load configuration, using defaults: ${e.message}")
USERNAME = DEFAULT_USERNAME
USER_TOKEN = DEFAULT_USER_TOKEN
USER_API_URL = DEFAULT_USER_API_URL
}
}
/**
* Create default configuration file with documentation
*/
private fun createDefaultConfig(configFile: File) {
configFile.writeText(
"""
# VPC Integration Configuration
# For get extra output
debug=false
# ---------- Part for work with UserAPI -----------
# Username from your VPC wallet
username=$DEFAULT_USERNAME
# Token for UserAPI
user_token=$DEFAULT_USER_TOKEN
# UserAPI URL
user_api_url='http://127.0.0.1:8010/api/'
# -------------------- END ------------------------
# ---------- Part for work with server ------------
# Which command run to add coins? (will run from console)
command_add_coins='$DEFAULT_COMMAND_ADD_COINS'
# From who we will run remove coins command? (player/console)
command_remove_mode=$DEFAULT_COMMAND_REMOVE_MODE
# Which command run to remove coins?
command_remove_coins='$DEFAULT_COMMAND_REMOVE_COINS'
# What shouldn't be in response after executing command
command_remove_error='$DEFAULT_COMMAND_REMOVE_ERROR'
# What is minimum Local Currency can be?
local_currency_minimum=1
# -------------------- END ------------------------
# --------- Part with configure course ------------
# Which mode use (dynamic/static, dynamic - set course based
course_mode=$DEFAULT_COURSE_MODE
# Course VPC to LC (Local Currency)
course_static_value=$DEFAULT_COURSE_STATIC_VALUE
# Which command will run for getting global balance
# If command not produce CLEAR Float/Int like 32.15 - do not set this
# For UNCLEAR global balance we need module
# Supported modules: baltop (Essentials)
course_dynamic_command='$DEFAULT_COURSE_DYNAMIC_COMMAND'
# For dynamic course recommended 5% and higher (avoid dupe), for static course you can set 0%
course_commission=$DEFAULT_COURSE_COMMISSION
# -------------------- END ------------------------
# --------- Invoice timeout configuration ----------
# After how many seconds unpaid invoices will be deleted (default: 300 seconds = 5 minutes)
invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
# -------------------- END ------------------------
# ------------------ Features ---------------------
# Should we enable currency convert (VPC -> LC and vice versa)
currency_convert=${DEFAULT_CURRENCY_CONVERT_FEATURE}
# Should we enable pay by signs from containers (chest/etc.)?
pay_by_sign=${DEFAULT_PAY_BY_SIGN_FEATURE}
# -------------------- END ------------------------
""".trimIndent()
)
}
override fun onEnable() {
// Get server instance
SERVER = Bukkit.getServer()
PLUGIN = this
// Load configuration
loadConfiguration()
// Initialize logger
LOGGER = MyLogger(DEBUG_FLAG, this.logger)
// Register command executor
getCommand("vpi")?.setExecutor(this)
// Register clicking on sign
server.pluginManager.registerEvents(SignHandler(), this)
// Background checks such a auth, invoice check, ...
startBackgroundChecks()
// Init data manager
DataManager.initialize(getDataFolder())
// Plugin startup logic
LOGGER.info("VP Server Integration plugin enabled!")
}
override fun onDisable() {
// Plugin shutdown logic
LOGGER.info("VP Server Integration plugin disabled!")
}
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (!command.name.equals("vpi", ignoreCase = true)) return false
if (sender !is Player) {
sender.sendMessage("&cТолько игроки могут выполнять данные команды.")
return true
}
// Run all logic asynchronously
Bukkit.getScheduler().runTaskAsynchronously(PLUGIN, Runnable {
when {
// Authentication
args.size == 2 && args[0] == "auth" -> {
handleAuthentication(sender, args)
}
args.isNotEmpty() && args[0] == "auth" -> {
Utils.send(sender, "/vpi auth <ник>")
}
// Currency conversion
args.isNotEmpty() && args[0] == "convert" -> {
if (CURRENCY_CONVERT_FEATURE) {
if (args.size in 3..4 && args[0] == "convert") {
handleCurrencyConversion(sender, args)
} else {
Utils.send(sender, "/vpi convert <vpc/lc> <сумма>")
}
} else {
Utils.send(sender, "&cДанная функциональность отключена.")
}
}
// Course info
args.isNotEmpty() && args[0] == "course" -> {
if (CURRENCY_CONVERT_FEATURE) {
val course = calculateExchangeRate()
LOGGER.info("Exchange rate calculated: $course")
if (course.isInfinite() || course == 0.0) {
LOGGER.error(if (course.isInfinite()) "Zero global balance?" else "Infinite global balance?")
Utils.send(
sender,
"&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже."
)
} else {
val course2VPC = Utils.round(course * (1 - COURSE_COMMISSION / 100), NUM_AFTER_DOT)
val course2LC = Utils.round(course * (1 + COURSE_COMMISSION / 100), NUM_AFTER_DOT)
Utils.send(sender, "VPC->LC = &6$course2VPC&3 | LC->VPC = &6$course2LC")
}
} else {
Utils.send(sender, "&cДанная функциональность отключена.")
}
}
// Cancel sign pay
args[0] == "signPay" && args[1] == "cancel" -> {
val invoiceId = TO_PAY_INVOICES[sender.name].toString()
VpcApi.delete_invoice(invoiceId)
synchronized(TO_PAY_SIGN_INVOICES) {
TO_PAY_SIGN_INVOICES.remove(sender.name)
INVOICE_CREATION_TIMES.remove(invoiceId)
SIGN_PAYMENT_INFO.remove(invoiceId)
}
Utils.send(sender, "Счёт успешно отменён.")
}
// Default help menu
else -> {
showHelpMenu(sender)
}
}
})
return true
}
/**
* Handle currency conversion logic
*/
private fun handleCurrencyConversion(sender: Player, args: Array<out String>) {
LOGGER.info("Starting currency conversion process - Step 1")
// Check if player is authenticated
val vpcUsername = DataManager.getPlayerVPCUsername(sender.name)
if (vpcUsername == null) {
Utils.send(sender, "&cВы должны авторизоваться сначала: /vpi auth")
return
}
LOGGER.info("Player is authenticated - Step 2")
val direction = args[1]
val amount = abs(args[2].toDouble())
// Validate conversion direction
if (!listOf("vpc", "lc").contains(direction)) {
Utils.send(sender, "&cНеверное направление. Используйте: vpc or lc")
return
}
LOGGER.info("Valid direction provided - Step 3")
// Calculate exchange rate
val course = calculateExchangeRate()
LOGGER.info("Exchange rate calculated: $course")
if (course.isInfinite()) {
LOGGER.error("Zero global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
return
} else if (course == 0.0) {
LOGGER.error("Infinite global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
return
}
when (direction) {
"vpc" -> handleVpcConversion(sender, Utils.round(amount, NUM_AFTER_DOT), course, args)
"lc" -> handleLcConversion(sender, Utils.round(amount, NUM_AFTER_DOT), course, args)
}
}
/**
* Calculate exchange rate based on configuration
*/
private fun calculateExchangeRate(): Double {
return if (COURSE_MODE == "static") {
COURSE_STATIC_VALUE
} else {
val dynamicRate: Any = calculateDynamicRate()
if (dynamicRate != 0.0) Utils.round(dynamicRate, NUM_AFTER_DOT)
else return 0.0
}
}
/**
* Calculate dynamic exchange rate
*/
private fun calculateDynamicRate(): Double {
return if (COURSE_DYNAMIC_COMMAND == "baltop force") {
val globalBalance = TotalBalanceModules.getEssentialsBalance()
LOGGER.info("Global balance: $globalBalance")
val vpcUser = VpcApi.user_in_db(username = USERNAME)
if (vpcUser == null) {
return 0.0
// throw Exception("VPC user not found")
}
val vpcBalance = vpcUser["balance"].toString().toDouble()
LOGGER.info("VPC balance: $vpcBalance")
val rate = globalBalance / vpcBalance
LOGGER.info("Calculated exchange rate: $rate")
rate
} else {
LOGGER.info("Calculating dynamic rate using custom command - Step 4.2")
val globalBalance = CommandCapture.execute(COURSE_DYNAMIC_COMMAND).toString().toDouble()
val vpcUser = VpcApi.user_in_db(username = USERNAME)
if (vpcUser == null) {
return 0.0
// throw Exception("VPC user not found")
}
val vpcBalance = vpcUser["balance"].toString().toDouble()
globalBalance / vpcBalance
}
}
/**
* Handle VPC to local currency conversion
*/
private fun handleVpcConversion(sender: Player, amount: Double, course: Double, args: Array<out String>) {
val amountVPC = Utils.round(amount / course * (1 - COURSE_COMMISSION / 100), NUM_AFTER_DOT)
val vpcUsername = DataManager.getPlayerVPCUsername(sender.name)
if (vpcUsername == null) {
Utils.send(sender, "&cВы должны авторизоваться сначала: /vpi auth")
return
}
if (amountVPC < 0.0001 || amount < LOCAL_CURRENCY_MINIMUM) {
Utils.send(sender, "&cСлишком маленькая входная или выходная сумма.")
return
}
if (args.size == 4) {
// Execute confirmed conversion
LOGGER.info("Executing confirmed conversion - Step 5")
val result = CommandCapture.execute(
COMMAND_REMOVE_COINS
.replace("%player%", sender.name)
.replace("%amount%", amount.toString())
)[0]
LOGGER.info("Command executed - Step 6")
if (result.contains(COMMAND_REMOVE_ERROR)) {
Utils.send(sender, "&cОшибка. Возможно недостаточно средств.")
return
}
LOGGER.info("Transferring coins to VPC - Step 8")
VpcApi.transfer_coins(vpcUsername, amountVPC)
} else {
// Show confirmation prompt
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите здесь, чтобы конвертировать &6$amount&3 в &6${String.format("%.4f", amountVPC)} VPC")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpi convert vpc $amount confirm")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpi convert vpc $amount confirm").create())
sender.spigot().sendMessage(message)
}
}
/**
* Handle local currency to VPC conversion
*/
private fun handleLcConversion(sender: Player, amount: Double, course: Double, args: Array<out String>) {
// Check if there's an existing pending invoice
if (sender.name in TO_PAY_INVOICES) {
handleExistingInvoice(sender)
return
}
LOGGER.info("Amount: $amount, Course: $course")
val amountLC = Utils.round(amount * course * (1 + COURSE_COMMISSION / 100), NUM_AFTER_DOT)
LOGGER.info("Converted amount: $amountLC")
if (amount < 0.0001 || amountLC < LOCAL_CURRENCY_MINIMUM) {
Utils.send(sender, "&cСлишком маленькая входная или выходная сумма.")
return
}
// Create new invoice
val invoiceId = VpcApi.create_invoice(amount).toString()
LOGGER.info("Invoice id: $invoiceId")
if (invoiceId == "null") {
Utils.send(sender,"&cПроизошла ошибка при генерации счёта. Обратитесь к администратору или повторите позже.")
return
}
TO_PAY_INVOICES[sender.name] = invoiceId
INVOICES_AMOUNT[invoiceId] = amountLC
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
sendPaymentPrompt(sender, amount, amountLC, invoiceId)
}
/**
* Handle existing payment invoice
*/
private fun handleExistingInvoice(sender: Player) {
val invoiceId = TO_PAY_INVOICES[sender.name]
val amount = INVOICES_AMOUNT[invoiceId]
if (amount == null) return
val course = if (COURSE_MODE == "static") COURSE_STATIC_VALUE else calculateDynamicRate()
val amountLC = amount * course * (1 + COURSE_COMMISSION / 100)
sendPaymentPrompt(sender, amount, amountLC, invoiceId)
}
/**
* Send payment prompt to player
*/
private fun sendPaymentPrompt(sender: Player, amount: Double, amountLC: Double, invoiceId: String?) {
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите здесь, чтобы конвертировать &6$amount VPC &3в &6${String.format("%.4f", amountLC)}")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpc pay $USERNAME $amount $invoiceId")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpc pay $USERNAME $amount $invoiceId").create())
sender.spigot().sendMessage(message)
}
/**
* Handle player authentication process
*/
private fun handleAuthentication(sender: Player, args: Array<out String>) {
val vpcUsername = DataManager.getPlayerVPCUsername(sender.name)
if (vpcUsername != null) {
Utils.send(sender, "&cВы уже авторизованы!")
return
}
// Check if authentication is already in progress
if (sender.name in TO_AUTH_PLAYERS) {
val invoiceId = TO_AUTH_PLAYERS_INVOICES[sender.name]
sendAuthPrompt(sender, invoiceId ?: "")
return
}
// Start new authentication process
val invoiceId = VpcApi.create_invoice(0.001).toString()
val vpcUsernameInput = args[1]
LOGGER.info("Invoice id: $invoiceId")
if (invoiceId == "null") {
Utils.send(sender,"&cПроизошла ошибка при генерации счёта. Обратитесь к администратору или повторите позже.")
return
}
LOGGER.info("Adding player to authentication lists")
synchronized(TO_AUTH_PLAYERS) {
TO_AUTH_PLAYERS[sender.name] = vpcUsernameInput
TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
}
LOGGER.info("Authentication lists updated")
sendAuthPrompt(sender, invoiceId)
}
/**
* Send authentication prompt to player
*/
private fun sendAuthPrompt(sender: Player, invoiceId: String) {
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите, чтобы перевести &60.001 VPC&3 для авторизации")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpc pay $USERNAME 0.001 $invoiceId")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpc pay $USERNAME 0.001 $invoiceId").create())
sender.spigot().sendMessage(message)
}
/**
* Display help menu to player
*/
private fun showHelpMenu(sender: CommandSender) {
Utils.send(sender, """Использование команд:
/vpi auth <ник> - Авторизация
/vpi course - Текущий курс
/vpi convert <куда: vpc/lc> <сумма> - Обмен VPC на локальную валюту или наоборот
Почему 'VPC-I'? Потому что это интеграция на конечном сервере - 'VPC Integration'
Кошелёк VPC:
ТГ: https://t.me/vp_coin_bot
ДС: Кошелёк VPC#3531""".trimIndent())
}
override fun onTabComplete(sender: CommandSender, command: Command, alias: String, args: Array<out String>): List<String> {
val completions = mutableListOf<String>()
if (command.name.equals("vpi", ignoreCase = true)) {
when (args.size) {
1 -> {
completions.addAll(listOf("help", "convert", "auth", "course"))
}
2 -> {
if (args[0].equals("convert", ignoreCase = true)) {
completions.add("<куда: vpc/lc>")
}
}
3 -> {
if (args[0].equals("convert", ignoreCase = true)) {
completions.add("<сумма>")
}
}
}
}
return completions.filter { it.startsWith(args.lastOrNull()?.lowercase() ?: "") }.sorted()
}
/**
* Start background checks for invoice processing
*/
private fun startBackgroundChecks() {
object : BukkitRunnable() {
override fun run() {
processAuthenticationInvoices()
processPaymentInvoices()
processSignInvoices()
cleanupExpiredInvoices()
}
}.runTaskTimerAsynchronously(this, 0L, 20L)
}
/**
* Process authentication invoices in background
*/
private fun processAuthenticationInvoices() {
synchronized(TO_AUTH_PLAYERS_INVOICES) {
if (TO_AUTH_PLAYERS_INVOICES.isNotEmpty()) {
val iterator = TO_AUTH_PLAYERS_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("Auth invoice ${entry.value} status: $status")
if (status != null && status.toString() == "true") {
LOGGER.info("Processing successful authentication")
VpcApi.delete_invoice(entry.value)
DataManager.setPlayerVPCUsername(entry.key, TO_AUTH_PLAYERS[entry.key].toString())
Utils.send(Bukkit.getPlayer(entry.key.toString()), "&aУспешная авторизация!")
// Clean up tracking maps
TO_AUTH_PLAYERS.remove(entry.key)
INVOICE_CREATION_TIMES.remove(entry.value)
iterator.remove()
}
} else {
logger.warning("Received null result for auth invoice ${entry.value}")
}
} catch (e: Exception) {
LOGGER.error("Error processing auth invoice ${entry.value}: ${e.message}")
e.printStackTrace()
}
}
}
}
}
/**
* Process payment invoices in background
*/
private fun processPaymentInvoices() {
synchronized(TO_PAY_INVOICES) {
if (TO_PAY_INVOICES.isNotEmpty()) {
val iterator = TO_PAY_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)
val amountLC = INVOICES_AMOUNT[entry.value]
// Execute coin addition on main thread
val commandToAddCoins = COMMAND_ADD_COINS
.replace("%player%", entry.key)
.replace("%amount%", amountLC.toString())
Bukkit.getScheduler().runTask(this@VpcServerIntegration, Runnable {
try {
CommandCapture.execute(commandToAddCoins)
} catch (e: Exception) {
LOGGER.error("Error executing coin addition command: ${e.message}")
e.printStackTrace()
}
})
// Clean up tracking maps
INVOICES_AMOUNT.remove(entry.key)
INVOICE_CREATION_TIMES.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()
}
}
}
}
}
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...")
Utils.send(paymentInfo.player, "&cВ контейнере недостаточно предметов, попытка вернуть 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")
Utils.send(paymentInfo.player, "&cНевозможно вернуть VPC, обратитесь к администратору: $transferResult")
}
} else {
// If we have needed items
val transferResult = VpcApi.transfer_coins(paymentInfo.dst_username, paymentInfo.cost).toString()
if (transferResult != "OK") {
LOGGER.error("Can't transfer VPC, result: $transferResult")
Utils.send(paymentInfo.player, "&cПеревод адресату невозможен, отмена покупки.")
// 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")
Utils.send(paymentInfo.player, "&cНевозможно вернуть VPC, обратитесь к администратору: $transferResult")
}
} else {
paymentInfo.player.inventory.addItem(itemStack)
paymentInfo.container.inventory.removeItem(itemStack)
}
}
// 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
*/
private fun cleanupExpiredInvoices() {
val currentTime = System.currentTimeMillis()
val timeoutMillis = INVOICE_TIMEOUT_SECONDS * 1000
// Cleanup auth invoices
synchronized(TO_AUTH_PLAYERS_INVOICES) {
val authIterator = TO_AUTH_PLAYERS_INVOICES.iterator()
while (authIterator.hasNext()) {
val entry = authIterator.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 auth invoice: $invoiceId")
try {
VpcApi.delete_invoice(invoiceId)
} catch (e: Exception) {
LOGGER.error("Error deleting expired auth invoice $invoiceId: ${e.message}")
}
// Clean up tracking maps
TO_AUTH_PLAYERS.remove(entry.key)
INVOICE_CREATION_TIMES.remove(invoiceId)
authIterator.remove()
// Notify player if online
val player = Bukkit.getPlayer(entry.key)
if (player != null && player.isOnline) {
Utils.send(player, "&cВаша авторизационная оплата истекла. Пожалуйста, попробуйте снова: /vpi auth <ник>")
}
}
}
}
// Cleanup payment invoices
synchronized(TO_PAY_INVOICES) {
val paymentIterator = TO_PAY_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
INVOICES_AMOUNT.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Ваша оплата истекла. Пожалуйста, создайте новую: /vpi convert lc <сумма>")
}
}
}
}
// 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Ваша оплата истекла. Нажмите ещё раз на табличку.")
}
}
}
}
}
}

View File

@ -1,724 +0,0 @@
package main.vp_server_integration
// ---------------- MAIN FILE -------------------
// Here you will see the main logic
// ----------------------------------------------
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.Bukkit
import java.util.logging.Logger
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import net.md_5.bungee.api.chat.ComponentBuilder
import net.md_5.bungee.api.chat.HoverEvent
import net.md_5.bungee.api.chat.ClickEvent
import net.md_5.bungee.api.chat.TextComponent
import org.bukkit.ChatColor
import org.bukkit.plugin.Plugin
import org.bukkit.scheduler.BukkitRunnable
import java.io.File
import java.io.FileInputStream
import java.util.Properties
import kotlin.collections.mutableMapOf
import kotlin.math.abs
import java.util.Date
import kotlin.math.round
class Vp_server_integration() : JavaPlugin(), CommandExecutor {
companion object {
// Configuration settings
var DEBUG_FLAG: Boolean = false
lateinit var USERNAME: String
lateinit var USER_TOKEN: String
lateinit var USER_API_URL: String
lateinit var COMMAND_ADD_COINS: String
lateinit var COMMAND_REMOVE_MODE: String
lateinit var COMMAND_REMOVE_COINS: String
lateinit var COMMAND_REMOVE_ERROR: String
var LOCAL_CURRENCY_MINIMUM: Double = DEFAULT_LOCAL_CURRENCY_MINIMUM
lateinit var COURSE_MODE: String
var COURSE_STATIC_VALUE: Double = DEFAULT_COURSE_STATIC_VALUE
lateinit var COURSE_DYNAMIC_COMMAND: String
var COURSE_COMMISSION: Float = DEFAULT_COURSE_COMMISSION
var INVOICE_TIMEOUT_SECONDS: Long = DEFAULT_INVOICE_TIMEOUT_SECONDS
// Default configuration values
const val DEFAULT_USERNAME: String = "test"
const val DEFAULT_USER_TOKEN: String = "test"
const val DEFAULT_USER_API_URL: String = "http://127.0.0.1:8010/api/"
const val DEFAULT_COMMAND_ADD_COINS: String = "eco give %player% %amount%"
const val DEFAULT_COMMAND_REMOVE_MODE: String = "console"
const val DEFAULT_COMMAND_REMOVE_COINS: String = "eco take %player% %amount%"
const val DEFAULT_COMMAND_REMOVE_ERROR: String = "Error:"
const val DEFAULT_LOCAL_CURRENCY_MINIMUM: Double = 1.00
const val DEFAULT_COURSE_MODE: String = "dynamic"
const val DEFAULT_COURSE_STATIC_VALUE: Double = 1000.0
const val DEFAULT_COURSE_DYNAMIC_COMMAND: String = "baltop force"
const val DEFAULT_COURSE_COMMISSION: Float = 5.0f
const val DEFAULT_INVOICE_TIMEOUT_SECONDS: Long = 300 // 5 minutes
// Static configurations
const val PREFIX = "&9[&bVPC&7-&6I&9] &3"
const val NUM_AFTER_DOT = 4
// Utility instances
lateinit var LOGGER: MyLogger
lateinit var SERVER: org.bukkit.Server
// var PLUGIN: org.bukkit.plugin.Plugin = Bukkit.getPluginManager().getPlugin("vp_server_integration")
lateinit var PLUGIN: org.bukkit.plugin.Plugin
// For background checks
@JvmStatic
var TO_AUTH_PLAYERS: MutableMap<String, String> = mutableMapOf() // Pair: {player: vpc_username}
@JvmStatic
var TO_AUTH_PLAYERS_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic
var TO_PAY_INVOICES: MutableMap<String, String> = mutableMapOf() // Pair: {player: invoice_id}
@JvmStatic
var INVOICES_AMOUNT: MutableMap<String, Double> = mutableMapOf() // How many should we pay to player after invoice?
// Track invoice creation times for timeout handling
@JvmStatic
var INVOICE_CREATION_TIMES: MutableMap<String, Long> = mutableMapOf() // Pair: {invoice_id: creation_timestamp}
}
/**
* Load configuration from config.properties file
*/
private fun loadConfiguration() {
try {
// In Spigot, we use getDataFolder() instead of dataDirectory
val dataFolder = getDataFolder()
if (!dataFolder.exists()) {
dataFolder.mkdirs()
}
val configFile = File(dataFolder, "config.properties")
if (!configFile.exists()) {
createDefaultConfig(configFile)
}
// Load properties from file
val properties = Properties()
FileInputStream(configFile).use { properties.load(it) }
// Parse configuration values
DEBUG_FLAG = properties.getProperty("debug", false.toString()).replace("'", "").toBoolean()
USERNAME = properties.getProperty("username", DEFAULT_USERNAME).replace("'", "")
USER_TOKEN = properties.getProperty("user_token", DEFAULT_USER_TOKEN).replace("'", "")
USER_API_URL = properties.getProperty("user_api_url", DEFAULT_USER_API_URL).replace("'", "")
COMMAND_ADD_COINS = properties.getProperty("command_add_coins", DEFAULT_COMMAND_ADD_COINS).replace("'", "")
COMMAND_REMOVE_MODE = properties.getProperty("command_remove_mode", DEFAULT_COMMAND_REMOVE_MODE).replace("'", "")
COMMAND_REMOVE_COINS = properties.getProperty("command_remove_coins", DEFAULT_COMMAND_REMOVE_COINS).replace("'", "")
COMMAND_REMOVE_ERROR = properties.getProperty("command_remove_error", DEFAULT_COMMAND_REMOVE_ERROR).replace("'", "")
LOCAL_CURRENCY_MINIMUM = properties.getProperty("local_currency_minimum", DEFAULT_LOCAL_CURRENCY_MINIMUM.toString()).replace("'", "").toDouble()
COURSE_MODE = properties.getProperty("course_mode", DEFAULT_COURSE_MODE).replace("'", "")
COURSE_STATIC_VALUE = properties.getProperty("course_static_value", DEFAULT_COURSE_STATIC_VALUE.toString()).replace("'", "").toDouble()
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()
INVOICE_TIMEOUT_SECONDS = properties.getProperty("invoice_timeout_seconds", DEFAULT_INVOICE_TIMEOUT_SECONDS.toString()).replace("'", "").toLong()
logger.info("Configuration loaded successfully - Username: $USERNAME, API URL: $USER_API_URL")
} catch (e: Exception) {
logger.severe("Failed to load configuration, using defaults: ${e.message}")
USERNAME = DEFAULT_USERNAME
USER_TOKEN = DEFAULT_USER_TOKEN
USER_API_URL = DEFAULT_USER_API_URL
}
}
/**
* Create default configuration file with documentation
*/
private fun createDefaultConfig(configFile: File) {
configFile.writeText(
"""
# VPC Integration Configuration
# For get extra output
debug=false
# ---------- Part for work with UserAPI -----------
# Username from your VPC wallet
username=$DEFAULT_USERNAME
# Token for UserAPI
user_token=$DEFAULT_USER_TOKEN
# UserAPI URL
user_api_url='http://127.0.0.1:8010/api/'
# -------------------- END ------------------------
# ---------- Part for work with server ------------
# Which command run to add coins? (will run from console)
command_add_coins='$DEFAULT_COMMAND_ADD_COINS'
# From who we will run remove coins command? (player/console)
command_remove_mode=$DEFAULT_COMMAND_REMOVE_MODE
# Which command run to remove coins?
command_remove_coins='$DEFAULT_COMMAND_REMOVE_COINS'
# What shouldn't be in response after executing command
command_remove_error='$DEFAULT_COMMAND_REMOVE_ERROR'
# What is minimum Local Currency can be?
local_currency_minimum=1
# -------------------- END ------------------------
# --------- Part with configure course ------------
# Which mode use (dynamic/static, dynamic - set course based
course_mode=$DEFAULT_COURSE_MODE
# Course VPC to LC (Local Currency)
course_static_value=$DEFAULT_COURSE_STATIC_VALUE
# Which command will run for getting global balance
# If command not produce CLEAR Float/Int like 32.15 - do not set this
# For UNCLEAR global balance we need module
# Supported modules: baltop (Essentials)
course_dynamic_command='$DEFAULT_COURSE_DYNAMIC_COMMAND'
# For dynamic course recommended 5% and higher (avoid dupe), for static course you can set 0%
course_commission=$DEFAULT_COURSE_COMMISSION
# -------------------- END ------------------------
# --------- Invoice timeout configuration ----------
# After how many seconds unpaid invoices will be deleted (default: 300 seconds = 5 minutes)
invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
# -------------------- END ------------------------
""".trimIndent()
)
}
override fun onEnable() {
// Get server instance
SERVER = Bukkit.getServer()
PLUGIN = this
// Load configuration
loadConfiguration()
// Initialize logger
LOGGER = MyLogger(DEBUG_FLAG, this.logger)
// Register command executor
getCommand("vpi")?.setExecutor(this)
// Background checks such a auth, invoice check, ...
startBackgroundChecks()
// Init data manager
DataManager.initialize(getDataFolder())
// Plugin startup logic
LOGGER.info("VP Server Integration plugin enabled!")
}
override fun onDisable() {
// Plugin shutdown logic
LOGGER.info("VP Server Integration plugin disabled!")
}
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (command.name.equals("vpi", ignoreCase = true)) {
if (sender !is Player) {
sender.sendMessage("&cТолько игроки могут выполнять данные команды.")
return true
}
when {
// Handle currency conversion command
(args.size == 3 || args.size == 4) && args[0] == "convert" -> {
handleCurrencyConversion(sender, args)
}
args.isNotEmpty() && args[0] == "convert" -> {
Utils.send(sender, "/vpi convert <vpc/lc> <сумма>")
}
// Handle course command
args.isNotEmpty() && args[0] == "course" -> {
// Calculate exchange rate
val course = calculateExchangeRate()
LOGGER.info("Exchange rate calculated: $course")
if (course.isInfinite()) {
LOGGER.error("Zero global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
} else if (course == 0.0) {
LOGGER.error("Infinite global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
} else {
val course2VPC = Utils.round(course * (1 - COURSE_COMMISSION / 100), NUM_AFTER_DOT)
val course2LC = Utils.round(course * (1 + COURSE_COMMISSION / 100), NUM_AFTER_DOT)
Utils.send(sender, "VPC->LC = &6$course2VPC&3 | LC->VPC = &6$course2LC")
}
}
// Handle authentication command
args.size == 2 && args[0] == "auth" -> {
handleAuthentication(sender, args)
}
args.isNotEmpty() && args[0] == "auth" -> {
Utils.send(sender, "/vpi auth <ник>")
}
// Show general help
else -> {
showHelpMenu(sender)
}
}
return true
}
return false
}
/**
* Handle currency conversion logic
*/
private fun handleCurrencyConversion(sender: Player, args: Array<out String>) {
LOGGER.info("Starting currency conversion process - Step 1")
// Check if player is authenticated
val vpcUsername = DataManager.getPlayerVPCUsername(sender.name)
if (vpcUsername == null) {
Utils.send(sender, "&cВы должны авторизоваться сначала: /vpi auth")
return
}
LOGGER.info("Player is authenticated - Step 2")
val direction = args[1]
val amount = abs(args[2].toDouble())
// Validate conversion direction
if (!listOf("vpc", "lc").contains(direction)) {
Utils.send(sender, "&cНеверное направление. Используйте: vpc or lc")
return
}
LOGGER.info("Valid direction provided - Step 3")
// Calculate exchange rate
val course = calculateExchangeRate()
LOGGER.info("Exchange rate calculated: $course")
if (course.isInfinite()) {
LOGGER.error("Zero global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
return
} else if (course == 0.0) {
LOGGER.error("Infinite global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
return
}
when (direction) {
"vpc" -> handleVpcConversion(sender, Utils.round(amount, NUM_AFTER_DOT), course, args)
"lc" -> handleLcConversion(sender, Utils.round(amount, NUM_AFTER_DOT), course, args)
}
}
/**
* Calculate exchange rate based on configuration
*/
private fun calculateExchangeRate(): Double {
return if (COURSE_MODE == "static") {
COURSE_STATIC_VALUE
} else {
Utils.round(calculateDynamicRate(), NUM_AFTER_DOT)
}
}
/**
* Calculate dynamic exchange rate
*/
private fun calculateDynamicRate(): Double {
return if (COURSE_DYNAMIC_COMMAND == "baltop force") {
val globalBalance = TotalBalanceModules.getEssentialsBalance()
LOGGER.info("Global balance: $globalBalance")
val vpcUser = VpcApi.user_in_db(username = USERNAME)
if (vpcUser == null) {
throw Exception("VPC user not found")
}
val vpcBalance = vpcUser["balance"].toString().toDouble()
LOGGER.info("VPC balance: $vpcBalance")
val rate = globalBalance / vpcBalance
LOGGER.info("Calculated exchange rate: $rate")
rate
} else {
LOGGER.info("Calculating dynamic rate using custom command - Step 4.2")
val globalBalance = CommandCapture.execute(COURSE_DYNAMIC_COMMAND).toString().toDouble()
val vpcUser = VpcApi.user_in_db(username = USERNAME)
if (vpcUser == null) {
throw Exception("VPC user not found")
}
val vpcBalance = vpcUser["balance"].toString().toDouble()
globalBalance / vpcBalance
}
}
/**
* Handle VPC to local currency conversion
*/
private fun handleVpcConversion(sender: Player, amount: Double, course: Double, args: Array<out String>) {
val amountVPC = Utils.round(amount / course * (1 - COURSE_COMMISSION / 100), NUM_AFTER_DOT)
val vpcUsername = DataManager.getPlayerVPCUsername(sender.name)
if (vpcUsername == null) {
Utils.send(sender, "&cВы должны авторизоваться сначала: /vpi auth")
return
}
if (amountVPC < 0.0001 || amount < LOCAL_CURRENCY_MINIMUM) {
Utils.send(sender, "&cСлишком маленькая входная или выходная сумма.")
return
}
if (args.size == 4) {
// Execute confirmed conversion
LOGGER.info("Executing confirmed conversion - Step 5")
val result = CommandCapture.execute(
COMMAND_REMOVE_COINS
.replace("%player%", sender.name)
.replace("%amount%", amount.toString())
)[0]
LOGGER.info("Command executed - Step 6")
if (result.contains(COMMAND_REMOVE_ERROR)) {
Utils.send(sender, "&cОшибка. Возможно недостаточно средств.")
return
}
LOGGER.info("Transferring coins to VPC - Step 8")
VpcApi.transfer_coins(vpcUsername, amountVPC)
} else {
// Show confirmation prompt
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите здесь, чтобы конвертировать &6$amount&3 в &6${String.format("%.4f", amountVPC)} VPC")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpi convert vpc $amount confirm")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpi convert vpc $amount confirm").create())
sender.spigot().sendMessage(message)
}
}
/**
* Handle local currency to VPC conversion
*/
private fun handleLcConversion(sender: Player, amount: Double, course: Double, args: Array<out String>) {
// Check if there's an existing pending invoice
if (sender.name in TO_PAY_INVOICES) {
handleExistingInvoice(sender)
return
}
LOGGER.info("Amount: $amount, Course: $course")
val amountLC = Utils.round(amount * course * (1 + COURSE_COMMISSION / 100), NUM_AFTER_DOT)
LOGGER.info("Converted amount: $amountLC")
if (amount < 0.0001 || amountLC < LOCAL_CURRENCY_MINIMUM) {
Utils.send(sender, "&cСлишком маленькая входная или выходная сумма.")
return
}
// Create new invoice
val invoiceId = VpcApi.create_invoice(amount).toString()
TO_PAY_INVOICES[sender.name] = invoiceId
INVOICES_AMOUNT[invoiceId] = amountLC
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
sendPaymentPrompt(sender, amount, amountLC, invoiceId)
}
/**
* Handle existing payment invoice
*/
private fun handleExistingInvoice(sender: Player) {
val invoiceId = TO_PAY_INVOICES[sender.name]
val amount = INVOICES_AMOUNT[invoiceId]
if (amount == null) return
val course = if (COURSE_MODE == "static") COURSE_STATIC_VALUE else calculateDynamicRate()
val amountLC = amount * course * (1 + COURSE_COMMISSION / 100)
sendPaymentPrompt(sender, amount, amountLC, invoiceId)
}
/**
* Send payment prompt to player
*/
private fun sendPaymentPrompt(sender: Player, amount: Double, amountLC: Double, invoiceId: String?) {
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите здесь, чтобы конвертировать &6$amount VPC &3в &6${String.format("%.4f", amountLC)}")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpc pay $USERNAME $amount $invoiceId")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpc pay $USERNAME $amount $invoiceId").create())
sender.spigot().sendMessage(message)
}
/**
* Handle player authentication process
*/
private fun handleAuthentication(sender: Player, args: Array<out String>) {
val vpcUsername = DataManager.getPlayerVPCUsername(sender.name)
if (vpcUsername != null) {
Utils.send(sender, "&cВы уже авторизованы!")
return
}
// Check if authentication is already in progress
if (sender.name in TO_AUTH_PLAYERS) {
val invoiceId = TO_AUTH_PLAYERS_INVOICES[sender.name]
sendAuthPrompt(sender, invoiceId ?: "")
return
}
// Start new authentication process
val invoiceId = VpcApi.create_invoice(0.001)
val vpcUsernameInput = args[1]
LOGGER.info("Adding player to authentication lists")
synchronized(TO_AUTH_PLAYERS) {
TO_AUTH_PLAYERS[sender.name] = vpcUsernameInput
TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId.toString()
INVOICE_CREATION_TIMES[invoiceId.toString()] = System.currentTimeMillis()
}
LOGGER.info("Authentication lists updated")
sendAuthPrompt(sender, invoiceId.toString())
}
/**
* Send authentication prompt to player
*/
private fun sendAuthPrompt(sender: Player, invoiceId: String) {
val colored = ChatColor.translateAlternateColorCodes('&',
"${PREFIX}Нажмите, чтобы перевести &60.001 VPC&3 для авторизации")
val message = TextComponent(colored)
message.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/vpc pay $USERNAME 0.001 $invoiceId")
message.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT,
ComponentBuilder("/vpc pay $USERNAME 0.001 $invoiceId").create())
sender.spigot().sendMessage(message)
}
/**
* Display help menu to player
*/
private fun showHelpMenu(sender: CommandSender) {
Utils.send(sender, """Использование команд:
/vpi auth <ник> - Авторизация
/vpi course - Текущий курс
/vpi convert <куда: vpc/lc> <сумма> - Обмен VPC на локальную валюту или наоборот
Почему 'VPC-I'? Потому что это интеграция на конечном сервере - 'VPC Integration'
Кошелёк VPC:
ТГ: https://t.me/vp_coin_bot
ДС: Кошелёк VPC#3531""".trimIndent())
}
override fun onTabComplete(sender: CommandSender, command: Command, alias: String, args: Array<out String>): List<String> {
val completions = mutableListOf<String>()
if (command.name.equals("vpi", ignoreCase = true)) {
when (args.size) {
1 -> {
completions.addAll(listOf("help", "convert", "auth", "course"))
}
2 -> {
if (args[0].equals("convert", ignoreCase = true)) {
completions.add("<куда: vpc/lc>")
}
}
3 -> {
if (args[0].equals("convert", ignoreCase = true)) {
completions.add("<сумма>")
}
}
}
}
return completions.filter { it.startsWith(args.lastOrNull()?.lowercase() ?: "") }.sorted()
}
/**
* Start background checks for invoice processing
*/
private fun startBackgroundChecks() {
object : BukkitRunnable() {
override fun run() {
processAuthenticationInvoices()
processPaymentInvoices()
cleanupExpiredInvoices()
}
}.runTaskTimerAsynchronously(this, 0L, 20L)
}
/**
* Process authentication invoices in background
*/
private fun processAuthenticationInvoices() {
synchronized(TO_AUTH_PLAYERS_INVOICES) {
if (TO_AUTH_PLAYERS_INVOICES.isNotEmpty()) {
val iterator = TO_AUTH_PLAYERS_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("Auth invoice ${entry.value} status: $status")
if (status != null && status.toString() == "true") {
LOGGER.info("Processing successful authentication")
VpcApi.delete_invoice(entry.value)
DataManager.setPlayerVPCUsername(entry.key, TO_AUTH_PLAYERS[entry.key].toString())
Utils.send(Bukkit.getPlayer(entry.key.toString()), "&aУспешная авторизация!")
// Clean up tracking maps
TO_AUTH_PLAYERS.remove(entry.key)
INVOICE_CREATION_TIMES.remove(entry.value)
iterator.remove()
}
} else {
logger.warning("Received null result for auth invoice ${entry.value}")
}
} catch (e: Exception) {
LOGGER.error("Error processing auth invoice ${entry.value}: ${e.message}")
e.printStackTrace()
}
}
}
}
}
/**
* Process payment invoices in background
*/
private fun processPaymentInvoices() {
synchronized(TO_PAY_INVOICES) {
if (TO_PAY_INVOICES.isNotEmpty()) {
val iterator = TO_PAY_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)
val amountLC = INVOICES_AMOUNT[entry.value]
// Execute coin addition on main thread
val commandToAddCoins = COMMAND_ADD_COINS
.replace("%player%", entry.key)
.replace("%amount%", amountLC.toString())
Bukkit.getScheduler().runTask(this@Vp_server_integration, Runnable {
try {
CommandCapture.execute(commandToAddCoins)
} catch (e: Exception) {
LOGGER.error("Error executing coin addition command: ${e.message}")
e.printStackTrace()
}
})
// Clean up tracking maps
INVOICES_AMOUNT.remove(entry.key)
INVOICE_CREATION_TIMES.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
*/
private fun cleanupExpiredInvoices() {
val currentTime = System.currentTimeMillis()
val timeoutMillis = INVOICE_TIMEOUT_SECONDS * 1000
// Cleanup auth invoices
synchronized(TO_AUTH_PLAYERS_INVOICES) {
val authIterator = TO_AUTH_PLAYERS_INVOICES.iterator()
while (authIterator.hasNext()) {
val entry = authIterator.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 auth invoice: $invoiceId")
try {
VpcApi.delete_invoice(invoiceId)
} catch (e: Exception) {
LOGGER.error("Error deleting expired auth invoice $invoiceId: ${e.message}")
}
// Clean up tracking maps
TO_AUTH_PLAYERS.remove(entry.key)
INVOICE_CREATION_TIMES.remove(invoiceId)
authIterator.remove()
// Notify player if online
val player = Bukkit.getPlayer(entry.key)
if (player != null && player.isOnline) {
Utils.send(player, "&cВаша авторизационная оплата истекла. Пожалуйста, попробуйте снова: /vpi auth <ник>")
}
}
}
}
// Cleanup payment invoices
synchronized(TO_PAY_INVOICES) {
val paymentIterator = TO_PAY_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
INVOICES_AMOUNT.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Ваша оплата истекла. Пожалуйста, создайте новую: /vpi convert lc <сумма>")
}
}
}
}
}
}

View File

@ -1,7 +1,7 @@
name: vp_server_integration
name: VpcSpigotIntegration
version: '1.0'
main: main.vp_server_integration.Vp_server_integration
load: STARTUP
main: main.VpcSpigotIntegration.VpcServerIntegration
load: POSTWORLD
authors: [ _SAN5_SkeLet0n_ ]
description: Integrate VPC into your server
website: voidproject.del.pw