From df0561fe511de85f7ca62bdf9633043219729135 Mon Sep 17 00:00:00 2001 From: justuser-31 Date: Sat, 29 Nov 2025 17:11:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B3=D0=BE=D0=B2:=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=B1=D0=B5=D0=B3=D0=B0=D0=B5=D0=BC=20=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2,=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B?= =?UTF-8?q?,=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=81=D1=83=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20UserAPI.=20=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8:=20=D0=B0=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B4=D0=BB=D1=8F=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=D1=85=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VpcSpigotIntegration/CommandCapture.kt | 62 ++++++++++++++++- .../TotalBalanceModules.kt | 3 +- .../main/VpcSpigotIntegration/VpcApi.kt | 48 ++++++-------- .../VpcServerIntegration.kt | 66 +++++++++++-------- 4 files changed, 121 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/main/VpcSpigotIntegration/CommandCapture.kt b/src/main/kotlin/main/VpcSpigotIntegration/CommandCapture.kt index e041677..6e4f688 100644 --- a/src/main/kotlin/main/VpcSpigotIntegration/CommandCapture.kt +++ b/src/main/kotlin/main/VpcSpigotIntegration/CommandCapture.kt @@ -4,8 +4,11 @@ package main.VpcSpigotIntegration // Here is implemented capture of output command (for dynamic course) // ---------------------------------------------- +import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PLUGIN import org.bukkit.Bukkit import org.bukkit.command.CommandSender +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit class CommandCapture { companion object { @@ -34,7 +37,8 @@ class CommandCapture { } } - fun execute(command: String): List { + // Deprecated stupid version for sync version which catch output not every time + fun executeOld(command: String): List { val output = mutableListOf() val customSender = createCapturingSender { message -> @@ -45,5 +49,61 @@ class CommandCapture { return output } + + const val MAX_WAIT_TIME_MS = 5000 + const val CHECK_INTERVAL_MS: Long = 300 + + fun execute(command: String): List { + val output = mutableListOf() + var isCommandFinished = false + + val customSender = createCapturingSender { message -> + output.add(message) + } + + Bukkit.getScheduler().runTask(PLUGIN) { + Bukkit.dispatchCommand(customSender, command) + } + + // Wait for command completion with timeout + val startTime = System.currentTimeMillis() + while (!isCommandFinished && (System.currentTimeMillis() - startTime) < MAX_WAIT_TIME_MS) { + try { + Thread.sleep(CHECK_INTERVAL_MS) + if (output.isNotEmpty()) { + isCommandFinished = true + } + } catch (e: InterruptedException) { + break + } + } + + return output + } + + // Untested another version + fun executeV3(command: String): List { + val output = mutableListOf() + 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() + } } } \ No newline at end of file diff --git a/src/main/kotlin/main/VpcSpigotIntegration/TotalBalanceModules.kt b/src/main/kotlin/main/VpcSpigotIntegration/TotalBalanceModules.kt index 9546d3e..10498f0 100644 --- a/src/main/kotlin/main/VpcSpigotIntegration/TotalBalanceModules.kt +++ b/src/main/kotlin/main/VpcSpigotIntegration/TotalBalanceModules.kt @@ -11,8 +11,7 @@ class TotalBalanceModules { fun getEssentialsBalance(): Float { LOGGER.info("Calculating dynamic rate using baltop") // Force update balance top and delay to avoid empty string - CommandCapture.execute("baltop force") - val output = CommandCapture.execute("baltop") + val output = CommandCapture.execute("baltop force") LOGGER.info("Balance top command output: $output") var total = 0.0f diff --git a/src/main/kotlin/main/VpcSpigotIntegration/VpcApi.kt b/src/main/kotlin/main/VpcSpigotIntegration/VpcApi.kt index 76bdc11..f36208e 100644 --- a/src/main/kotlin/main/VpcSpigotIntegration/VpcApi.kt +++ b/src/main/kotlin/main/VpcSpigotIntegration/VpcApi.kt @@ -23,10 +23,10 @@ import kotlin.math.max class VpcApi { companion object { - // Rate limiting: track timestamps of requests - private val requestTimestamps = ConcurrentLinkedQueue() - private const val MAX_REQUESTS_PER_MINUTE = 60 - private const val MINUTE_IN_MILLIS = 60 * 1000L + // Rate limiting: lock and last execution time + private val rateLimitLock = Object() + private var lastExecutionTime = 0L + private const val MIN_INTERVAL_MILLIS = 1000L // 1 second between requests // Using Gson for JSON serialization - much cleaner than manual string building fun jsonify(args: List): String { @@ -49,37 +49,27 @@ class VpcApi { } /** - * Enforces rate limiting by checking if we've exceeded the request limit. - * If so, sleeps until we're within the limit again. + * Enforces rate limiting by ensuring requests are executed one at a time + * with at least 1 second between them. */ private fun enforceRateLimit() { - val now = System.currentTimeMillis() + synchronized(rateLimitLock) { + val now = System.currentTimeMillis() + val timeSinceLastExecution = now - lastExecutionTime - // Remove timestamps older than 1 minute - while (requestTimestamps.isNotEmpty() && now - requestTimestamps.peek() > MINUTE_IN_MILLIS) { - requestTimestamps.poll() - } - - // If we've hit the limit, calculate sleep time - if (requestTimestamps.size >= MAX_REQUESTS_PER_MINUTE) { - val oldestRequestTime = requestTimestamps.peek() - val sleepTime = max(0, MINUTE_IN_MILLIS - (now - oldestRequestTime)) + 100 // Add small buffer - - try { - Thread.sleep(sleepTime) - } catch (e: InterruptedException) { - Thread.currentThread().interrupt() + // If less than 1 second has passed since last execution, wait + if (timeSinceLastExecution < MIN_INTERVAL_MILLIS) { + val sleepTime = MIN_INTERVAL_MILLIS - timeSinceLastExecution + try { + Thread.sleep(sleepTime) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } } - // After sleeping, clean up old timestamps again - val afterSleep = System.currentTimeMillis() - while (requestTimestamps.isNotEmpty() && afterSleep - requestTimestamps.peek() > MINUTE_IN_MILLIS) { - requestTimestamps.poll() - } + // Update last execution time to now + lastExecutionTime = System.currentTimeMillis() } - - // Add current timestamp - requestTimestamps.offer(now) } fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse { diff --git a/src/main/kotlin/main/VpcSpigotIntegration/VpcServerIntegration.kt b/src/main/kotlin/main/VpcSpigotIntegration/VpcServerIntegration.kt index eedc0c5..c5d0563 100644 --- a/src/main/kotlin/main/VpcSpigotIntegration/VpcServerIntegration.kt +++ b/src/main/kotlin/main/VpcSpigotIntegration/VpcServerIntegration.kt @@ -21,6 +21,7 @@ import java.io.FileInputStream import java.util.Properties import kotlin.collections.mutableMapOf import kotlin.math.abs +import kotlin.text.isEmpty class VpcServerIntegration() : JavaPlugin(), CommandExecutor { companion object { @@ -216,32 +217,31 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS } override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { - if (command.name.equals("vpi", ignoreCase = true)) { - if (sender !is Player) { - sender.sendMessage("&cТолько игроки могут выполнять данные команды.") - return true - } + 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 { - // Handle currency conversion command - (args.size == 3 || args.size == 4) && args[0] == "convert" -> { + // Currency conversion + args.size in 3..4 && args[0] == "convert" -> { handleCurrencyConversion(sender, args) } args.isNotEmpty() && args[0] == "convert" -> { Utils.send(sender, "/vpi convert <сумма>") } - // Handle course command + // Course info 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?") + 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) @@ -250,7 +250,7 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS } } - // Handle authentication command + // Authentication args.size == 2 && args[0] == "auth" -> { handleAuthentication(sender, args) } @@ -258,14 +258,14 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS Utils.send(sender, "/vpi auth <ник>") } - // Show general help + // Default help menu else -> { showHelpMenu(sender) } } - return true - } - return false + }) + + return true } /** @@ -320,7 +320,9 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS return if (COURSE_MODE == "static") { COURSE_STATIC_VALUE } else { - Utils.round(calculateDynamicRate(), NUM_AFTER_DOT) + val dynamicRate: Any = calculateDynamicRate() + if (dynamicRate != 0.0) Utils.round(dynamicRate, NUM_AFTER_DOT) + else return 0.0 } } @@ -334,7 +336,8 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS val vpcUser = VpcApi.user_in_db(username = USERNAME) if (vpcUser == null) { - throw Exception("VPC user not found") + return 0.0 +// throw Exception("VPC user not found") } val vpcBalance = vpcUser["balance"].toString().toDouble() @@ -349,7 +352,8 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS val vpcUser = VpcApi.user_in_db(username = USERNAME) if (vpcUser == null) { - throw Exception("VPC user not found") + return 0.0 +// throw Exception("VPC user not found") } val vpcBalance = vpcUser["balance"].toString().toDouble() @@ -423,6 +427,11 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS // 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() @@ -475,18 +484,23 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS } // Start new authentication process - val invoiceId = VpcApi.create_invoice(0.001) + 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.toString() - INVOICE_CREATION_TIMES[invoiceId.toString()] = System.currentTimeMillis() + TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId + INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis() } LOGGER.info("Authentication lists updated") - sendAuthPrompt(sender, invoiceId.toString()) + sendAuthPrompt(sender, invoiceId) } /**