Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c89dc74fba | |||
| 64dae5fdc4 | |||
| 1dbb100f97 | |||
| 57dc685c5d | |||
| 5bd184ec2a | |||
| 79d21942bb | |||
| df0561fe51 | |||
| 0ce332f050 | |||
| 63f0648387 |
@ -6,9 +6,6 @@
|
||||
|
||||
### Функциональность на данный момент:
|
||||
* Конвертация VPC в локальную валюту и наоборот
|
||||
|
||||
### Разрабатывается:
|
||||
* Оплата по табличке
|
||||
* Покупка из сундука/... по табличке
|
||||
|
||||
# Как использовать?
|
||||
|
||||
12
pom.xml
12
pom.xml
@ -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}"/>-->
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
164
src/main/kotlin/main/VpcSpigotIntegration/SignHandler.kt
Normal file
164
src/main/kotlin/main/VpcSpigotIntegration/SignHandler.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
@ -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Ваша оплата истекла. Нажмите ещё раз на табличку.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 <сумма>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user