Compare commits
No commits in common. "master" and "v0.0.1" have entirely different histories.
@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
### Функциональность на данный момент:
|
### Функциональность на данный момент:
|
||||||
* Конвертация VPC в локальную валюту и наоборот
|
* Конвертация VPC в локальную валюту и наоборот
|
||||||
|
|
||||||
|
### Разрабатывается:
|
||||||
|
* Оплата по табличке
|
||||||
* Покупка из сундука/... по табличке
|
* Покупка из сундука/... по табличке
|
||||||
|
|
||||||
# Как использовать?
|
# Как использовать?
|
||||||
|
|||||||
12
pom.xml
12
pom.xml
@ -5,11 +5,11 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>main</groupId>
|
<groupId>main</groupId>
|
||||||
<artifactId>VpcSpigotIntegration</artifactId>
|
<artifactId>vp_server_integration</artifactId>
|
||||||
<version>1.0</version>
|
<version>1.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>VpcSpigotIntegration</name>
|
<name>vp_server_integration</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
@ -106,10 +106,10 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<echo message="UPLOADING file to ${ftps.username}@${ftps.host} ${srv.ftps.remote.directory}"/>
|
<echo message="UPLOADING file to ${ftps.username}@${ftps.host} ${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}"/>
|
<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}"/>
|
||||||
<exec executable="sh">
|
<exec executable="sh">
|
||||||
<arg value="-c"/>
|
<arg value="-c"/>
|
||||||
<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}"/>
|
<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}"/>
|
||||||
</exec>
|
</exec>
|
||||||
</target>
|
</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
@ -125,10 +125,10 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<echo message="SENDING reload plugin request to ${srv.restart.endpoint}"/>-->
|
<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 VpcSpigotIntegration'"/>-->
|
<echo message="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload vp_server_integration'"/>-->
|
||||||
<exec executable="sh">
|
<exec executable="sh">
|
||||||
<arg value="-c"/>
|
<arg value="-c"/>
|
||||||
<arg value="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload VpcSpigotIntegration'"/>
|
<arg value="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.command.endpoint} -d 'plugman reload vp_server_integration'"/>
|
||||||
</exec>
|
</exec>
|
||||||
<!-- <echo message="SENDING restart request to ${srv.restart.endpoint}"/>-->
|
<!-- <echo message="SENDING restart request to ${srv.restart.endpoint}"/>-->
|
||||||
<!-- <echo message="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.restart.endpoint}"/>-->
|
<!-- <echo message="curl -k -X POST -H 'Authorization: Bearer ${minihost.token}' ${srv.restart.endpoint}"/>-->
|
||||||
|
|||||||
@ -1,164 +0,0 @@
|
|||||||
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,888 +0,0 @@
|
|||||||
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,14 +1,11 @@
|
|||||||
package main.VpcSpigotIntegration
|
package main.vp_server_integration
|
||||||
|
|
||||||
// -------------- Capture output ----------------
|
// -------------- Capture output ----------------
|
||||||
// Here is implemented capture of output command (for dynamic course)
|
// Here is implemented capture of output command (for dynamic course)
|
||||||
// ----------------------------------------------
|
// ----------------------------------------------
|
||||||
|
|
||||||
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PLUGIN
|
|
||||||
import org.bukkit.Bukkit
|
import org.bukkit.Bukkit
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class CommandCapture {
|
class CommandCapture {
|
||||||
companion object {
|
companion object {
|
||||||
@ -37,73 +34,16 @@ class CommandCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 ->
|
|
||||||
output.add(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
Bukkit.dispatchCommand(customSender, command)
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
const val MAX_WAIT_TIME_MS = 5000
|
|
||||||
const val CHECK_INTERVAL_MS: Long = 300
|
|
||||||
|
|
||||||
fun execute(command: String): List<String> {
|
fun execute(command: String): List<String> {
|
||||||
val output = mutableListOf<String>()
|
val output = mutableListOf<String>()
|
||||||
var isCommandFinished = false
|
|
||||||
|
|
||||||
val customSender = createCapturingSender { message ->
|
val customSender = createCapturingSender { message ->
|
||||||
output.add(message)
|
output.add(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
Bukkit.getScheduler().runTask(PLUGIN) {
|
|
||||||
Bukkit.dispatchCommand(customSender, command)
|
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
|
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,6 +1,7 @@
|
|||||||
package main.VpcSpigotIntegration
|
package main.vp_server_integration
|
||||||
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration
|
import org.bukkit.configuration.file.YamlConfiguration
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DataManager(private val dataFolder: File) {
|
class DataManager(private val dataFolder: File) {
|
||||||
@ -1,17 +1,18 @@
|
|||||||
package main.VpcSpigotIntegration
|
package main.vp_server_integration
|
||||||
|
|
||||||
// ----------------- VPC API --------------------
|
// ----------------- VPC API --------------------
|
||||||
// Here is modules for check global balance (for further using in DYNAMIC course)
|
// Here is modules for check global balance (for further using in DYNAMIC course)
|
||||||
// ----------------------------------------------
|
// ----------------------------------------------
|
||||||
|
|
||||||
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.LOGGER
|
import main.vp_server_integration.Vp_server_integration.Companion.LOGGER
|
||||||
|
|
||||||
class TotalBalanceModules {
|
class TotalBalanceModules {
|
||||||
companion object {
|
companion object {
|
||||||
fun getEssentialsBalance(): Float {
|
fun getEssentialsBalance(): Float {
|
||||||
LOGGER.info("Calculating dynamic rate using baltop")
|
LOGGER.info("Calculating dynamic rate using baltop")
|
||||||
// Force update balance top and delay to avoid empty string
|
// Force update balance top and delay to avoid empty string
|
||||||
val output = CommandCapture.execute("baltop force")
|
CommandCapture.execute("baltop force")
|
||||||
|
val output = CommandCapture.execute("baltop")
|
||||||
LOGGER.info("Balance top command output: $output")
|
LOGGER.info("Balance top command output: $output")
|
||||||
|
|
||||||
var total = 0.0f
|
var total = 0.0f
|
||||||
@ -1,13 +1,15 @@
|
|||||||
package main.VpcSpigotIntegration
|
package main.vp_server_integration
|
||||||
|
|
||||||
// ----------------- VPC API --------------------
|
// ----------------- VPC API --------------------
|
||||||
// Here is utils help for working with some stuff
|
// Here is utils help for working with some stuff
|
||||||
// ----------------------------------------------
|
// ----------------------------------------------
|
||||||
|
|
||||||
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PREFIX
|
import main.vp_server_integration.Vp_server_integration.Companion.PREFIX
|
||||||
import org.bukkit.ChatColor
|
import org.bukkit.ChatColor
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
|
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
class MyLogger(debugEnabled: Boolean, private val loggerOriginal: java.util.logging.Logger) {
|
class MyLogger(debugEnabled: Boolean, private val loggerOriginal: java.util.logging.Logger) {
|
||||||
private val debug_enabled = debugEnabled
|
private val debug_enabled = debugEnabled
|
||||||
private val loggerSpigot = loggerOriginal
|
private val loggerSpigot = loggerOriginal
|
||||||
@ -20,7 +22,7 @@ class MyLogger(debugEnabled: Boolean, private val loggerOriginal: java.util.logg
|
|||||||
|
|
||||||
fun error(message: String) {
|
fun error(message: String) {
|
||||||
if (debug_enabled) {
|
if (debug_enabled) {
|
||||||
loggerSpigot.severe("[DBG] $message")
|
loggerSpigot.severe("[DBG] message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,724 @@
|
|||||||
|
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,4 +1,4 @@
|
|||||||
package main.VpcSpigotIntegration
|
package main.vp_server_integration
|
||||||
|
|
||||||
// ----------------- VPC API --------------------
|
// ----------------- VPC API --------------------
|
||||||
// Here is implemented calls to VPC UserAPI
|
// Here is implemented calls to VPC UserAPI
|
||||||
@ -8,25 +8,29 @@ import com.google.gson.Gson
|
|||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USERNAME
|
import main.vp_server_integration.Vp_server_integration.Companion.PREFIX
|
||||||
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USER_API_URL
|
import main.vp_server_integration.Vp_server_integration.Companion.USERNAME
|
||||||
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.USER_TOKEN
|
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 org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.entity.Player
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class VpcApi {
|
class VpcApi {
|
||||||
companion object {
|
companion object {
|
||||||
// Rate limiting: lock and last execution time
|
// Rate limiting: track timestamps of requests
|
||||||
private val rateLimitLock = Object()
|
private val requestTimestamps = ConcurrentLinkedQueue<Long>()
|
||||||
private var lastExecutionTime = 0L
|
private const val MAX_REQUESTS_PER_MINUTE = 60
|
||||||
private const val MIN_INTERVAL_MILLIS = 1000L // 1 second between requests
|
private const val MINUTE_IN_MILLIS = 60 * 1000L
|
||||||
|
|
||||||
// Using Gson for JSON serialization - much cleaner than manual string building
|
// Using Gson for JSON serialization - much cleaner than manual string building
|
||||||
fun jsonify(args: List<String>): String {
|
fun jsonify(args: List<String>): String {
|
||||||
@ -49,27 +53,37 @@ class VpcApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces rate limiting by ensuring requests are executed one at a time
|
* Enforces rate limiting by checking if we've exceeded the request limit.
|
||||||
* with at least 1 second between them.
|
* If so, sleeps until we're within the limit again.
|
||||||
*/
|
*/
|
||||||
private fun enforceRateLimit() {
|
private fun enforceRateLimit() {
|
||||||
synchronized(rateLimitLock) {
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val timeSinceLastExecution = now - lastExecutionTime
|
|
||||||
|
|
||||||
// If less than 1 second has passed since last execution, wait
|
// Remove timestamps older than 1 minute
|
||||||
if (timeSinceLastExecution < MIN_INTERVAL_MILLIS) {
|
while (requestTimestamps.isNotEmpty() && now - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
|
||||||
val sleepTime = MIN_INTERVAL_MILLIS - timeSinceLastExecution
|
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
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(sleepTime)
|
Thread.sleep(sleepTime)
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
Thread.currentThread().interrupt()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last execution time to now
|
// Add current timestamp
|
||||||
lastExecutionTime = System.currentTimeMillis()
|
requestTimestamps.offer(now)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse {
|
fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
name: VpcSpigotIntegration
|
name: vp_server_integration
|
||||||
version: '1.0'
|
version: '1.0'
|
||||||
main: main.VpcSpigotIntegration.VpcServerIntegration
|
main: main.vp_server_integration.Vp_server_integration
|
||||||
load: POSTWORLD
|
load: STARTUP
|
||||||
authors: [ _SAN5_SkeLet0n_ ]
|
authors: [ _SAN5_SkeLet0n_ ]
|
||||||
description: Integrate VPC into your server
|
description: Integrate VPC into your server
|
||||||
website: voidproject.del.pw
|
website: voidproject.del.pw
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user