<?php __HALT_COMPILER(); ?>
¤  #               
   plugin.yml>      >  ;"º¤         resources/config.yml        2ãìפ         resources/lang/deu.yml¸      ¸  dzƒõ¤         resources/lang/eng.ymlÌ
      Ì
  üåkí¤         resources/lang/fra.ymlÒ      Ò  *6¿¤         resources/lang/ind.ymlê
      ê
  }̥!¤         resources/lang/ita.yml6      6   :Àõ¤         resources/lang/jpn.yml�      �  ˆ¡Ê¤         resources/lang/kor.ymlv      v  ßpXҤ         resources/lang/por.yml&      &  ˆõÄ¤         resources/lang/rus.ymlV      V  v‹¹¤         resources/lang/spa.yml        £Þ@¤         resources/lang/tur.ymlé
      é
  »gÿ‘¤         resources/lang/ukr.yml*      *  ”s¤         resources/lang/vie.ymlb      b  ¦r�!¤         resources/lang/zho.yml§
      §
  1³…N¤         resources/simplesql/mysql.sqlb      b  ¿õ¥½¤         resources/simplesql/sqlite.sql1      1  Q;kH¤      *   src/NhanAZ/SimpleEconomy/BalanceAmount.php        CYµœ¤      *   src/NhanAZ/SimpleEconomy/BalanceReader.php€      €  Œq¤      .   src/NhanAZ/SimpleEconomy/CurrencyFormatter.php½      ½  ¨Փs¤      4   src/NhanAZ/SimpleEconomy/InvalidBalanceException.phpª       ª   µ³{±¤      (   src/NhanAZ/SimpleEconomy/LangManager.phpM      M  ÊV.ó¤      ,   src/NhanAZ/SimpleEconomy/LeaderboardTask.phpà      à  1ò€Ҥ      !   src/NhanAZ/SimpleEconomy/Main.phpj[      j[  2ž$]¤      -   src/NhanAZ/SimpleEconomy/ScoreHudListener.phpÛ      Û  ¯\3¤      4   src/NhanAZ/SimpleEconomy/command/AddMoneyCommand.phpí      í  @ãðT¤      1   src/NhanAZ/SimpleEconomy/command/MoneyCommand.php        'ã‚U¤      /   src/NhanAZ/SimpleEconomy/command/PayCommand.php        Œxh¤      7   src/NhanAZ/SimpleEconomy/command/ReduceMoneyCommand.php˜      ˜  .•é¤      4   src/NhanAZ/SimpleEconomy/command/SetMoneyCommand.php          Éô”¤      4   src/NhanAZ/SimpleEconomy/command/TopMoneyCommand.php        ò†'¤      3   src/NhanAZ/SimpleEconomy/event/TransactionEvent.php)      )  8-�¤      9   src/NhanAZ/SimpleEconomy/event/TransactionSubmitEvent.php‡      ‡  š̞~¤      :   src/NhanAZ/SimpleEconomy/event/TransactionSuccessEvent.php=      =  e�i¤      name: SimpleEconomy
main: NhanAZ\SimpleEconomy\Main
version: 1.1.1
api: 5.0.0
author: NhanAZ
description: "An economy plugin powered by SimpleSQL hybrid SQL-YAML storage with paired SQL transfers."
softdepend: ScoreHud

permissions:
  simpleeconomy.command.money:
    description: "View balances."
    default: true
  simpleeconomy.command.pay:
    description: "Transfer money to another player."
    default: true
  simpleeconomy.command.topmoney:
    description: "View the richest players leaderboard."
    default: true
  simpleeconomy.command.setmoney:
    description: "Set a player's balance."
    default: op
  simpleeconomy.command.addmoney:
    description: "Add money to a player's balance."
    default: op
  simpleeconomy.command.reducemoney:
    description: "Reduce money from a player's balance."
    default: op
# ──────────────────────────────────────────────────────────
#  SimpleEconomy - Configuration
# ──────────────────────────────────────────────────────────

# Do not change this value. Used for automatic config migration detection.
config-version: 1

# Language: "eng" (English), "vie" (Tiếng Việt), "kor" (한국어), "rus" (Русский),
#           "spa" (Español), "ukr" (Українська), "zho" (简体中文), "ind" (Bahasa Indonesia),
#           "tur" (Türkçe), "fra" (Français), "por" (Português), "deu" (Deutsch),
#           "jpn" (日本語), "ita" (Italiano)
language: "eng"

# Starting balance for new players.
default-balance: 1000

# Currency settings.
currency:
  # Symbol displayed before amounts.
  symbol: "$"
  # Formatter mode: "default" ($1,000,000) or "compact" ($1.5K, $2.3M, $1B)
  formatter: "default"

# Number of entries per page in /topmoney.
topmoney-per-page: 10

# Maximum number of entries in the leaderboard cache.
# Higher = more complete leaderboard, but uses more memory.
leaderboard-size: 100

# Database configuration (passed directly to SimpleSQL / libasynql).
database:
  # Database type: "sqlite" or "mysql"
  type: sqlite
  sqlite:
    file: economy.sqlite
  mysql:
    host: 127.0.0.1
    username: root
    password: ""
    schema: simple_economy
  worker-limit: 1
# ──────────────────────────────────────────────
#  SimpleEconomy - Deutsche Sprachdatei
# ──────────────────────────────────────────────

general:
  data-loading: "§eDaten von '{player}' werden geladen. Bitte warten..."
  data-access-error: "§cZugriff auf die Daten von '{player}' nicht möglich."
  save-failed: "§cSpeichern der Daten von '{player}' fehlgeschlagen."
  no-economy-data: "§cSpieler '{player}' hat keine Wirtschaftsdaten."
  player-not-online: "§cSpieler '{player}' ist nicht online."
  only-ingame: "§cDieser Befehl kann nur im Spiel verwendet werden."
  amount-not-number: "§cDer Betrag muss eine Zahl sein."
  amount-positive: "§cDer Betrag muss größer als Null sein."
  amount-not-negative: "§cDer Betrag darf nicht negativ sein."
  transaction-cancelled: "§cTransaktion abgebrochen."

money:
  usage: "§cVerwendung: /money <Spieler>"
  self: "§aDein Kontostand: §e{balance} §7(#{rank})"
  self-unranked: "§aDein Kontostand: §e{balance}"
  other: "§aKontostand von {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aKontostand von {player}: §e{balance}"
  error: "§cDaten von '{player}' konnten nicht abgerufen werden."

pay:
  usage: "§cVerwendung: /pay <Spieler> <Betrag>"
  no-self: "§cDu kannst dir nicht selbst Geld senden."
  loading-self: "§eDeine Daten werden noch geladen. Bitte warte einen Moment."
  loading-target: "§eDaten von {player} werden geladen. Bitte warten..."
  insufficient: "§cNicht genug Geld. Dein Kontostand: §e{balance}"
  cancelled: "§cZahlung abgebrochen."
  sent: "§aDu hast §e{amount}§a an §b{player}§a gesendet."
  received: "§aDu hast §e{amount}§a von §b{player}§a erhalten."

setmoney:
  usage: "§cVerwendung: /setmoney <Spieler> <Betrag>"
  success: "§aKontostand von §b{player}§a gesetzt: §e{old} §a→ §e{new}"
  notify: "§aDein Kontostand wurde von einem Administrator auf §e{amount}§a gesetzt."

addmoney:
  usage: "§cVerwendung: /addmoney <Spieler> <Betrag>"
  success: "§a§e{amount}§a zu §b{player}§a hinzugefügt. Neuer Kontostand: §e{balance}"
  notify: "§aDu hast §e{amount}§a von einem Administrator erhalten. Neuer Kontostand: §e{balance}"

reducemoney:
  usage: "§cVerwendung: /reducemoney <Spieler> <Betrag>"
  insufficient: "§c{player} hat nur §e{balance}§c. Kann §e{amount}§c nicht abziehen."
  success: "§a§e{amount}§a von §b{player}§a abgezogen. Neuer Kontostand: §e{balance}"
  notify: "§cEin Administrator hat §e{amount}§c von deinem Kontostand abgezogen. Neuer Kontostand: §e{balance}"

topmoney:
  empty: "§eNoch keine Wirtschaftsdaten vorhanden."
  header: "§6══ Top Geld §7(Seite {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - English Language File
# ──────────────────────────────────────────────

general:
  data-loading: "§eData for '{player}' is still loading. Please wait..."
  data-access-error: "§cCould not access data for '{player}'."
  save-failed: "§cFailed to save data for '{player}'."
  no-economy-data: "§cPlayer '{player}' has no economy data."
  player-not-online: "§cPlayer '{player}' is not online."
  only-ingame: "§cThis command can only be used in-game."
  amount-not-number: "§cAmount must be a number."
  amount-positive: "§cAmount must be greater than zero."
  amount-not-negative: "§cAmount cannot be negative."
  transaction-cancelled: "§cTransaction was cancelled."

money:
  usage: "§cUsage: /money <player>"
  self: "§aYour balance: §e{balance} §7(#{rank})"
  self-unranked: "§aYour balance: §e{balance}"
  other: "§a{player}'s balance: §e{balance} §7(#{rank})"
  other-unranked: "§a{player}'s balance: §e{balance}"
  error: "§cCould not retrieve data for '{player}'."

pay:
  usage: "§cUsage: /pay <player> <amount>"
  no-self: "§cYou cannot pay yourself."
  loading-self: "§eYour data is still loading. Please wait a moment."
  loading-target: "§e{player}'s data is still loading. Please wait..."
  insufficient: "§cInsufficient funds. Your balance: §e{balance}"
  cancelled: "§cPayment was cancelled."
  sent: "§aYou sent §e{amount}§a to §b{player}§a."
  received: "§aYou received §e{amount}§a from §b{player}§a."

setmoney:
  usage: "§cUsage: /setmoney <player> <amount>"
  success: "§aSet §b{player}§a's balance: §e{old} §a→ §e{new}"
  notify: "§aYour balance has been set to §e{amount}§a by an administrator."

addmoney:
  usage: "§cUsage: /addmoney <player> <amount>"
  success: "§aAdded §e{amount}§a to §b{player}§a. New balance: §e{balance}"
  notify: "§aYou received §e{amount}§a from an administrator. New balance: §e{balance}"

reducemoney:
  usage: "§cUsage: /reducemoney <player> <amount>"
  insufficient: "§c{player} only has §e{balance}§c. Cannot reduce §e{amount}§c."
  success: "§aRemoved §e{amount}§a from §b{player}§a. New balance: §e{balance}"
  notify: "§c§e{amount}§c was removed from your balance by an administrator. New balance: §e{balance}"

topmoney:
  empty: "§eNo economy data available yet."
  header: "§6══ Top Money §7(Page {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Fichier de langue Français
# ──────────────────────────────────────────────

general:
  data-loading: "§eLes données de '{player}' sont en cours de chargement. Veuillez patienter..."
  data-access-error: "§cImpossible d'accéder aux données de '{player}'."
  save-failed: "§cÉchec de la sauvegarde des données de '{player}'."
  no-economy-data: "§cLe joueur '{player}' n'a pas de données économiques."
  player-not-online: "§cLe joueur '{player}' n'est pas en ligne."
  only-ingame: "§cCette commande ne peut être utilisée qu'en jeu."
  amount-not-number: "§cLe montant doit être un nombre."
  amount-positive: "§cLe montant doit être supérieur à zéro."
  amount-not-negative: "§cLe montant ne peut pas être négatif."
  transaction-cancelled: "§cTransaction annulée."

money:
  usage: "§cUtilisation : /money <joueur>"
  self: "§aVotre solde : §e{balance} §7(#{rank})"
  self-unranked: "§aVotre solde : §e{balance}"
  other: "§aSolde de {player} : §e{balance} §7(#{rank})"
  other-unranked: "§aSolde de {player} : §e{balance}"
  error: "§cImpossible de récupérer les données de '{player}'."

pay:
  usage: "§cUtilisation : /pay <joueur> <montant>"
  no-self: "§cVous ne pouvez pas vous payer vous-même."
  loading-self: "§eVos données sont en cours de chargement. Veuillez patienter."
  loading-target: "§eLes données de {player} sont en cours de chargement. Veuillez patienter..."
  insufficient: "§cFonds insuffisants. Votre solde : §e{balance}"
  cancelled: "§cPaiement annulé."
  sent: "§aVous avez envoyé §e{amount}§a à §b{player}§a."
  received: "§aVous avez reçu §e{amount}§a de §b{player}§a."

setmoney:
  usage: "§cUtilisation : /setmoney <joueur> <montant>"
  success: "§aSolde de §b{player}§a défini : §e{old} §a→ §e{new}"
  notify: "§aVotre solde a été défini à §e{amount}§a par un administrateur."

addmoney:
  usage: "§cUtilisation : /addmoney <joueur> <montant>"
  success: "§aAjouté §e{amount}§a à §b{player}§a. Nouveau solde : §e{balance}"
  notify: "§aVous avez reçu §e{amount}§a d'un administrateur. Nouveau solde : §e{balance}"

reducemoney:
  usage: "§cUtilisation : /reducemoney <joueur> <montant>"
  insufficient: "§c{player} n'a que §e{balance}§c. Impossible de retirer §e{amount}§c."
  success: "§aRetiré §e{amount}§a de §b{player}§a. Nouveau solde : §e{balance}"
  notify: "§cUn administrateur a retiré §e{amount}§c de votre solde. Nouveau solde : §e{balance}"

topmoney:
  empty: "§eAucune donnée économique disponible."
  header: "§6══ Top Argent §7(Page {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Berkas Bahasa Indonesia
# ──────────────────────────────────────────────

general:
  data-loading: "§eData '{player}' sedang dimuat. Mohon tunggu..."
  data-access-error: "§cTidak dapat mengakses data '{player}'."
  save-failed: "§cGagal menyimpan data '{player}'."
  no-economy-data: "§cPemain '{player}' tidak memiliki data ekonomi."
  player-not-online: "§cPemain '{player}' tidak online."
  only-ingame: "§cPerintah ini hanya bisa digunakan di dalam game."
  amount-not-number: "§cJumlah harus berupa angka."
  amount-positive: "§cJumlah harus lebih dari nol."
  amount-not-negative: "§cJumlah tidak boleh negatif."
  transaction-cancelled: "§cTransaksi dibatalkan."

money:
  usage: "§cPenggunaan: /money <pemain>"
  self: "§aSaldo kamu: §e{balance} §7(#{rank})"
  self-unranked: "§aSaldo kamu: §e{balance}"
  other: "§aSaldo {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aSaldo {player}: §e{balance}"
  error: "§cTidak dapat mengambil data '{player}'."

pay:
  usage: "§cPenggunaan: /pay <pemain> <jumlah>"
  no-self: "§cKamu tidak bisa membayar dirimu sendiri."
  loading-self: "§eData kamu sedang dimuat. Mohon tunggu."
  loading-target: "§eData {player} sedang dimuat. Mohon tunggu..."
  insufficient: "§cSaldo tidak cukup. Saldo kamu: §e{balance}"
  cancelled: "§cPembayaran dibatalkan."
  sent: "§aKamu mengirim §e{amount}§a ke §b{player}§a."
  received: "§aKamu menerima §e{amount}§a dari §b{player}§a."

setmoney:
  usage: "§cPenggunaan: /setmoney <pemain> <jumlah>"
  success: "§aSaldo §b{player}§a diatur: §e{old} §a→ §e{new}"
  notify: "§aSaldo kamu telah diatur menjadi §e{amount}§a oleh administrator."

addmoney:
  usage: "§cPenggunaan: /addmoney <pemain> <jumlah>"
  success: "§aDitambahkan §e{amount}§a ke §b{player}§a. Saldo baru: §e{balance}"
  notify: "§aKamu menerima §e{amount}§a dari administrator. Saldo baru: §e{balance}"

reducemoney:
  usage: "§cPenggunaan: /reducemoney <pemain> <jumlah>"
  insufficient: "§c{player} hanya punya §e{balance}§c. Tidak bisa mengurangi §e{amount}§c."
  success: "§aDikurangi §e{amount}§a dari §b{player}§a. Saldo baru: §e{balance}"
  notify: "§c§e{amount}§c telah dikurangi dari saldo kamu oleh administrator. Saldo baru: §e{balance}"

topmoney:
  empty: "§eBelum ada data ekonomi."
  header: "§6══ Top Uang §7(Halaman {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - File di lingua Italiano
# ──────────────────────────────────────────────

general:
  data-loading: "§eI dati di '{player}' sono in caricamento. Attendere prego..."
  data-access-error: "§cImpossibile accedere ai dati di '{player}'."
  save-failed: "§cSalvataggio dei dati di '{player}' fallito."
  no-economy-data: "§cIl giocatore '{player}' non ha dati economici."
  player-not-online: "§cIl giocatore '{player}' non è online."
  only-ingame: "§cQuesto comando può essere usato solo in gioco."
  amount-not-number: "§cL'importo deve essere un numero."
  amount-positive: "§cL'importo deve essere maggiore di zero."
  amount-not-negative: "§cL'importo non può essere negativo."
  transaction-cancelled: "§cTransazione annullata."

money:
  usage: "§cUso: /money <giocatore>"
  self: "§aIl tuo saldo: §e{balance} §7(#{rank})"
  self-unranked: "§aIl tuo saldo: §e{balance}"
  other: "§aSaldo di {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aSaldo di {player}: §e{balance}"
  error: "§cImpossibile recuperare i dati di '{player}'."

pay:
  usage: "§cUso: /pay <giocatore> <importo>"
  no-self: "§cNon puoi pagare te stesso."
  loading-self: "§eI tuoi dati sono in caricamento. Attendere prego."
  loading-target: "§eI dati di {player} sono in caricamento. Attendere prego..."
  insufficient: "§cFondi insufficienti. Il tuo saldo: §e{balance}"
  cancelled: "§cPagamento annullato."
  sent: "§aHai inviato §e{amount}§a a §b{player}§a."
  received: "§aHai ricevuto §e{amount}§a da §b{player}§a."

setmoney:
  usage: "§cUso: /setmoney <giocatore> <importo>"
  success: "§aSaldo di §b{player}§a impostato: §e{old} §a→ §e{new}"
  notify: "§aIl tuo saldo è stato impostato a §e{amount}§a da un amministratore."

addmoney:
  usage: "§cUso: /addmoney <giocatore> <importo>"
  success: "§aAggiunto §e{amount}§a a §b{player}§a. Nuovo saldo: §e{balance}"
  notify: "§aHai ricevuto §e{amount}§a da un amministratore. Nuovo saldo: §e{balance}"

reducemoney:
  usage: "§cUso: /reducemoney <giocatore> <importo>"
  insufficient: "§c{player} ha solo §e{balance}§c. Impossibile ridurre di §e{amount}§c."
  success: "§aRimosso §e{amount}§a da §b{player}§a. Nuovo saldo: §e{balance}"
  notify: "§cUn amministratore ha rimosso §e{amount}§c dal tuo saldo. Nuovo saldo: §e{balance}"

topmoney:
  empty: "§eNessun dato economico disponibile."
  header: "§6══ Top Soldi §7(Pagina {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - 日本語言語ファイル
# ──────────────────────────────────────────────

general:
  data-loading: "§e'{player}' のデータを読み込み中です。しばらくお待ちください..."
  data-access-error: "§c'{player}' のデータにアクセスできませんでした。"
  save-failed: "§c'{player}' のデータの保存に失敗しました。"
  no-economy-data: "§cプレイヤー '{player}' の経済データがありません。"
  player-not-online: "§cプレイヤー '{player}' はオンラインではありません。"
  only-ingame: "§cこのコマンドはゲーム内でのみ使用できます。"
  amount-not-number: "§c金額は数字でなければなりません。"
  amount-positive: "§c金額は0より大きくなければなりません。"
  amount-not-negative: "§c金額はマイナスにできません。"
  transaction-cancelled: "§c取引がキャンセルされました。"

money:
  usage: "§c使い方: /money <プレイヤー>"
  self: "§aあなたの残高: §e{balance} §7(#{rank})"
  self-unranked: "§aあなたの残高: §e{balance}"
  other: "§a{player} の残高: §e{balance} §7(#{rank})"
  other-unranked: "§a{player} の残高: §e{balance}"
  error: "§c'{player}' のデータを取得できませんでした。"

pay:
  usage: "§c使い方: /pay <プレイヤー> <金額>"
  no-self: "§c自分自身に送金することはできません。"
  loading-self: "§eあなたのデータを読み込み中です。しばらくお待ちください。"
  loading-target: "§e{player} のデータを読み込み中です。しばらくお待ちください..."
  insufficient: "§c残高が不足しています。あなたの残高: §e{balance}"
  cancelled: "§c支払いがキャンセルされました。"
  sent: "§a§b{player}§a に §e{amount}§a を送金しました。"
  received: "§a§b{player}§a から §e{amount}§a を受け取りました。"

setmoney:
  usage: "§c使い方: /setmoney <プレイヤー> <金額>"
  success: "§a§b{player}§a の残高を設定: §e{old} §a→ §e{new}"
  notify: "§a管理者によってあなたの残高が §e{amount}§a に設定されました。"

addmoney:
  usage: "§c使い方: /addmoney <プレイヤー> <金額>"
  success: "§a§b{player}§a に §e{amount}§a を追加しました。新しい残高: §e{balance}"
  notify: "§a管理者から §e{amount}§a を受け取りました。新しい残高: §e{balance}"

reducemoney:
  usage: "§c使い方: /reducemoney <プレイヤー> <金額>"
  insufficient: "§c{player} の残高は §e{balance}§c しかありません。§e{amount}§c を引くことはできません。"
  success: "§a§b{player}§a から §e{amount}§a を差し引きました。新しい残高: §e{balance}"
  notify: "§c管理者によってあなたの残高から §e{amount}§c が差し引かれました。新しい残高: §e{balance}"

topmoney:
  empty: "§e経済データはまだありません。"
  header: "§6══ 所持金ランキング §7({page}/{max}ページ) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - 한국어 언어 파일
# ──────────────────────────────────────────────

general:
  data-loading: "§e'{player}'의 데이터를 불러오는 중입니다. 잠시 기다려주세요..."
  data-access-error: "§c'{player}'의 데이터에 접근할 수 없습니다."
  save-failed: "§c'{player}'의 데이터 저장에 실패했습니다."
  no-economy-data: "§c'{player}' 플레이어의 경제 데이터가 없습니다."
  player-not-online: "§c'{player}' 플레이어가 온라인이 아닙니다."
  only-ingame: "§c이 명령어는 게임 내에서만 사용할 수 있습니다."
  amount-not-number: "§c금액은 숫자여야 합니다."
  amount-positive: "§c금액은 0보다 커야 합니다."
  amount-not-negative: "§c금액은 음수가 될 수 없습니다."
  transaction-cancelled: "§c거래가 취소되었습니다."

money:
  usage: "§c사용법: /money <플레이어>"
  self: "§a내 잔액: §e{balance} §7(#{rank})"
  self-unranked: "§a내 잔액: §e{balance}"
  other: "§a{player}의 잔액: §e{balance} §7(#{rank})"
  other-unranked: "§a{player}의 잔액: §e{balance}"
  error: "§c'{player}'의 데이터를 가져올 수 없습니다."

pay:
  usage: "§c사용법: /pay <플레이어> <금액>"
  no-self: "§c자신에게 송금할 수 없습니다."
  loading-self: "§e내 데이터를 불러오는 중입니다. 잠시 기다려주세요."
  loading-target: "§e{player}의 데이터를 불러오는 중입니다. 잠시 기다려주세요..."
  insufficient: "§c잔액이 부족합니다. 현재 잔액: §e{balance}"
  cancelled: "§c송금이 취소되었습니다."
  sent: "§a§b{player}§a에게 §e{amount}§a을(를) 보냈습니다."
  received: "§a§b{player}§a로부터 §e{amount}§a을(를) 받았습니다."

setmoney:
  usage: "§c사용법: /setmoney <플레이어> <금액>"
  success: "§a§b{player}§a의 잔액 설정: §e{old} §a→ §e{new}"
  notify: "§a관리자에 의해 잔액이 §e{amount}§a(으)로 설정되었습니다."

addmoney:
  usage: "§c사용법: /addmoney <플레이어> <금액>"
  success: "§a§b{player}§a에게 §e{amount}§a 추가. 새 잔액: §e{balance}"
  notify: "§a관리자로부터 §e{amount}§a을(를) 받았습니다. 새 잔액: §e{balance}"

reducemoney:
  usage: "§c사용법: /reducemoney <플레이어> <금액>"
  insufficient: "§c{player}의 잔액은 §e{balance}§c뿐입니다. §e{amount}§c을(를) 차감할 수 없습니다."
  success: "§a§b{player}§a에서 §e{amount}§a 차감. 새 잔액: §e{balance}"
  notify: "§c관리자에 의해 §e{amount}§c이(가) 차감되었습니다. 새 잔액: §e{balance}"

topmoney:
  empty: "§e경제 데이터가 아직 없습니다."
  header: "§6══ 부자 순위 §7({page}/{max}페이지) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Arquivo de idioma Português
# ──────────────────────────────────────────────

general:
  data-loading: "§eOs dados de '{player}' estão carregando. Por favor aguarde..."
  data-access-error: "§cNão foi possível acessar os dados de '{player}'."
  save-failed: "§cFalha ao salvar os dados de '{player}'."
  no-economy-data: "§cO jogador '{player}' não possui dados econômicos."
  player-not-online: "§cO jogador '{player}' não está online."
  only-ingame: "§cEste comando só pode ser usado no jogo."
  amount-not-number: "§cO valor deve ser um número."
  amount-positive: "§cO valor deve ser maior que zero."
  amount-not-negative: "§cO valor não pode ser negativo."
  transaction-cancelled: "§cTransação cancelada."

money:
  usage: "§cUso: /money <jogador>"
  self: "§aSeu saldo: §e{balance} §7(#{rank})"
  self-unranked: "§aSeu saldo: §e{balance}"
  other: "§aSaldo de {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aSaldo de {player}: §e{balance}"
  error: "§cNão foi possível obter os dados de '{player}'."

pay:
  usage: "§cUso: /pay <jogador> <valor>"
  no-self: "§cVocê não pode pagar a si mesmo."
  loading-self: "§eSeus dados estão carregando. Por favor aguarde."
  loading-target: "§eOs dados de {player} estão carregando. Por favor aguarde..."
  insufficient: "§cSaldo insuficiente. Seu saldo: §e{balance}"
  cancelled: "§cPagamento cancelado."
  sent: "§aVocê enviou §e{amount}§a para §b{player}§a."
  received: "§aVocê recebeu §e{amount}§a de §b{player}§a."

setmoney:
  usage: "§cUso: /setmoney <jogador> <valor>"
  success: "§aSaldo de §b{player}§a definido: §e{old} §a→ §e{new}"
  notify: "§aSeu saldo foi definido para §e{amount}§a por um administrador."

addmoney:
  usage: "§cUso: /addmoney <jogador> <valor>"
  success: "§aAdicionado §e{amount}§a a §b{player}§a. Novo saldo: §e{balance}"
  notify: "§aVocê recebeu §e{amount}§a de um administrador. Novo saldo: §e{balance}"

reducemoney:
  usage: "§cUso: /reducemoney <jogador> <valor>"
  insufficient: "§c{player} só tem §e{balance}§c. Não é possível reduzir §e{amount}§c."
  success: "§aRemovido §e{amount}§a de §b{player}§a. Novo saldo: §e{balance}"
  notify: "§cUm administrador removeu §e{amount}§c do seu saldo. Novo saldo: §e{balance}"

topmoney:
  empty: "§eNenhum dado econômico disponível ainda."
  header: "§6══ Top Dinheiro §7(Página {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Файл русского языка
# ──────────────────────────────────────────────

general:
  data-loading: "§eДанные '{player}' загружаются. Пожалуйста, подождите..."
  data-access-error: "§cНе удалось получить доступ к данным '{player}'."
  save-failed: "§cНе удалось сохранить данные '{player}'."
  no-economy-data: "§cУ игрока '{player}' нет экономических данных."
  player-not-online: "§cИгрок '{player}' не в сети."
  only-ingame: "§cЭта команда доступна только в игре."
  amount-not-number: "§cСумма должна быть числом."
  amount-positive: "§cСумма должна быть больше нуля."
  amount-not-negative: "§cСумма не может быть отрицательной."
  transaction-cancelled: "§cТранзакция отменена."

money:
  usage: "§cИспользование: /money <игрок>"
  self: "§aВаш баланс: §e{balance} §7(#{rank})"
  self-unranked: "§aВаш баланс: §e{balance}"
  other: "§aБаланс {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aБаланс {player}: §e{balance}"
  error: "§cНе удалось получить данные '{player}'."

pay:
  usage: "§cИспользование: /pay <игрок> <сумма>"
  no-self: "§cВы не можете перевести деньги себе."
  loading-self: "§eВаши данные загружаются. Пожалуйста, подождите."
  loading-target: "§eДанные {player} загружаются. Пожалуйста, подождите..."
  insufficient: "§cНедостаточно средств. Ваш баланс: §e{balance}"
  cancelled: "§cПлатёж отменён."
  sent: "§aВы отправили §e{amount}§a игроку §b{player}§a."
  received: "§aВы получили §e{amount}§a от §b{player}§a."

setmoney:
  usage: "§cИспользование: /setmoney <игрок> <сумма>"
  success: "§aБаланс §b{player}§a установлен: §e{old} §a→ §e{new}"
  notify: "§aВаш баланс установлен на §e{amount}§a администратором."

addmoney:
  usage: "§cИспользование: /addmoney <игрок> <сумма>"
  success: "§aДобавлено §e{amount}§a игроку §b{player}§a. Новый баланс: §e{balance}"
  notify: "§aВы получили §e{amount}§a от администратора. Новый баланс: §e{balance}"

reducemoney:
  usage: "§cИспользование: /reducemoney <игрок> <сумма>"
  insufficient: "§cУ {player} только §e{balance}§c. Невозможно списать §e{amount}§c."
  success: "§aСписано §e{amount}§a у §b{player}§a. Новый баланс: §e{balance}"
  notify: "§cАдминистратор списал §e{amount}§c с вашего баланса. Новый баланс: §e{balance}"

topmoney:
  empty: "§eЭкономические данные пока отсутствуют."
  header: "§6══ Топ баланса §7(Стр. {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Archivo de idioma Español
# ──────────────────────────────────────────────

general:
  data-loading: "§eLos datos de '{player}' se están cargando. Por favor espera..."
  data-access-error: "§cNo se pudo acceder a los datos de '{player}'."
  save-failed: "§cError al guardar los datos de '{player}'."
  no-economy-data: "§cEl jugador '{player}' no tiene datos económicos."
  player-not-online: "§cEl jugador '{player}' no está en línea."
  only-ingame: "§cEste comando solo se puede usar en el juego."
  amount-not-number: "§cLa cantidad debe ser un número."
  amount-positive: "§cLa cantidad debe ser mayor que cero."
  amount-not-negative: "§cLa cantidad no puede ser negativa."
  transaction-cancelled: "§cTransacción cancelada."

money:
  usage: "§cUso: /money <jugador>"
  self: "§aTu saldo: §e{balance} §7(#{rank})"
  self-unranked: "§aTu saldo: §e{balance}"
  other: "§aSaldo de {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aSaldo de {player}: §e{balance}"
  error: "§cNo se pudieron obtener los datos de '{player}'."

pay:
  usage: "§cUso: /pay <jugador> <cantidad>"
  no-self: "§cNo puedes pagarte a ti mismo."
  loading-self: "§eTus datos se están cargando. Por favor espera."
  loading-target: "§eLos datos de {player} se están cargando. Por favor espera..."
  insufficient: "§cFondos insuficientes. Tu saldo: §e{balance}"
  cancelled: "§cPago cancelado."
  sent: "§aEnviaste §e{amount}§a a §b{player}§a."
  received: "§aRecibiste §e{amount}§a de §b{player}§a."

setmoney:
  usage: "§cUso: /setmoney <jugador> <cantidad>"
  success: "§aSaldo de §b{player}§a establecido: §e{old} §a→ §e{new}"
  notify: "§aTu saldo ha sido establecido en §e{amount}§a por un administrador."

addmoney:
  usage: "§cUso: /addmoney <jugador> <cantidad>"
  success: "§aAñadido §e{amount}§a a §b{player}§a. Nuevo saldo: §e{balance}"
  notify: "§aRecibiste §e{amount}§a de un administrador. Nuevo saldo: §e{balance}"

reducemoney:
  usage: "§cUso: /reducemoney <jugador> <cantidad>"
  insufficient: "§c{player} solo tiene §e{balance}§c. No se puede reducir §e{amount}§c."
  success: "§aRetirado §e{amount}§a de §b{player}§a. Nuevo saldo: §e{balance}"
  notify: "§cUn administrador retiró §e{amount}§c de tu saldo. Nuevo saldo: §e{balance}"

topmoney:
  empty: "§eAún no hay datos económicos."
  header: "§6══ Top Dinero §7(Página {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Türkçe Dil Dosyası
# ──────────────────────────────────────────────

general:
  data-loading: "§e'{player}' verisi yükleniyor. Lütfen bekleyin..."
  data-access-error: "§c'{player}' verisine erişilemedi."
  save-failed: "§c'{player}' verisi kaydedilemedi."
  no-economy-data: "§c'{player}' oyuncusunun ekonomi verisi yok."
  player-not-online: "§c'{player}' oyuncusu çevrimiçi değil."
  only-ingame: "§cBu komut yalnızca oyun içinde kullanılabilir."
  amount-not-number: "§cMiktar bir sayı olmalıdır."
  amount-positive: "§cMiktar sıfırdan büyük olmalıdır."
  amount-not-negative: "§cMiktar negatif olamaz."
  transaction-cancelled: "§cİşlem iptal edildi."

money:
  usage: "§cKullanım: /money <oyuncu>"
  self: "§aBakiyeniz: §e{balance} §7(#{rank})"
  self-unranked: "§aBakiyeniz: §e{balance}"
  other: "§a{player} bakiyesi: §e{balance} §7(#{rank})"
  other-unranked: "§a{player} bakiyesi: §e{balance}"
  error: "§c'{player}' verisi alınamadı."

pay:
  usage: "§cKullanım: /pay <oyuncu> <miktar>"
  no-self: "§cKendinize ödeme yapamazsınız."
  loading-self: "§eVeriniz yükleniyor. Lütfen bekleyin."
  loading-target: "§e{player} verisi yükleniyor. Lütfen bekleyin..."
  insufficient: "§cYetersiz bakiye. Bakiyeniz: §e{balance}"
  cancelled: "§cÖdeme iptal edildi."
  sent: "§a§b{player}§a oyuncusuna §e{amount}§a gönderdiniz."
  received: "§a§b{player}§a oyuncusundan §e{amount}§a aldınız."

setmoney:
  usage: "§cKullanım: /setmoney <oyuncu> <miktar>"
  success: "§a§b{player}§a bakiyesi ayarlandı: §e{old} §a→ §e{new}"
  notify: "§aBakiyeniz yönetici tarafından §e{amount}§a olarak ayarlandı."

addmoney:
  usage: "§cKullanım: /addmoney <oyuncu> <miktar>"
  success: "§a§b{player}§a oyuncusuna §e{amount}§a eklendi. Yeni bakiye: §e{balance}"
  notify: "§aYöneticiden §e{amount}§a aldınız. Yeni bakiye: §e{balance}"

reducemoney:
  usage: "§cKullanım: /reducemoney <oyuncu> <miktar>"
  insufficient: "§c{player} sadece §e{balance}§c sahip. §e{amount}§c düşülemez."
  success: "§a§b{player}§a oyuncusundan §e{amount}§a düşüldü. Yeni bakiye: §e{balance}"
  notify: "§cYönetici bakiyenizden §e{amount}§c düştü. Yeni bakiye: §e{balance}"

topmoney:
  empty: "§eHenüz ekonomi verisi yok."
  header: "§6══ Para Sıralaması §7(Sayfa {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Файл української мови
# ──────────────────────────────────────────────

general:
  data-loading: "§eДані '{player}' завантажуються. Будь ласка, зачекайте..."
  data-access-error: "§cНе вдалося отримати доступ до даних '{player}'."
  save-failed: "§cНе вдалося зберегти дані '{player}'."
  no-economy-data: "§cГравець '{player}' не має економічних даних."
  player-not-online: "§cГравець '{player}' не в мережі."
  only-ingame: "§cЦя команда доступна лише у грі."
  amount-not-number: "§cСума повинна бути числом."
  amount-positive: "§cСума повинна бути більше нуля."
  amount-not-negative: "§cСума не може бути від'ємною."
  transaction-cancelled: "§cТранзакцію скасовано."

money:
  usage: "§cВикористання: /money <гравець>"
  self: "§aВаш баланс: §e{balance} §7(#{rank})"
  self-unranked: "§aВаш баланс: §e{balance}"
  other: "§aБаланс {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aБаланс {player}: §e{balance}"
  error: "§cНе вдалося отримати дані '{player}'."

pay:
  usage: "§cВикористання: /pay <гравець> <сума>"
  no-self: "§cВи не можете переказати гроші собі."
  loading-self: "§eВаші дані завантажуються. Будь ласка, зачекайте."
  loading-target: "§eДані {player} завантажуються. Будь ласка, зачекайте..."
  insufficient: "§cНедостатньо коштів. Ваш баланс: §e{balance}"
  cancelled: "§cПлатіж скасовано."
  sent: "§aВи відправили §e{amount}§a гравцю §b{player}§a."
  received: "§aВи отримали §e{amount}§a від §b{player}§a."

setmoney:
  usage: "§cВикористання: /setmoney <гравець> <сума>"
  success: "§aБаланс §b{player}§a встановлено: §e{old} §a→ §e{new}"
  notify: "§aВаш баланс встановлено на §e{amount}§a адміністратором."

addmoney:
  usage: "§cВикористання: /addmoney <гравець> <сума>"
  success: "§aДодано §e{amount}§a гравцю §b{player}§a. Новий баланс: §e{balance}"
  notify: "§aВи отримали §e{amount}§a від адміністратора. Новий баланс: §e{balance}"

reducemoney:
  usage: "§cВикористання: /reducemoney <гравець> <сума>"
  insufficient: "§cУ {player} лише §e{balance}§c. Неможливо зняти §e{amount}§c."
  success: "§aЗнято §e{amount}§a у §b{player}§a. Новий баланс: §e{balance}"
  notify: "§cАдміністратор зняв §e{amount}§c з вашого балансу. Новий баланс: §e{balance}"

topmoney:
  empty: "§eЕкономічних даних поки немає."
  header: "§6══ Топ балансу §7(Стор. {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - Tệp ngôn ngữ Tiếng Việt
# ──────────────────────────────────────────────

general:
  data-loading: "§eDữ liệu của '{player}' đang tải. Vui lòng chờ..."
  data-access-error: "§cKhông thể truy cập dữ liệu của '{player}'."
  save-failed: "§cLưu dữ liệu thất bại cho '{player}'."
  no-economy-data: "§cNgười chơi '{player}' không có dữ liệu kinh tế."
  player-not-online: "§cNgười chơi '{player}' không trực tuyến."
  only-ingame: "§cLệnh này chỉ dùng được trong game."
  amount-not-number: "§cSố tiền phải là một con số."
  amount-positive: "§cSố tiền phải lớn hơn 0."
  amount-not-negative: "§cSố tiền không được âm."
  transaction-cancelled: "§cGiao dịch đã bị hủy."

money:
  usage: "§cCách dùng: /money <tên người chơi>"
  self: "§aSố dư của bạn: §e{balance} §7(#{rank})"
  self-unranked: "§aSố dư của bạn: §e{balance}"
  other: "§aSố dư của {player}: §e{balance} §7(#{rank})"
  other-unranked: "§aSố dư của {player}: §e{balance}"
  error: "§cKhông thể lấy dữ liệu của '{player}'."

pay:
  usage: "§cCách dùng: /pay <tên người chơi> <số tiền>"
  no-self: "§cBạn không thể chuyển tiền cho chính mình."
  loading-self: "§eDữ liệu của bạn đang tải. Vui lòng chờ..."
  loading-target: "§eDữ liệu của {player} đang tải. Vui lòng chờ..."
  insufficient: "§cKhông đủ tiền. Số dư: §e{balance}"
  cancelled: "§cGiao dịch đã bị hủy."
  sent: "§aBạn đã gửi §e{amount}§a cho §b{player}§a."
  received: "§aBạn nhận được §e{amount}§a từ §b{player}§a."

setmoney:
  usage: "§cCách dùng: /setmoney <tên người chơi> <số tiền>"
  success: "§aĐặt số dư §b{player}§a: §e{old} §a→ §e{new}"
  notify: "§aSố dư của bạn đã được đặt thành §e{amount}§a bởi quản trị viên."

addmoney:
  usage: "§cCách dùng: /addmoney <tên người chơi> <số tiền>"
  success: "§aĐã cộng §e{amount}§a cho §b{player}§a. Số dư mới: §e{balance}"
  notify: "§aBạn nhận được §e{amount}§a từ quản trị viên. Số dư mới: §e{balance}"

reducemoney:
  usage: "§cCách dùng: /reducemoney <tên người chơi> <số tiền>"
  insufficient: "§c{player} chỉ có §e{balance}§c. Không thể trừ §e{amount}§c."
  success: "§aĐã trừ §e{amount}§a từ §b{player}§a. Số dư mới: §e{balance}"
  notify: "§c§e{amount}§c đã bị trừ khỏi số dư bởi quản trị viên. Số dư mới: §e{balance}"

topmoney:
  empty: "§eChưa có dữ liệu kinh tế nào."
  header: "§6══ BXH Tiền §7(Trang {page}/{max}) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
# ──────────────────────────────────────────────
#  SimpleEconomy - 简体中文语言文件
# ──────────────────────────────────────────────

general:
  data-loading: "§e'{player}' 的数据正在加载中，请稍候..."
  data-access-error: "§c无法访问 '{player}' 的数据。"
  save-failed: "§c保存 '{player}' 的数据失败。"
  no-economy-data: "§c玩家 '{player}' 没有经济数据。"
  player-not-online: "§c玩家 '{player}' 不在线。"
  only-ingame: "§c此命令只能在游戏中使用。"
  amount-not-number: "§c金额必须是数字。"
  amount-positive: "§c金额必须大于零。"
  amount-not-negative: "§c金额不能为负数。"
  transaction-cancelled: "§c交易已取消。"

money:
  usage: "§c用法: /money <玩家>"
  self: "§a你的余额: §e{balance} §7(#{rank})"
  self-unranked: "§a你的余额: §e{balance}"
  other: "§a{player} 的余额: §e{balance} §7(#{rank})"
  other-unranked: "§a{player} 的余额: §e{balance}"
  error: "§c无法获取 '{player}' 的数据。"

pay:
  usage: "§c用法: /pay <玩家> <金额>"
  no-self: "§c你不能给自己转账。"
  loading-self: "§e你的数据正在加载中，请稍候。"
  loading-target: "§e{player} 的数据正在加载中，请稍候..."
  insufficient: "§c余额不足。你的余额: §e{balance}"
  cancelled: "§c付款已取消。"
  sent: "§a你向 §b{player}§a 发送了 §e{amount}§a。"
  received: "§a你收到了来自 §b{player}§a 的 §e{amount}§a。"

setmoney:
  usage: "§c用法: /setmoney <玩家> <金额>"
  success: "§a已设置 §b{player}§a 的余额: §e{old} §a→ §e{new}"
  notify: "§a管理员已将你的余额设置为 §e{amount}§a。"

addmoney:
  usage: "§c用法: /addmoney <玩家> <金额>"
  success: "§a已向 §b{player}§a 添加 §e{amount}§a。新余额: §e{balance}"
  notify: "§a你收到了管理员发送的 §e{amount}§a。新余额: §e{balance}"

reducemoney:
  usage: "§c用法: /reducemoney <玩家> <金额>"
  insufficient: "§c{player} 只有 §e{balance}§c，无法扣除 §e{amount}§c。"
  success: "§a已从 §b{player}§a 扣除 §e{amount}§a。新余额: §e{balance}"
  notify: "§c管理员从你的余额中扣除了 §e{amount}§c。新余额: §e{balance}"

topmoney:
  empty: "§e暂无经济数据。"
  header: "§6══ 财富排行榜 §7(第 {page}/{max} 页) §6══"
  entry: "§e#{rank} {highlight}{player} §7- §a{balance}"
  footer: "§6══════════════════════"
-- #!mysql
-- #{ simplesql
    -- #{ init
        CREATE TABLE IF NOT EXISTS simplesql_data (
            id VARCHAR(128) NOT NULL PRIMARY KEY,
            data LONGTEXT NOT NULL,
            revision INT UNSIGNED NOT NULL DEFAULT 0
        ) ENGINE=InnoDB;
    -- #}
    -- #{ load
        -- #    :id string
        SELECT data, revision FROM simplesql_data WHERE id = :id;
    -- #}
    -- #{ save
        -- #    :id string
        -- #    :data string
        -- #    :revision int
        INSERT INTO simplesql_data (id, data, revision) VALUES (:id, :data, :revision)
        ON DUPLICATE KEY UPDATE data = VALUES(data), revision = VALUES(revision);
        -- #&
        SELECT id, data, revision FROM simplesql_data WHERE id = :id;
    -- #}
    -- #{ pair_engine
        SELECT ENGINE AS engine FROM information_schema.TABLES
        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'simplesql_data';
    -- #}
    -- #{ save_pair
        -- #    :first_id string
        -- #    :first_data string
        -- #    :first_revision int
        -- #    :second_id string
        -- #    :second_data string
        -- #    :second_revision int
        INSERT INTO simplesql_data (id, data, revision) VALUES
            (:first_id, :first_data, :first_revision),
            (:second_id, :second_data, :second_revision)
        ON DUPLICATE KEY UPDATE data = VALUES(data), revision = VALUES(revision);
        -- #&
        SELECT id, data, revision FROM simplesql_data WHERE id IN (:first_id, :second_id);
    -- #}
    -- #{ delete
        -- #    :id string
        DELETE FROM simplesql_data WHERE id = :id;
    -- #}
-- #}
-- #!sqlite
-- #{ simplesql
    -- #{ init
        CREATE TABLE IF NOT EXISTS simplesql_data (
            id TEXT NOT NULL PRIMARY KEY,
            data TEXT NOT NULL,
            revision INTEGER NOT NULL DEFAULT 0
        );
    -- #}
    -- #{ load
        -- #    :id string
        SELECT data, revision FROM simplesql_data WHERE id = :id;
    -- #}
    -- #{ save
        -- #    :id string
        -- #    :data string
        -- #    :revision int
        INSERT INTO simplesql_data (id, data, revision) VALUES (:id, :data, :revision)
        ON CONFLICT(id) DO UPDATE SET data = excluded.data, revision = excluded.revision;
        -- #&
        SELECT id, data, revision FROM simplesql_data WHERE id = :id;
    -- #}
    -- #{ pair_engine
        SELECT 'sqlite' AS engine FROM sqlite_master WHERE type = 'table' AND name = 'simplesql_data';
    -- #}
    -- #{ save_pair
        -- #    :first_id string
        -- #    :first_data string
        -- #    :first_revision int
        -- #    :second_id string
        -- #    :second_data string
        -- #    :second_revision int
        INSERT INTO simplesql_data (id, data, revision) VALUES
            (:first_id, :first_data, :first_revision),
            (:second_id, :second_data, :second_revision)
        ON CONFLICT(id) DO UPDATE SET data = excluded.data, revision = excluded.revision;
        -- #&
        SELECT id, data, revision FROM simplesql_data WHERE id IN (:first_id, :second_id);
    -- #}
    -- #{ delete
        -- #    :id string
        DELETE FROM simplesql_data WHERE id = :id;
    -- #}
-- #}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

final class BalanceAmount {

	public static function parseCommand(string $raw): ?int {
		if (preg_match('/\A\+?([0-9]+)(?:\.[0-9]+)?\z/', trim($raw), $matches) !== 1) {
			return null;
		}
		$digits = ltrim($matches[1], '0');
		$value = filter_var($digits === '' ? '0' : $digits, FILTER_VALIDATE_INT);
		return is_int($value) && $value >= 0 ? $value : null;
	}

	public static function add(int $balance, int $amount): ?int {
		if ($balance < 0 || $amount < 0 || $amount > PHP_INT_MAX - $balance) {
			return null;
		}
		return $balance + $amount;
	}

	public static function reduce(int $balance, int $amount): ?int {
		if ($balance < 0 || $amount < 0 || $amount > $balance) {
			return null;
		}
		return $balance - $amount;
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

use NhanAZ\SimpleSQL\Session;

final class BalanceReader {

	public static function read(Session $session): int {
		$value = $session->get("balance", 0);
		if (is_int($value) && $value >= 0) {
			return $value;
		}
		if (is_string($value) && preg_match('/^[0-9]+$/D', $value) === 1) {
			$digits = ltrim($value, "0");
			$parsed = filter_var($digits === "" ? "0" : $digits, FILTER_VALIDATE_INT);
			if (is_int($parsed) && $parsed >= 0) {
				return $parsed;
			}
		}
		throw new InvalidBalanceException("Invalid balance data for session '" . $session->getId() . "'.");
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

/**
 * Formats monetary amounts with configurable styles.
 *
 * Supported formatters:
 *   - "default":  $1,000,000
 *   - "compact":  $1.5K, $2.3M, $1B, $4.2T
 */
class CurrencyFormatter {

	public const DEFAULT = "default";
	public const COMPACT = "compact";

	public function __construct(
		private readonly string $symbol,
		private readonly string $mode,
	) {}

	/**
	 * Format an amount using the configured style.
	 */
	public function format(int|float $amount): string {
		return match ($this->mode) {
			self::COMPACT => $this->compact((float) $amount),
			default => $this->commadot((float) $amount),
		};
	}

	/**
	 * Default format: $1,000,000
	 */
	private function commadot(float $amount): string {
		return $this->symbol . number_format($amount, 0, ".", ",");
	}

	/**
	 * Compact format: $1.5K, $2.3M, $1B, $4.2T
	 */
	private function compact(float $amount): string {
		$str = match (true) {
			$amount >= 1_000_000_000_000 => round($amount / 1_000_000_000_000, 1) . "T",
			$amount >= 1_000_000_000 => round($amount / 1_000_000_000, 1) . "B",
			$amount >= 1_000_000 => round($amount / 1_000_000, 1) . "M",
			$amount >= 1_000 => round($amount / 1_000, 1) . "K",
			default => number_format($amount, 0, ".", ","),
		};
		return $this->symbol . $str;
	}

	public function getSymbol(): string {
		return $this->symbol;
	}

	public function getMode(): string {
		return $this->mode;
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

use UnexpectedValueException;

final class InvalidBalanceException extends UnexpectedValueException {
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

use pocketmine\plugin\PluginBase;

/**
 * Simple multi-language manager.
 *
 * Loads messages from YAML files in resources/lang/.
 * Supports placeholder replacement via {key} syntax.
 */
class LangManager {

	/** @var array<string, string> */
	private array $messages = [];

	private string $language;

	public function __construct(PluginBase $plugin, string $language) {
		$this->language = $language;

		// Save all language files to data folder
		foreach (["eng", "vie", "kor", "rus", "spa", "ukr", "zho", "ind", "tur", "fra", "por", "deu", "jpn", "ita"] as $lang) {
			$plugin->saveResource("lang/$lang.yml", false);
		}

		// Load selected language
		$langFile = $plugin->getDataFolder() . "lang/$language.yml";
		if (!file_exists($langFile)) {
			$plugin->getLogger()->warning("Language file '$language.yml' not found, falling back to 'eng'.");
			$langFile = $plugin->getDataFolder() . "lang/eng.yml";
			$this->language = "eng";
		}

		$data = @yaml_parse_file($langFile);
		if (is_array($data)) {
			$this->messages = $this->flatten($data);
		}
	}

	/**
	 * Get a translated message with placeholder replacement.
	 *
	 * @param array<string, string> $params Placeholders like ["player" => "Steve", "amount" => "$100"]
	 */
	public function get(string $key, array $params = []): string {
		$msg = $this->messages[$key] ?? $key;
		foreach ($params as $k => $v) {
			$msg = str_replace("{" . $k . "}", (string) $v, $msg);
		}
		return $msg;
	}

	public function getLanguage(): string {
		return $this->language;
	}

	/**
	 * Flatten a nested array into dot-notation keys.
	 *
	 * @param array<array-key, mixed> $data
	 * @return array<string, string>
	 */
	private function flatten(array $data, string $prefix = ""): array {
		$result = [];
		foreach ($data as $key => $value) {
			$fullKey = $prefix === "" ? (string) $key : $prefix . "." . $key;
			if (is_array($value)) {
				$result = array_merge($result, $this->flatten($value, $fullKey));
			} elseif (is_scalar($value)) {
				$result[$fullKey] = (string) $value;
			}
		}
		return $result;
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

use pocketmine\scheduler\AsyncTask;

/**
 * Asynchronous task that scans YAML mirror files to rebuild the leaderboard cache.
 *
 * Runs on an async worker thread to avoid blocking the main thread.
 * Only the top N entries (by balance) are kept to bound memory usage.
 */
class LeaderboardTask extends AsyncTask {

	public function __construct(
		private readonly string $yamlPath,
		private readonly int $limit,
	) {}

	public function onRun(): void {
		if (!is_dir($this->yamlPath)) {
			$this->setResult([]);
			return;
		}

		$entries = [];
		$iterator = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator($this->yamlPath, \FilesystemIterator::SKIP_DOTS)
		);

		foreach ($iterator as $file) {
			/** @var \SplFileInfo $file */
			if ($file->getExtension() !== "yml") {
				continue;
			}

			$content = @file_get_contents($file->getPathname());
			if ($content === false) {
				continue;
			}

			$data = @yaml_parse($content);
			if (!is_array($data) || !isset($data["data"]) || !is_array($data["data"])) {
				continue;
			}
			$balance = $data["data"]["balance"] ?? null;
			if (!is_int($balance) || $balance < 0) {
				continue;
			}

			$name = strtolower(pathinfo($file->getFilename(), PATHINFO_FILENAME));
			$entries[$name] = $balance;

			// Keep array bounded: if over 2x limit, trim to limit
			if (count($entries) > $this->limit * 2) {
				arsort($entries);
				$entries = array_slice($entries, 0, $this->limit, true);
			}
		}

		// Final sort + trim
		arsort($entries);
		$entries = array_slice($entries, 0, $this->limit, true);

		$this->setResult($entries);
	}

	public function onCompletion(): void {
		$plugin = Main::getInstance();
		if ($plugin === null) {
			return;
		}

		$result = $this->getResult();
		if (!is_array($result)) {
			return;
		}

		foreach ($result as $name => $balance) {
			if (is_int($balance) && $balance >= 0) {
				$plugin->updateBalanceCache((string) $name, $balance);
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

use Closure;
use InvalidArgumentException;
use NhanAZ\SimpleEconomy\command\AddMoneyCommand;
use NhanAZ\SimpleEconomy\command\MoneyCommand;
use NhanAZ\SimpleEconomy\command\PayCommand;
use NhanAZ\SimpleEconomy\command\ReduceMoneyCommand;
use NhanAZ\SimpleEconomy\command\SetMoneyCommand;
use NhanAZ\SimpleEconomy\command\TopMoneyCommand;
use NhanAZ\SimpleEconomy\event\TransactionEvent;
use NhanAZ\SimpleEconomy\event\TransactionSubmitEvent;
use NhanAZ\SimpleEconomy\event\TransactionSuccessEvent;
use NhanAZ\SimpleSQL\Session;
use NhanAZ\SimpleSQL\SimpleSQL;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\event\player\PlayerQuitEvent;
use pocketmine\player\Player;
use pocketmine\plugin\PluginBase;

/**
 * SimpleEconomy plugin powered by SimpleSQL.
 *
 * Features:
 *   - Simple API for other plugins (getMoney, setMoney, addMoney, reduceMoney)
 *   - Async API for offline player data (getMoneyAsync)
 *   - Transaction events for third-party plugin integration
 *   - Leaderboard with async cache rebuild (S3 compliant)
 *   - Configurable currency formatter (default / compact)
 *   - Name prefix matching for commands
 *   - Offline player support via temporary sessions
 *   - Multi-language support (eng / vie)
 */
class Main extends PluginBase implements Listener {

	private const CONFIG_VERSION = 1;

	private static ?self $instance = null;

	private SimpleSQL $simpleSQL;
	private LangManager $lang;
	private CurrencyFormatter $formatter;
	private int $defaultBalance;
	private int $topmoneyPerPage;
	private int $leaderboardSize;

	/** @var array<string, int> lowercased name => balance, sorted desc, bounded by leaderboardSize */
	private array $balanceCache = [];
	/** @var array<string, true> */
	private array $pendingCommandSaves = [];

	// ──────────────────────────────────────────────
	//  Static accessor
	// ──────────────────────────────────────────────

	/**
	 * Get the SimpleEconomy plugin instance.
	 *
	 * Usage from another plugin:
	 *   $eco = SimpleEconomy::getInstance();
	 *   $balance = $eco?->getMoney("Steve");
	 */
	public static function getInstance(): ?self {
		return self::$instance;
	}

	// ──────────────────────────────────────────────
	//  Plugin lifecycle
	// ──────────────────────────────────────────────

	protected function onEnable(): void {
		$this->saveDefaultConfig();

		// Config version check
		$configVersion = self::configInt($this->getConfig()->get("config-version", 0), "config-version", 0);
		if ($configVersion < self::CONFIG_VERSION) {
			$this->getLogger()->warning("Your config.yml is outdated (v$configVersion, latest: v" . self::CONFIG_VERSION . "). Please regenerate it.");
		}

		$this->defaultBalance = self::configInt($this->getConfig()->get("default-balance", 1000), "default-balance", 0);
		$this->topmoneyPerPage = self::configInt($this->getConfig()->get("topmoney-per-page", 10), "topmoney-per-page", 1);
		$this->leaderboardSize = self::configInt($this->getConfig()->get("leaderboard-size", 100), "leaderboard-size", 1);

		// Currency
		$currencyConfig = $this->getConfig()->get("currency", []);
		if (!is_array($currencyConfig)) {
			throw new InvalidArgumentException("Config 'currency' must be a mapping.");
		}
		$symbol = self::configString($currencyConfig["symbol"] ?? "$", "currency.symbol");
		$formatterMode = self::configString($currencyConfig["formatter"] ?? CurrencyFormatter::DEFAULT, "currency.formatter");
		if ($formatterMode !== CurrencyFormatter::DEFAULT && $formatterMode !== CurrencyFormatter::COMPACT) {
			throw new InvalidArgumentException("Config 'currency.formatter' must be 'default' or 'compact'.");
		}
		$this->formatter = new CurrencyFormatter($symbol, $formatterMode);

		// Language
		$language = self::configString($this->getConfig()->get("language", "eng"), "language");
		if (!in_array($language, ["eng", "vie", "kor", "rus", "spa", "ukr", "zho", "ind", "tur", "fra", "por", "deu", "jpn", "ita"], true)) {
			throw new InvalidArgumentException("Config 'language' must name a bundled language.");
		}
		$this->lang = new LangManager($this, $language);

		// SimpleSQL
		$databaseConfig = $this->getConfig()->get("database");
		if (!is_array($databaseConfig)) {
			throw new InvalidArgumentException("Config 'database' must be a mapping.");
		}
		$dbConfig = [];
		foreach ($databaseConfig as $key => $value) {
			if (!is_string($key)) {
				throw new InvalidArgumentException("Config 'database' must use string keys.");
			}
			$dbConfig[$key] = $value;
		}
		$this->simpleSQL = SimpleSQL::create(
			plugin: $this,
			dbConfig: $dbConfig,
		);
		self::$instance = $this;

		// Rebuild leaderboard cache asynchronously (S3 compliant)
		$this->getServer()->getAsyncPool()->submitTask(
			new LeaderboardTask($this->simpleSQL->getYamlDataPath(), $this->leaderboardSize)
		);

		// Events
		$this->getServer()->getPluginManager()->registerEvents($this, $this);

		// ScoreHud integration (softdepend - only if ScoreHud is installed)
		if ($this->getServer()->getPluginManager()->getPlugin("ScoreHud") !== null) {
			$this->getServer()->getPluginManager()->registerEvents(new ScoreHudListener($this), $this);
		}

		// Commands - fallback prefix = plugin name (C2a)
		$map = $this->getServer()->getCommandMap();
		$map->register($this->getName(), new MoneyCommand($this));
		$map->register($this->getName(), new PayCommand($this));
		$map->register($this->getName(), new SetMoneyCommand($this));
		$map->register($this->getName(), new AddMoneyCommand($this));
		$map->register($this->getName(), new ReduceMoneyCommand($this));
		$map->register($this->getName(), new TopMoneyCommand($this));
	}

	protected function onDisable(): void {
		self::$instance = null;
		if (isset($this->simpleSQL)) {
			$this->simpleSQL->close();
		}
	}

	private static function configInt(mixed $value, string $key, int $minimum): int {
		if (!is_int($value) || $value < $minimum) {
			throw new InvalidArgumentException("Config '$key' must be an integer of at least $minimum.");
		}
		return $value;
	}

	private static function configString(mixed $value, string $key): string {
		if (!is_string($value)) {
			throw new InvalidArgumentException("Config '$key' must be a string.");
		}
		return $value;
	}

	// ──────────────────────────────────────────────
	//  Event handlers
	// ──────────────────────────────────────────────

	public function onJoin(PlayerJoinEvent $event): void {
		$player = $event->getPlayer();
		$name = strtolower($player->getName());

		$this->simpleSQL->openSession($name, function (Session $session) use ($name): void {
			if (!$session->has("balance")) {
				$session->set("balance", $this->defaultBalance);
				$session->save();
			}

			try {
				$this->updateBalanceCache($name, BalanceReader::read($session));
			} catch (InvalidBalanceException $e) {
				$this->getLogger()->error($e->getMessage());
				$this->simpleSQL->closeSession($name);
			}
		});
	}

	public function onQuit(PlayerQuitEvent $event): void {
		$name = strtolower($event->getPlayer()->getName());
		$this->simpleSQL->closeSession($name);
	}

	// ──────────────────────────────────────────────
	//  Public API - Synchronous (online players only)
	// ──────────────────────────────────────────────

	/**
	 * Get a player's balance.
	 * Returns null if the player is offline or session is not loaded.
	 */
	public function getMoney(string $name): ?int {
		$session = $this->simpleSQL->getSession(strtolower($name));
		if ($session === null) {
			return null;
		}
		return BalanceReader::read($session);
	}

	/**
	 * Set a player's balance.
	 * Returns false if the player is offline or the transaction was cancelled by another plugin.
	 */
	public function setMoney(string $name, int $amount): bool {
		if ($amount < 0) {
			return false;
		}
		$lower = strtolower($name);
		if (isset($this->pendingCommandSaves[$lower])) {
			return false;
		}
		$session = $this->simpleSQL->getSession($lower);
		if ($session === null) {
			return false;
		}

		$oldBalance = BalanceReader::read($session);

		// Fire pre-transaction event
		$submitEvent = new TransactionSubmitEvent($name, $oldBalance, $amount, TransactionEvent::TYPE_SET);
		$submitEvent->call();
		if ($submitEvent->isCancelled()) {
			return false;
		}

		$session->set("balance", $amount);
		$session->save();
		$this->updateBalanceCache($lower, $amount);

		// Fire post-transaction event
		(new TransactionSuccessEvent($name, $oldBalance, $amount, TransactionEvent::TYPE_SET))->call();

		return true;
	}

	/**
	 * Add money to a player's balance.
	 * Returns false if the player is offline or the transaction was cancelled.
	 */
	public function addMoney(string $name, int $amount): bool {
		if ($amount < 0) {
			return false;
		}
		$lower = strtolower($name);
		if (isset($this->pendingCommandSaves[$lower])) {
			return false;
		}
		$session = $this->simpleSQL->getSession($lower);
		if ($session === null) {
			return false;
		}

		$oldBalance = BalanceReader::read($session);
		$newBalance = BalanceAmount::add($oldBalance, $amount);
		if ($newBalance === null) {
			return false;
		}

		$submitEvent = new TransactionSubmitEvent($name, $oldBalance, $newBalance, TransactionEvent::TYPE_ADD);
		$submitEvent->call();
		if ($submitEvent->isCancelled()) {
			return false;
		}

		$session->set("balance", $newBalance);
		$session->save();
		$this->updateBalanceCache($lower, $newBalance);

		(new TransactionSuccessEvent($name, $oldBalance, $newBalance, TransactionEvent::TYPE_ADD))->call();

		return true;
	}

	/**
	 * Reduce money from a player's balance.
	 * Returns false if the player is offline, has insufficient funds, or the transaction was cancelled.
	 */
	public function reduceMoney(string $name, int $amount): bool {
		if ($amount < 0) {
			return false;
		}
		$lower = strtolower($name);
		if (isset($this->pendingCommandSaves[$lower])) {
			return false;
		}
		$session = $this->simpleSQL->getSession($lower);
		if ($session === null) {
			return false;
		}

		$oldBalance = BalanceReader::read($session);
		$newBalance = BalanceAmount::reduce($oldBalance, $amount);
		if ($newBalance === null) {
			return false;
		}

		$submitEvent = new TransactionSubmitEvent($name, $oldBalance, $newBalance, TransactionEvent::TYPE_REDUCE);
		$submitEvent->call();
		if ($submitEvent->isCancelled()) {
			return false;
		}

		$session->set("balance", $newBalance);
		$session->save();
		$this->updateBalanceCache($lower, $newBalance);

		(new TransactionSuccessEvent($name, $oldBalance, $newBalance, TransactionEvent::TYPE_REDUCE))->call();

		return true;
	}

	/**
	 * Report whether the balance was saved to SQL and confirmed by readback.
	 * The YAML mirror is queued separately after SQL succeeds.
	 * @param Closure(bool): void $onComplete
	 */
	public function setMoneyConfirmed(string $name, int $amount, Closure $onComplete): void {
		$this->changeMoneyConfirmed($name, $amount, TransactionEvent::TYPE_SET, $onComplete);
	}

	/** @param Closure(bool): void $onComplete */
	public function addMoneyConfirmed(string $name, int $amount, Closure $onComplete): void {
		$this->changeMoneyConfirmed($name, $amount, TransactionEvent::TYPE_ADD, $onComplete);
	}

	/** @param Closure(bool): void $onComplete */
	public function reduceMoneyConfirmed(string $name, int $amount, Closure $onComplete): void {
		$this->changeMoneyConfirmed($name, $amount, TransactionEvent::TYPE_REDUCE, $onComplete);
	}

	/** @param Closure(bool): void $onComplete */
	private function changeMoneyConfirmed(string $name, int $amount, string $type, Closure $onComplete): void {
		$lower = strtolower($name);
		if ($amount < 0 || isset($this->pendingCommandSaves[$lower])) {
			$onComplete(false);
			return;
		}
		$session = $this->simpleSQL->getSession($lower);
		if ($session === null) {
			$onComplete(false);
			return;
		}
		$oldBalance = BalanceReader::read($session);
		$newBalance = match ($type) {
			TransactionEvent::TYPE_SET => $amount,
			TransactionEvent::TYPE_ADD => BalanceAmount::add($oldBalance, $amount),
			TransactionEvent::TYPE_REDUCE => BalanceAmount::reduce($oldBalance, $amount),
			default => throw new InvalidArgumentException('Unsupported balance change type'),
		};
		if ($newBalance === null) {
			$onComplete(false);
			return;
		}
		$submitEvent = new TransactionSubmitEvent($name, $oldBalance, $newBalance, $type);
		$submitEvent->call();
		if ($submitEvent->isCancelled()) {
			$onComplete(false);
			return;
		}
		$this->saveCommandBalance($session, $name, $oldBalance, $newBalance,
			function (bool $success) use ($name, $oldBalance, $newBalance, $type, $onComplete): void {
				if ($success) {
					(new TransactionSuccessEvent($name, $oldBalance, $newBalance, $type))->call();
				}
				$onComplete($success);
			});
	}

	// ──────────────────────────────────────────────
	//  Public API - Asynchronous (online + offline)
	// ──────────────────────────────────────────────

	/**
	 * Get a player's balance asynchronously. Works for both online and offline players.
	 *
	 * @param Closure(?int): void $callback - receives the balance, or null if the player has never played.
	 */
	public function getMoneyAsync(string $name, Closure $callback): void {
		$lower = strtolower($name);

		// Online - instant
		if ($this->simpleSQL->hasSession($lower)) {
			$session = $this->simpleSQL->getSession($lower);
			$callback($session !== null ? BalanceReader::read($session) : null);
			return;
		}

		// Currently loading - return from cache if available
		if ($this->simpleSQL->isLoading($lower)) {
			$callback($this->balanceCache[$lower] ?? null);
			return;
		}

		// Offline - open temporary session
		$this->simpleSQL->openSession($lower, function (Session $session) use ($lower, $callback): void {
			try {
				$callback($session->has("balance") ? BalanceReader::read($session) : null);
			} finally {
				$this->simpleSQL->closeSession($lower);
			}
		});
	}

	// ──────────────────────────────────────────────
	//  Helpers for commands
	// ──────────────────────────────────────────────

	/**
	 * Resolve an online player by name prefix (case-insensitive).
	 * Returns null if no match or ambiguous.
	 */
	public function resolvePlayer(string $input): ?Player {
		return $this->getServer()->getPlayerByPrefix($input);
	}

	/**
	 * Execute a callback with a player's session.
	 *
	 * - For online players: uses the existing session, $temporary = false.
	 * - For offline players: opens a temp session, $temporary = true.
	 *   Caller must call closeTempSession() when done with a temporary session.
	 *
	 * @param Closure(Session, bool $temporary): void $onSession
	 * @param Closure(string $errorMessage): void $onError
	 */
	public function withPlayerSession(string $name, Closure $onSession, Closure $onError): void {
		$lower = strtolower($name);
		if (isset($this->pendingCommandSaves[$lower])) {
			$onError($this->lang->get("general.data-loading", ["player" => $name]));
			return;
		}

		// Already loaded (online player)
		if ($this->simpleSQL->hasSession($lower)) {
			$session = $this->simpleSQL->getSession($lower);
			if ($session !== null) {
				try {
					$onSession($session, false);
				} catch (InvalidBalanceException $e) {
					$this->getLogger()->error($e->getMessage());
					$onError($this->lang->get("general.data-access-error", ["player" => $name]));
				}
			} else {
				$onError($this->lang->get("general.data-access-error", ["player" => $name]));
			}
			return;
		}

		// Currently loading
		if ($this->simpleSQL->isLoading($lower)) {
			$onError($this->lang->get("general.data-loading", ["player" => $name]));
			return;
		}

		// Offline - open temporary session
		$this->simpleSQL->openSession($lower, function (Session $session) use ($onSession, $onError, $lower, $name): void {
			try {
				$onSession($session, true);
			} catch (InvalidBalanceException $e) {
				$this->getLogger()->error($e->getMessage());
				$this->simpleSQL->closeSession($lower);
				$onError($this->lang->get("general.data-access-error", ["player" => $name]));
			}
		});
	}

	/**
	 * @param Closure(bool): void $onComplete
	 */
	public function saveCommandBalance(Session $session, string $name, int $oldBalance, int $newBalance, Closure $onComplete): void {
		$lower = strtolower($name);
		if (isset($this->pendingCommandSaves[$lower])) {
			$onComplete(false);
			return;
		}
		$this->pendingCommandSaves[$lower] = true;
		try {
			$session->set("balance", $newBalance);
			$mutationVersion = $session->_getMutationVersion();
			$session->save(function (bool $success) use ($session, $lower, $oldBalance, $newBalance, $mutationVersion, $onComplete): void {
				if ($success) {
					$this->updateBalanceCache($lower, $newBalance);
				} elseif (!$session->isClosed()) {
					if ($session->_getMutationVersion() === $mutationVersion) {
						$session->set("balance", $oldBalance);
					} else {
						$this->getLogger()->warning("Could not restore the balance for '{$lower}' after a failed save because the session changed again.");
					}
				}
				unset($this->pendingCommandSaves[$lower]);
				$onComplete($success);
			});
		} catch (\Throwable $error) {
			unset($this->pendingCommandSaves[$lower]);
			throw $error;
		}
	}

	/**
	 * Persist both sides of a payment before changing either visible balance.
	 * @param Closure(bool): void $onComplete
	 */
	public function saveTransfer(
		Session $senderSession,
		string $senderName,
		int $senderBalance,
		Session $targetSession,
		string $targetName,
		int $targetBalance,
		int $amount,
		Closure $onComplete,
	): void {
		$senderId = strtolower($senderName);
		$targetId = strtolower($targetName);
		if ($senderId === $targetId || $amount <= 0 ||
			isset($this->pendingCommandSaves[$senderId]) || isset($this->pendingCommandSaves[$targetId]) ||
			$senderSession->getId() !== $senderId || $targetSession->getId() !== $targetId ||
			BalanceReader::read($senderSession) !== $senderBalance || BalanceReader::read($targetSession) !== $targetBalance) {
			$onComplete(false);
			return;
		}
		$newSenderBalance = BalanceAmount::reduce($senderBalance, $amount);
		$newTargetBalance = BalanceAmount::add($targetBalance, $amount);
		if ($newSenderBalance === null || $newTargetBalance === null) {
			$onComplete(false);
			return;
		}
		$senderData = $senderSession->getAll();
		$targetData = $targetSession->getAll();
		$senderData["balance"] = $newSenderBalance;
		$targetData["balance"] = $newTargetBalance;
		$this->pendingCommandSaves[$senderId] = true;
		$this->pendingCommandSaves[$targetId] = true;
		try {
			$this->simpleSQL->saveSessionPair($senderSession, $senderData, $targetSession, $targetData,
				function (bool $success) use ($senderId, $targetId, $newSenderBalance, $newTargetBalance, $onComplete): void {
					unset($this->pendingCommandSaves[$senderId], $this->pendingCommandSaves[$targetId]);
					if ($success) {
						$this->updateBalanceCache($senderId, $newSenderBalance);
						$this->updateBalanceCache($targetId, $newTargetBalance);
					}
					$onComplete($success);
				});
		} catch (\Throwable $error) {
			unset($this->pendingCommandSaves[$senderId], $this->pendingCommandSaves[$targetId]);
			throw $error;
		}
	}

	/**
	 * Close a temporary session, saving first if dirty.
	 */
	public function closeTempSession(string $name): void {
		$lower = strtolower($name);
		$session = $this->simpleSQL->getSession($lower);
		if ($session !== null && $session->isDirty()) {
			$session->save(function (bool $success) use ($lower): void {
				$this->simpleSQL->closeSession($lower);
			});
		} else {
			$this->simpleSQL->closeSession($lower);
		}
	}

	// ──────────────────────────────────────────────
	//  Leaderboard cache (bounded - S3 compliant)
	// ──────────────────────────────────────────────

	/**
	 * Update the balance cache for a player and re-sort.
	 * Cache is bounded by leaderboard-size config to prevent O(accounts) memory.
	 */
	public function updateBalanceCache(string $name, int $balance): void {
		$lower = strtolower($name);
		$this->balanceCache[$lower] = $balance;
		arsort($this->balanceCache);

		// Trim to leaderboard size
		if (count($this->balanceCache) > $this->leaderboardSize) {
			$this->balanceCache = array_slice($this->balanceCache, 0, $this->leaderboardSize, true);
		}
	}

	/**
	 * Get top balances for the leaderboard.
	 *
	 * @return array<int, array{name: string, balance: int}>
	 */
	public function getTopBalances(int $limit = 10, int $offset = 0): array {
		$result = [];
		$i = 0;
		foreach ($this->balanceCache as $name => $balance) {
			if ($i >= $offset + $limit) {
				break;
			}
			if ($i >= $offset) {
				$result[] = ["name" => (string) $name, "balance" => $balance];
			}
			$i++;
		}
		return $result;
	}

	/**
	 * Get total number of entries in the balance cache.
	 */
	public function getBalanceCacheCount(): int {
		return count($this->balanceCache);
	}

	/**
	 * Get a player's rank position on the leaderboard.
	 * Returns null if the player is not in the cache.
	 */
	public function getPlayerRank(string $name): ?int {
		$lower = strtolower($name);
		$rank = 1;
		foreach ($this->balanceCache as $key => $balance) {
			if ($key === $lower) {
				return $rank;
			}
			$rank++;
		}
		return null;
	}

	// ──────────────────────────────────────────────
	//  Accessors
	// ──────────────────────────────────────────────

	public function getSimpleSQL(): SimpleSQL {
		return $this->simpleSQL;
	}

	public function getLang(): LangManager {
		return $this->lang;
	}

	public function getFormatter(): CurrencyFormatter {
		return $this->formatter;
	}

	public function getDefaultBalance(): int {
		return $this->defaultBalance;
	}

	public function getTopmoneyPerPage(): int {
		return $this->topmoneyPerPage;
	}

	/**
	 * Format a monetary amount using the configured currency formatter.
	 */
	public function formatMoney(int|float $amount): string {
		return $this->formatter->format($amount);
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy;

use Ifera\ScoreHud\event\PlayerTagsUpdateEvent;
use Ifera\ScoreHud\event\TagsResolveEvent;
use Ifera\ScoreHud\scoreboard\ScoreTag;
use NhanAZ\SimpleEconomy\event\TransactionSuccessEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\player\Player;

/**
 * ScoreHud integration - provides economy tags for scoreboards.
 *
 * This listener is ONLY registered when ScoreHud is installed (softdepend).
 * Do NOT instantiate this class if ScoreHud is not present.
 *
 * Available tags:
 *   {simpleeconomy.balance}  - formatted balance (e.g. "$1,000")
 *   {simpleeconomy.rank}     - leaderboard position (e.g. "3")
 *   {simpleeconomy.raw}      - raw balance number (e.g. "1000")
 */
class ScoreHudListener implements Listener {

	public const TAG_BALANCE = "simpleeconomy.balance";
	public const TAG_RANK = "simpleeconomy.rank";
	public const TAG_RAW = "simpleeconomy.raw";

	public function __construct(private readonly Main $plugin) {}

	/**
	 * Push tag updates for a specific player.
	 */
	private function updateTags(Player $player): void {
		$balance = $this->plugin->getMoney($player->getName());
		if ($balance === null) {
			return;
		}

		$rank = $this->plugin->getPlayerRank($player->getName());

		$event = new PlayerTagsUpdateEvent($player, [
			new ScoreTag(self::TAG_BALANCE, $this->plugin->formatMoney($balance)),
			new ScoreTag(self::TAG_RANK, $rank !== null ? (string) $rank : "-"),
			new ScoreTag(self::TAG_RAW, (string) $balance),
		]);
		$event->call();
	}

	/**
	 * Update tags when a player joins and their session is ready.
	 *
	 * We use a delayed task because the session may not be loaded yet at join time.
	 */
	public function onPlayerJoin(PlayerJoinEvent $event): void {
		$player = $event->getPlayer();

		// Delay to allow SimpleSQL session to load first
		$this->plugin->getScheduler()->scheduleDelayedTask(
			new \pocketmine\scheduler\ClosureTask(function () use ($player): void {
				if ($player->isOnline()) {
					$this->updateTags($player);
				}
			}),
			20 // 1 second delay
		);
	}

	/**
	 * Update tags when any transaction succeeds.
	 */
	public function onTransactionSuccess(TransactionSuccessEvent $event): void {
		$player = $this->plugin->getServer()->getPlayerExact($event->playerName);
		if ($player !== null) {
			$this->updateTags($player);
		}
	}

	/**
	 * Resolve tags on demand when ScoreHud requests them.
	 */
	public function onTagResolve(TagsResolveEvent $event): void {
		$player = $event->getPlayer();
		$tag = $event->getTag();

		$balance = $this->plugin->getMoney($player->getName());

		match ($tag->getName()) {
			self::TAG_BALANCE => $tag->setValue($balance !== null ? $this->plugin->formatMoney($balance) : "N/A"),
			self::TAG_RANK => $tag->setValue(($rank = $this->plugin->getPlayerRank($player->getName())) !== null ? (string) $rank : "-"),
			self::TAG_RAW => $tag->setValue($balance !== null ? (string) $balance : "0"),
			default => null,
		};
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\command;

use NhanAZ\SimpleEconomy\BalanceReader;
use NhanAZ\SimpleEconomy\BalanceAmount;
use NhanAZ\SimpleEconomy\event\TransactionEvent;
use NhanAZ\SimpleEconomy\event\TransactionSubmitEvent;
use NhanAZ\SimpleEconomy\event\TransactionSuccessEvent;
use NhanAZ\SimpleEconomy\Main;
use NhanAZ\SimpleSQL\Session;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\PluginOwnedTrait;

class AddMoneyCommand extends Command implements PluginOwned {
	use PluginOwnedTrait;

	public function __construct(
		private readonly Main $plugin,
	) {
		parent::__construct("addmoney", "Add money to a player's balance (OP only).", "/addmoney <player> <amount>");
		$this->setPermission("simpleeconomy.command.addmoney");
		$this->owningPlugin = $plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args): void {
		$lang = $this->plugin->getLang();

		if (count($args) < 2) {
			$sender->sendMessage($lang->get("addmoney.usage"));
			return;
		}

		$amountRaw = $args[1];
		if (is_numeric($amountRaw) && str_starts_with(trim($amountRaw), '-')) {
			$sender->sendMessage($lang->get("general.amount-positive"));
			return;
		}
		$amount = BalanceAmount::parseCommand($amountRaw);
		if ($amount === null) {
			$sender->sendMessage($lang->get("general.amount-not-number"));
			return;
		}
		if ($amount <= 0) {
			$sender->sendMessage($lang->get("general.amount-positive"));
			return;
		}

		$input = $args[0];
		$player = $this->plugin->resolvePlayer($input);
		$targetName = $player !== null ? $player->getName() : $input;

		$this->plugin->withPlayerSession(
			$targetName,
			onSession: function (Session $session, bool $temporary) use ($sender, $targetName, $amount, $lang): void {
				$oldBalance = BalanceReader::read($session);
				$newBalance = BalanceAmount::add($oldBalance, $amount);
				if ($newBalance === null) {
					$sender->sendMessage($lang->get("general.amount-not-number"));
					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
					return;
				}

				// Fire pre-transaction event
				$submitEvent = new TransactionSubmitEvent($targetName, $oldBalance, $newBalance, TransactionEvent::TYPE_ADD);
				$submitEvent->call();
				if ($submitEvent->isCancelled()) {
					$sender->sendMessage($lang->get("general.transaction-cancelled"));
					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
					return;
				}

				$this->plugin->saveCommandBalance($session, $targetName, $oldBalance, $newBalance, function (bool $success) use ($sender, $targetName, $oldBalance, $amount, $newBalance, $temporary, $lang): void {
					if ($success) {
						// Fire post-transaction event
						(new TransactionSuccessEvent($targetName, $oldBalance, $newBalance, TransactionEvent::TYPE_ADD))->call();

						$sender->sendMessage($lang->get("addmoney.success", [
							"amount" => $this->plugin->formatMoney($amount),
							"player" => $targetName,
							"balance" => $this->plugin->formatMoney($newBalance),
						]));

						$target = $this->plugin->getServer()->getPlayerExact($targetName);
						if ($target !== null && strtolower($target->getName()) !== strtolower($sender->getName())) {
							$target->sendMessage($lang->get("addmoney.notify", [
								"amount" => $this->plugin->formatMoney($amount),
								"balance" => $this->plugin->formatMoney($newBalance),
							]));
						}
					} else {
						$sender->sendMessage($lang->get("general.save-failed", ["player" => $targetName]));
					}

					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
				});
			},
			onError: function (string $msg) use ($sender): void {
				$sender->sendMessage($msg);
			},
		);
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\command;

use NhanAZ\SimpleEconomy\BalanceReader;
use NhanAZ\SimpleEconomy\Main;
use NhanAZ\SimpleSQL\Session;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\player\Player;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\PluginOwnedTrait;

class MoneyCommand extends Command implements PluginOwned {
	use PluginOwnedTrait;

	public function __construct(
		private readonly Main $plugin,
	) {
		parent::__construct("money", "View your balance or another player's balance.", "/money [player]");
		$this->setPermission("simpleeconomy.command.money");
		$this->owningPlugin = $plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args): void {
		$lang = $this->plugin->getLang();

		if (count($args) >= 1) {
			$input = $args[0];

			// Prefix match online players
			$player = $this->plugin->resolvePlayer($input);
			if ($player !== null) {
				$this->showBalance($sender, $player->getName());
				return;
			}

			// Offline - temp session
			$this->plugin->withPlayerSession(
				$input,
				onSession: function (Session $session, bool $temporary) use ($sender, $input, $lang): void {
					if (!$session->has("balance")) {
						$sender->sendMessage($lang->get("general.no-economy-data", ["player" => $input]));
					} else {
						$balance = BalanceReader::read($session);
						$rank = $this->plugin->getPlayerRank($input);
						$formatted = $this->plugin->formatMoney($balance);

						if ($rank !== null) {
							$sender->sendMessage($lang->get("money.other", [
								"player" => $input,
								"balance" => $formatted,
								"rank" => (string) $rank,
							]));
						} else {
							$sender->sendMessage($lang->get("money.other-unranked", [
								"player" => $input,
								"balance" => $formatted,
							]));
						}
					}
					if ($temporary) {
						$this->plugin->closeTempSession($input);
					}
				},
				onError: function (string $msg) use ($sender): void {
					$sender->sendMessage($msg);
				},
			);
		} elseif ($sender instanceof Player) {
			$this->showBalance($sender, $sender->getName());
		} else {
			$sender->sendMessage($lang->get("money.usage"));
		}
	}

	private function showBalance(CommandSender $sender, string $targetName): void {
		$lang = $this->plugin->getLang();
		$balance = $this->plugin->getMoney($targetName);

		if ($balance === null) {
			$sender->sendMessage($lang->get("money.error", ["player" => $targetName]));
			return;
		}

		$formatted = $this->plugin->formatMoney($balance);
		$rank = $this->plugin->getPlayerRank($targetName);
		$isSelf = $sender instanceof Player && strtolower($targetName) === strtolower($sender->getName());

		if ($isSelf) {
			$key = $rank !== null ? "money.self" : "money.self-unranked";
		} else {
			$key = $rank !== null ? "money.other" : "money.other-unranked";
		}

		$params = ["player" => $targetName, "balance" => $formatted];
		if ($rank !== null) {
			$params["rank"] = (string) $rank;
		}

		$sender->sendMessage($lang->get($key, $params));
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\command;

use NhanAZ\SimpleEconomy\BalanceAmount;
use NhanAZ\SimpleEconomy\BalanceReader;
use NhanAZ\SimpleEconomy\event\TransactionEvent;
use NhanAZ\SimpleEconomy\event\TransactionSubmitEvent;
use NhanAZ\SimpleEconomy\event\TransactionSuccessEvent;
use NhanAZ\SimpleEconomy\Main;
use NhanAZ\SimpleSQL\Session;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\player\Player;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\PluginOwnedTrait;

class PayCommand extends Command implements PluginOwned {
    use PluginOwnedTrait;

	public function __construct(private readonly Main $plugin) {
		parent::__construct("pay", "Transfer money to another online player.", "/pay <player> <amount>");
		$this->setPermission("simpleeconomy.command.pay");
		$this->owningPlugin = $plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args): void {
		$lang = $this->plugin->getLang();
		if (!$sender instanceof Player) {
			$sender->sendMessage($lang->get("general.only-ingame"));
			return;
		}
		if (count($args) < 2) {
			$sender->sendMessage($lang->get("pay.usage"));
			return;
		}
		$receiver = $this->plugin->resolvePlayer($args[0]);
		if ($receiver === null || !$receiver->isOnline()) {
			$sender->sendMessage($lang->get("general.player-not-online", ["player" => $args[0]]));
			return;
		}
		$senderName = $sender->getName();
		$targetName = $receiver->getName();
		if (strtolower($senderName) === strtolower($targetName)) {
			$sender->sendMessage($lang->get("pay.no-self"));
			return;
		}
		$amountRaw = $args[1];
		if (is_numeric($amountRaw) && str_starts_with(trim($amountRaw), "-")) {
			$sender->sendMessage($lang->get("general.amount-positive"));
			return;
		}
		$amount = BalanceAmount::parseCommand($amountRaw);
		if ($amount === null) {
			$sender->sendMessage($lang->get("general.amount-not-number"));
			return;
		}
		if ($amount <= 0) {
			$sender->sendMessage($lang->get("general.amount-positive"));
			return;
		}

		$this->plugin->withPlayerSession($senderName,
			onSession: function (Session $senderSession, bool $temporary) use ($sender, $senderName, $targetName, $amount, $lang): void {
				if ($temporary) {
					$this->plugin->closeTempSession($senderName);
					$sender->sendMessage($lang->get("pay.loading-self"));
					return;
				}
				$senderBalance = BalanceReader::read($senderSession);
				$newSenderBalance = BalanceAmount::reduce($senderBalance, $amount);
				if ($newSenderBalance === null) {
					$sender->sendMessage($lang->get("pay.insufficient", ["balance" => $this->plugin->formatMoney($senderBalance)]));
					return;
				}
				$this->plugin->withPlayerSession($targetName,
					onSession: function (Session $targetSession, bool $targetTemporary) use ($sender, $senderSession, $senderName, $senderBalance, $newSenderBalance, $targetName, $amount, $lang): void {
						if ($targetTemporary) {
							$this->plugin->closeTempSession($targetName);
							$sender->sendMessage($lang->get("pay.loading-target", ["player" => $targetName]));
							return;
						}
						if ($this->plugin->getServer()->getPlayerExact($targetName) === null) {
							$sender->sendMessage($lang->get("general.player-not-online", ["player" => $targetName]));
							return;
						}
						$targetBalance = BalanceReader::read($targetSession);
						$newTargetBalance = BalanceAmount::add($targetBalance, $amount);
						if ($newTargetBalance === null) {
							$sender->sendMessage($lang->get("general.amount-not-number"));
							return;
						}
						$submitEvent = new TransactionSubmitEvent($senderName, $senderBalance, $newSenderBalance, TransactionEvent::TYPE_PAY);
						$submitEvent->call();
						if ($submitEvent->isCancelled()) {
							$sender->sendMessage($lang->get("pay.cancelled"));
							return;
						}
						$this->plugin->saveTransfer($senderSession, $senderName, $senderBalance, $targetSession, $targetName, $targetBalance, $amount,
							function (bool $success) use ($sender, $senderName, $senderBalance, $newSenderBalance, $targetName, $targetBalance, $newTargetBalance, $amount, $lang): void {
								if (!$success) {
									$sender->sendMessage($lang->get("general.save-failed", ["player" => $targetName]));
									return;
								}
								(new TransactionSuccessEvent($senderName, $senderBalance, $newSenderBalance, TransactionEvent::TYPE_PAY))->call();
								(new TransactionSuccessEvent($targetName, $targetBalance, $newTargetBalance, TransactionEvent::TYPE_PAY))->call();
								$formatted = $this->plugin->formatMoney($amount);
								if ($sender->isOnline()) $sender->sendMessage($lang->get("pay.sent", ["amount" => $formatted, "player" => $targetName]));
								$this->plugin->getServer()->getPlayerExact($targetName)?->sendMessage($lang->get("pay.received", ["amount" => $formatted, "player" => $senderName]));
							});
					},
					onError: function (string $message) use ($sender): void { $sender->sendMessage($message); },
				);
			},
			onError: function (string $message) use ($sender): void { $sender->sendMessage($message); },
		);
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\command;

use NhanAZ\SimpleEconomy\BalanceReader;
use NhanAZ\SimpleEconomy\BalanceAmount;
use NhanAZ\SimpleEconomy\event\TransactionEvent;
use NhanAZ\SimpleEconomy\event\TransactionSubmitEvent;
use NhanAZ\SimpleEconomy\event\TransactionSuccessEvent;
use NhanAZ\SimpleEconomy\Main;
use NhanAZ\SimpleSQL\Session;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\PluginOwnedTrait;

class ReduceMoneyCommand extends Command implements PluginOwned {
	use PluginOwnedTrait;

	public function __construct(
		private readonly Main $plugin,
	) {
		parent::__construct("reducemoney", "Reduce money from a player's balance (OP only).", "/reducemoney <player> <amount>");
		$this->setPermission("simpleeconomy.command.reducemoney");
		$this->owningPlugin = $plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args): void {
		$lang = $this->plugin->getLang();

		if (count($args) < 2) {
			$sender->sendMessage($lang->get("reducemoney.usage"));
			return;
		}

		$amountRaw = $args[1];
		if (is_numeric($amountRaw) && str_starts_with(trim($amountRaw), '-')) {
			$sender->sendMessage($lang->get("general.amount-positive"));
			return;
		}
		$amount = BalanceAmount::parseCommand($amountRaw);
		if ($amount === null) {
			$sender->sendMessage($lang->get("general.amount-not-number"));
			return;
		}
		if ($amount <= 0) {
			$sender->sendMessage($lang->get("general.amount-positive"));
			return;
		}

		$input = $args[0];
		$player = $this->plugin->resolvePlayer($input);
		$targetName = $player !== null ? $player->getName() : $input;

		$this->plugin->withPlayerSession(
			$targetName,
			onSession: function (Session $session, bool $temporary) use ($sender, $targetName, $amount, $lang): void {
				$oldBalance = BalanceReader::read($session);

				if ($oldBalance < $amount) {
					$sender->sendMessage($lang->get("reducemoney.insufficient", [
						"player" => $targetName,
						"balance" => $this->plugin->formatMoney($oldBalance),
						"amount" => $this->plugin->formatMoney($amount),
					]));
					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
					return;
				}

				$newBalance = $oldBalance - $amount;

				// Fire pre-transaction event
				$submitEvent = new TransactionSubmitEvent($targetName, $oldBalance, $newBalance, TransactionEvent::TYPE_REDUCE);
				$submitEvent->call();
				if ($submitEvent->isCancelled()) {
					$sender->sendMessage($lang->get("general.transaction-cancelled"));
					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
					return;
				}

				$this->plugin->saveCommandBalance($session, $targetName, $oldBalance, $newBalance, function (bool $success) use ($sender, $targetName, $oldBalance, $amount, $newBalance, $temporary, $lang): void {
					if ($success) {
						// Fire post-transaction event
						(new TransactionSuccessEvent($targetName, $oldBalance, $newBalance, TransactionEvent::TYPE_REDUCE))->call();

						$sender->sendMessage($lang->get("reducemoney.success", [
							"amount" => $this->plugin->formatMoney($amount),
							"player" => $targetName,
							"balance" => $this->plugin->formatMoney($newBalance),
						]));

						$target = $this->plugin->getServer()->getPlayerExact($targetName);
						if ($target !== null && strtolower($target->getName()) !== strtolower($sender->getName())) {
							$target->sendMessage($lang->get("reducemoney.notify", [
								"amount" => $this->plugin->formatMoney($amount),
								"balance" => $this->plugin->formatMoney($newBalance),
							]));
						}
					} else {
						$sender->sendMessage($lang->get("general.save-failed", ["player" => $targetName]));
					}

					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
				});
			},
			onError: function (string $msg) use ($sender): void {
				$sender->sendMessage($msg);
			},
		);
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\command;

use NhanAZ\SimpleEconomy\BalanceReader;
use NhanAZ\SimpleEconomy\BalanceAmount;
use NhanAZ\SimpleEconomy\event\TransactionEvent;
use NhanAZ\SimpleEconomy\event\TransactionSubmitEvent;
use NhanAZ\SimpleEconomy\event\TransactionSuccessEvent;
use NhanAZ\SimpleEconomy\Main;
use NhanAZ\SimpleSQL\Session;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\PluginOwnedTrait;

class SetMoneyCommand extends Command implements PluginOwned {
	use PluginOwnedTrait;

	public function __construct(
		private readonly Main $plugin,
	) {
		parent::__construct("setmoney", "Set a player's balance (OP only).", "/setmoney <player> <amount>");
		$this->setPermission("simpleeconomy.command.setmoney");
		$this->owningPlugin = $plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args): void {
		$lang = $this->plugin->getLang();

		if (count($args) < 2) {
			$sender->sendMessage($lang->get("setmoney.usage"));
			return;
		}

		$amountRaw = $args[1];
		if (is_numeric($amountRaw) && str_starts_with(trim($amountRaw), '-')) {
			$sender->sendMessage($lang->get("general.amount-not-negative"));
			return;
		}
		$amount = BalanceAmount::parseCommand($amountRaw);
		if ($amount === null) {
			$sender->sendMessage($lang->get("general.amount-not-number"));
			return;
		}

		$input = $args[0];
		$player = $this->plugin->resolvePlayer($input);
		$targetName = $player !== null ? $player->getName() : $input;

		$this->plugin->withPlayerSession(
			$targetName,
			onSession: function (Session $session, bool $temporary) use ($sender, $targetName, $amount, $lang): void {
				$oldBalance = BalanceReader::read($session);

				// Fire pre-transaction event
				$submitEvent = new TransactionSubmitEvent($targetName, $oldBalance, $amount, TransactionEvent::TYPE_SET);
				$submitEvent->call();
				if ($submitEvent->isCancelled()) {
					$sender->sendMessage($lang->get("general.transaction-cancelled"));
					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
					return;
				}

				$this->plugin->saveCommandBalance($session, $targetName, $oldBalance, $amount, function (bool $success) use ($sender, $targetName, $oldBalance, $amount, $temporary, $lang): void {
					if ($success) {
						// Fire post-transaction event
						(new TransactionSuccessEvent($targetName, $oldBalance, $amount, TransactionEvent::TYPE_SET))->call();

						$sender->sendMessage($lang->get("setmoney.success", [
							"player" => $targetName,
							"old" => $this->plugin->formatMoney($oldBalance),
							"new" => $this->plugin->formatMoney($amount),
						]));

						$target = $this->plugin->getServer()->getPlayerExact($targetName);
						if ($target !== null && strtolower($target->getName()) !== strtolower($sender->getName())) {
							$target->sendMessage($lang->get("setmoney.notify", [
								"amount" => $this->plugin->formatMoney($amount),
							]));
						}
					} else {
						$sender->sendMessage($lang->get("general.save-failed", ["player" => $targetName]));
					}

					if ($temporary) {
						$this->plugin->closeTempSession($targetName);
					}
				});
			},
			onError: function (string $msg) use ($sender): void {
				$sender->sendMessage($msg);
			},
		);
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\command;

use NhanAZ\SimpleEconomy\Main;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\plugin\PluginOwned;
use pocketmine\plugin\PluginOwnedTrait;

class TopMoneyCommand extends Command implements PluginOwned {
	use PluginOwnedTrait;

	public function __construct(
		private readonly Main $plugin,
	) {
		parent::__construct("topmoney", "View the richest players leaderboard.", "/topmoney [page]");
		$this->setPermission("simpleeconomy.command.topmoney");
		$this->owningPlugin = $plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args): void {
		$lang = $this->plugin->getLang();
		$perPage = $this->plugin->getTopmoneyPerPage();

		$page = 1;
		if (count($args) >= 1 && is_numeric($args[0])) {
			$page = max(1, (int) $args[0]);
		}

		$total = $this->plugin->getBalanceCacheCount();
		if ($total === 0) {
			$sender->sendMessage($lang->get("topmoney.empty"));
			return;
		}

		$maxPage = max(1, (int) ceil($total / $perPage));
		$page = min($page, $maxPage);
		$offset = ($page - 1) * $perPage;

		$top = $this->plugin->getTopBalances($perPage, $offset);

		$sender->sendMessage($lang->get("topmoney.header", ["page" => (string) $page, "max" => (string) $maxPage]));

		foreach ($top as $i => $entry) {
			$rank = $offset + $i + 1;
			$name = $entry["name"];
			$balance = $this->plugin->formatMoney($entry["balance"]);
			$highlight = strtolower($name) === strtolower($sender->getName()) ? "§a" : "§f";

			$sender->sendMessage($lang->get("topmoney.entry", [
				"rank" => (string) $rank,
				"highlight" => $highlight,
				"player" => $name,
				"balance" => $balance,
			]));
		}

		$sender->sendMessage($lang->get("topmoney.footer"));
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\event;

use pocketmine\event\Event;

/**
 * Base event for all economy transactions.
 *
 * Provides read-only access to transaction details.
 * Listen to subclasses (TransactionSubmitEvent, TransactionSuccessEvent) instead.
 */
abstract class TransactionEvent extends Event {

	public const TYPE_SET = "set";
	public const TYPE_ADD = "add";
	public const TYPE_REDUCE = "reduce";
	public const TYPE_PAY = "pay";

	public function __construct(
		public readonly string $playerName,
		public readonly int $oldBalance,
		public readonly int $newBalance,
		public readonly string $type,
	) {}

	/**
	 * Get the difference between old and new balance.
	 */
	public function getAmount(): int {
		return abs($this->newBalance - $this->oldBalance);
	}
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\event;

use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;

/**
 * Called BEFORE a transaction is executed.
 *
 * Other plugins can listen to this event and cancel it to prevent the transaction.
 *
 * Example usage from another plugin:
 *
 *   public function onTransaction(TransactionSubmitEvent $event): void {
 *       if ($event->type === TransactionEvent::TYPE_PAY && $event->getAmount() > 10000) {
 *           $event->cancel();
 *       }
 *   }
 */
class TransactionSubmitEvent extends TransactionEvent implements Cancellable {
	use CancellableTrait;
}
<?php

declare(strict_types=1);

namespace NhanAZ\SimpleEconomy\event;

/**
 * Called AFTER a transaction has been successfully completed.
 *
 * This event is read-only - the transaction has already happened.
 * Use this for logging, notifications, or syncing with external systems.
 *
 * Example usage from another plugin:
 *
 *   public function onTransactionSuccess(TransactionSuccessEvent $event): void {
 *       $logger->info("{$event->playerName}: {$event->oldBalance} -> {$event->newBalance}");
 *   }
 */
class TransactionSuccessEvent extends TransactionEvent {
}
Ȓ!ÄQ£rút·N4aAV¥.Ôð„LyKÔ½ê§¹l   GBMB