Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dca9160c7 | ||
|
|
2bcc3c6af7 | ||
|
|
223b124f5c | ||
|
|
9ea3a45341 | ||
|
|
8a1782804e | ||
|
|
1723dc8c0c | ||
|
|
3db90103e1 | ||
|
|
2c5d1ab576 | ||
|
|
63b490ed39 | ||
|
|
31e41cdc5a | ||
|
|
5ffb160f25 | ||
|
|
5a45860f4c | ||
|
|
28a674cc6c | ||
|
|
0a5c35f057 | ||
|
|
8997512b46 | ||
|
|
5c7ab67726 | ||
|
|
f82b20919d | ||
|
|
2445f1e490 | ||
|
|
b25b66b62f | ||
|
|
220c1e1bb6 | ||
|
|
80fa951e56 | ||
|
|
af428263b5 | ||
|
|
cee62fbe5c | ||
|
|
404111d5ce | ||
|
|
1c9c60aae8 | ||
|
|
ac11753844 | ||
|
|
e5b7697c7f | ||
|
|
337fc3f2b8 | ||
|
|
ad0a0ad2d4 | ||
|
|
69e5fa2f64 | ||
|
|
8fbb70bedf | ||
|
|
c816383e12 | ||
|
|
29c4d46dd5 | ||
|
|
e051856b7a | ||
|
|
a9f0985398 | ||
|
|
25f66ba41f | ||
|
|
1293994911 | ||
|
|
e9195e9573 | ||
|
|
291a96f505 | ||
|
|
8ba2b53ba7 | ||
|
|
a874878fb4 | ||
|
|
4561ffa3d2 | ||
|
|
4b6c64fabd | ||
|
|
9754218e85 | ||
|
|
0d3b285111 | ||
|
|
c366bccb66 | ||
|
|
09a14a7c85 | ||
|
|
bcc0a70d2e | ||
|
|
8068f50d19 | ||
|
|
6962ab1a6c | ||
|
|
1987d30f18 | ||
|
|
322dd3f71b |
No files matched your search
@@ -1 +0,0 @@
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -1,3 +0,0 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
@@ -0,0 +1,16 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- dependency-name: "org.purpurmc.purpur:purpur-api"
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
target-branch: "shards"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- dependency-name: "org.purpurmc.purpur:purpur-api"
|
||||
@@ -0,0 +1,67 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
|
||||
|
||||
name: Java CI with Gradle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "shards" ]
|
||||
pull_request:
|
||||
branches: [ "master", "shards" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
# Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
|
||||
# See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
|
||||
- name: Build with Gradle Wrapper
|
||||
run: ./gradlew build
|
||||
|
||||
# NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
|
||||
# If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
|
||||
#
|
||||
# - name: Setup Gradle
|
||||
# uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
# with:
|
||||
# gradle-version: '8.9'
|
||||
#
|
||||
# - name: Build with Gradle 8.9
|
||||
# run: gradle build
|
||||
|
||||
dependency-submission:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
# Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
|
||||
# See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
.kotlin
|
||||
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "libs/Utilities-OG"]
|
||||
path = libs/Utilities-OG
|
||||
url = https://git.trueog.net/trueog/Utilities-OG
|
||||
@@ -1,27 +0,0 @@
|
||||
# DiamondBank-OG defect chart delta
|
||||
|
||||
* `K.consumeFromPlayer`, `J.consumeFromPlayer`, `K.playerPayPlayer`, `J.playerPayPlayer` (offline payer): fixed. `3df19f6` moves Utilities-OG to `1439e99`, whose offline inventory helpers read the converted profile and write the whole compound back (`UtilitiesOG.java:600-633`), together with TrueOG Purpur `97656e3c`, whose `getPlayerData` runs the DataFixer and stamps the current `DataVersion` (`PlayerDataStorage.java:257-269`).
|
||||
* `J.addToPlayerBankShards`, `J.subtractFromPlayerBankShards`, `J.getBankShards`, `J.getInventoryShards`, `J.getEnderChestShards`, `J.getTotalShards`, `J.getAllShards`, `J.consumeFromPlayer`, `J.playerPayPlayer`: unfixed, no change.
|
||||
* K and J argument and exception contracts: unfixed. Remains: K validation escapes its Result contract, J omits checked failures, and K payment KDoc names the wrong failure. Undeclared unchecked Java argument exceptions are withdrawn as a declaration defect; the current K argument and K/J exception documentation row carries the remaining issues (`9835d79` only moved `Balancetop.kt` lines).
|
||||
* `/setbankbalance`, `/setbankbal`: unfixed, no change.
|
||||
* `/compress`, `AutoCompress.compress` (from `onEntityPickupItem`, `onInventoryClose`): unfixed, no change to the space checks.
|
||||
* `/deposit all`, `/withdraw all`: unfixed, no change.
|
||||
* `/pay`: unfixed, no change.
|
||||
* `AutoDeposit.deposit` via `onEntityPickupItem`: partially fixed. `aba9961` credits the whole entity (`AutoDeposit.kt:16`, `27`; `Events.kt:181`) and `2e64c21` skips cancelled pickups (`Events.kt:148`). Remains: a failed credit still loses the emptied stack (`AutoDeposit.kt:39-40`, `44`).
|
||||
* `onPlayerJoin`: unfixed, no change.
|
||||
* `DiamondBankReload` (`/diamondbankreload`) and the executors it leaves behind (`/deposit`, `/withdraw`, `/pay`, `/compress`): partially fixed. `c1452b7` adds the admin check (`DiamondBankReload.kt:16-21`). Remains: stale executors, now seven classes after `9835d79` (`Balance.kt:18-19`, `Balancetop.kt:20-21`, `SetBankBalance.kt:20-21`), the pool leak, and Redis not rebuilt.
|
||||
* `Redis.getValue`, `Redis.setValue` via `onEntityPickupItem`, `onInventoryClose`, `/autocompress`, `/autodeposit`: unfixed, no change to `Redis.kt`; `b65077a` moved the currency test inside the disabled branch (`Events.kt:154-173`), so the main-thread call at `Events.kt:180` now runs for every item picked up.
|
||||
* `onDisable`: unfixed. `35feaf6` (`DiamondBankOG.kt:148-157`) only moves the cancel and join ahead of the Redis and manager shutdown; in-flight restores are still dropped (now the submitted bank writes and `runOnMainThread { restoreTo }` completion row, which also covers caller cancellation and cache divergence after submitted writes).
|
||||
* `plugin.yml`: partially fixed. `3df19f6` adds Utilities-OG to `softdepend` (`plugin.yml:59`); the LuckPerms clause did not reproduce (LuckPerms declares `load: STARTUP`) and is withdrawn. Remains: the `/dbtoggledebug` usage at `plugin.yml:56`.
|
||||
* `onEnable` after a same-instance disable, and recipes across `/minecraft:reload`: unfixed, no change. The same-instance re-enable clause is withdrawn, because Paper closes the plugin class loader on disable (`PaperPluginInstanceManager.java:226-235`) and a re-enable fails there whatever DiamondBank does; the recipe loss across `/minecraft:reload` and the missing prismarine bricks guard remain.
|
||||
* `TransactionLock.withLockSuspend`: unfixed, no change; `b65077a` (`Events.kt:154-173`, `205-224`) widens the trigger to pickups and drops of any item.
|
||||
* `InventorySnapshotUtils.removeAllShards`: regressed. `fc6ea81` (`InventorySnapshotUtils.kt:29`, `107`; `InventoryExtensions.kt:36-71`) removes renamed shards, which ends the infinite deposit, but the new remover throws on an emptied offhand stack; see `InventoryExtensions.removeItemWithSameKey` on an `InventorySnapshot`.
|
||||
* `InventorySnapshot.restoreTo` and the lock guards in `onInventoryMoveItem`: partially fixed. `b65077a` (`Events.kt:175`, `226`, `271`, `314`, `355`) now cancels every pickup, drop, click and block or entity interaction while locked and removes the own-inventory early return. Remains: the stale 41-slot restore across main-thread hops, applied after the bank write for bank-backed operations; right-click at air (`Events.kt:283`), armor stands, the recipe book, bow and trident release, consumption, totems, death and drags; and offline consumption writing the profile after the debit. With playerdata saving disabled, that offline write silently succeeds without persisting (`PlayerDataStorage.java:183`, `UtilitiesOG.java:627-628`), as covered by the shared `InventorySnapshot.restoreTo` row.
|
||||
* `CommonOperations.getPlayerUsingUuidOrName` and its callers' existence check: unfixed, no change.
|
||||
* `PostgreSQL.internalAddToPlayerShards`: unfixed, no change.
|
||||
* `PostgreSQL.getBaltopWithUuid`: unfixed, no change.
|
||||
* `InventorySnapshotUtils.removeShards`: unfixed, no change.
|
||||
* `PostgreSQL.setPlayerShards`, `addToPlayerShards`, `transferBankShards` (event emission): unfixed, no change; not carried forward, because no contract requires a listener's re-read to see the write and the event already carries the committed values.
|
||||
* `CommonOperations.diamondsToShards`: unfixed, no change.
|
||||
|
||||
Current commit `3df19f636773d113bcb6b58d715d3e6c2df852d3`. Reviewer: `ck:inspector` (Opus), because Codex (`gpt-5.6-sol` at `xhigh`) failed on the first run and on the retry after the reset with `ERROR: You’ve hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/settings/usage to purchase more credits or try again at 11:43 PM.` Reviewer findings: 20 accepted, 3 rejected. Codex (gpt-5.6-sol, xhigh): 11 accepted, 3 rejected.
|
||||
@@ -1,65 +0,0 @@
|
||||
# DiamondBank-OG defect chart
|
||||
|
||||
| Severity | Meaning |
|
||||
|---|---|
|
||||
| 🔴 | Critical. Freezes the economy or server, or loses, duplicates or misdirects value or items, and any player can trigger it in normal play (API consumers included) |
|
||||
| 🟠 | Serious. Wrong money or state that needs an admin action, another plugin, a reload or a rare state to arm, or a feature any player hits that is broken or refuses valid input |
|
||||
| 🟡 | Minor. Bounded slowdown, wrong message or audit value, API declaration mismatch, edge-input refusal |
|
||||
|
||||
## 1. Public API, offline-player compatibility
|
||||
|
||||
## 2. Public API, general correctness
|
||||
|
||||
| Entry point | Severity | Note |
|
||||
|---|---|---|
|
||||
| `J.addToPlayerBankShards`, `J.subtractFromPlayerBankShards`, `J.getBankShards`, `J.getInventoryShards`, `J.getEnderChestShards`, `J.getTotalShards`, `J.getAllShards`, `J.consumeFromPlayer`, `J.playerPayPlayer` | 🔴 server freeze | **J on the main thread deadlocks against any lock holder that is waiting for the next tick.** Every J method does `runBlocking { transactionLock.withLockSuspend(uuid) { ... } }` (`DiamondBankAPIJava.kt:40`, `76`, `155`, `169`, `226`, `285`), which parks the main thread in `Semaphore.acquire()` (`TransactionLock.kt:28`). Every holder that touches an inventory releases only after `runOnMainThread` (`MainThreadBlock.kt:14-26`), which needs the next scheduler heartbeat: `/deposit`, `/withdraw`, `/pay`, `/compress` (`Deposit.kt:82`, `Withdraw.kt:85`, `Pay.kt:95`, `Compress.kt:54`), `AutoCompress` (`AutoCompress.kt:39`), K and J `consumeFromPlayer` and `playerPayPlayer` called off the main thread, and the recounts in `onPlayerJoin`, `onPlayerQuit`, `onPlayerRespawn`, `onEntityPickupItem`, `onPlayerDropItem`, `onInventoryClose` (`Events.kt:80`, `105`, `133`, `190`, `233`, `383`). The pickup and drop recounts run for every item a player picks up or drops (`Events.kt:184-197`, `231-239`), so the window opens constantly. Repro: a plugin's command executor calls `J.getBankShards` for a player in the tick that player drops any item: the main thread parks, the recount never gets its tick, and the watchdog kills the server. The KDoc documents blocking, synchronous use (`:25-26`). *Fix: when `Bukkit.isPrimaryThread()`, acquire the lock in a polling loop that drains queued `runOnMainThread` blocks from a plugin-owned queue instead of `acquire()`.* |
|
||||
| `K.addToPlayerBankShards`, `K.subtractFromPlayerBankShards`, `K.consumeFromPlayer`, `K.playerPayPlayer`, `K.getBankShards`, `K.getInventoryShards`, `K.getEnderChestShards`, `K.getTotalShards`, `K.getAllShards`, `K.getBaltop` under caller cancellation, and the same J methods when the calling thread is interrupted | 🟠 economy disabled without a database error | **Caller cancellation at a database await is classified as a database error and disables the economy.** The `PostgreSQL` methods catch `Exception`, including the `CancellationException` thrown by a cancelled await (`PostgreSQL.kt:97`, `119`, `201`, `231`, `266`, `279`, `305`, `334`, `425`, `444`; kotlinx-coroutines 1.10.2 `future/Future.kt:148-154`, `168-173`), and return `DatabaseException`. K then calls `handleError` (`DiamondBankAPIKotlin.kt:42`, `47`, `77`, `82`, `111`, `129`, `142`, `176`, `184`, `241`, `251`) and so does J (`DiamondBankAPIJava.kt:46`, `51`, `83`, `88`, `158`, `180`, `201`, `237`, `245`, `299`, `309`), although no database error occurred. Interrupting J's `runBlocking` caller cancels its coroutine (`Builders.kt:93-98`). Repro: `withTimeout(1) { api.getBankShards(uuid) }` while an uncached UUID's database read is pending disables the economy for everyone. *Fix: rethrow `CancellationException` before every generic catch in `PostgreSQL`.* |
|
||||
| K argument contracts and K/J exception documentation: `K.addToPlayerBankShards`, `K.subtractFromPlayerBankShards`, `K.consumeFromPlayer`, `K.playerPayPlayer`, `K.getBaltop`, `J.consumeFromPlayer`, `J.playerPayPlayer`, `J.diamondsToShards` | 🟡 API contract mismatch | **K argument validation escapes its Result contract, J omits checked exceptions, and one KDoc names a nonexistent failure.** (a) `require(shards <= Long.MAX_VALUE.toULong())` (`DiamondBankAPIKotlin.kt:33`, `67`, `161`, `221`) and `require(offset >= 0)` (`CachingBalanceManager.kt:180`, reached from `DiamondBankAPIKotlin.kt:141`) throw `IllegalArgumentException` instead of returning K's promised `Result` (`api/README.md:29`). (b) J rethrows `InsufficientBalanceException` (`DiamondBankAPIJava.kt:244`, `308`, and `240`, `302`, which rethrow every non-database failure of `consume`) that its `@Throws` (`220`, `273-278`) and the README omit; reachable via a negative receiver bank (`PostgreSQL.kt:143-144`), via the excess-change credit on a negative bank (`InventorySnapshotUtils.kt:41-43`, `CachingBalanceManager.kt:67-68`), or split caches after reload. (c) `MoreThanOneDecimalDigitRuntimeException` extends the checked `DiamondBankException` (`DiamondBankException.kt:20`, base `Exception` at `:3`) and `J.diamondsToShards` has no `@Throws` (`DiamondBankAPIJava.kt:335`, `343`), so `catch (MoreThanOneDecimalDigitRuntimeException e)` does not compile in Java and `catch (RuntimeException e)` misses it. (d) `K.playerPayPlayer` KDoc (`DiamondBankAPIKotlin.kt:206-208`) names `CouldNotRemoveEnoughException.notRemoved` and tells callers to continue with a partial amount; the real failure is `InsufficientFundsException.short` and nothing moved. Repro: `K.getBaltop(-1)` throws `IllegalArgumentException` instead of returning a `Result`. *Fix: return `Result.failure(InvalidArgumentException())` for K's four shard validations and validate K's offset into a failure before the manager call at `DiamondBankAPIKotlin.kt:141`; keep these caller errors out of `handleError`; add `InsufficientBalanceException` to both J `@Throws` lists and the README; make `MoreThanOneDecimalDigitRuntimeException` extend `DiamondBankRuntimeException`; correct the KDoc.* |
|
||||
|
||||
## 3. Commands
|
||||
|
||||
| Entry point | Severity | Note |
|
||||
|---|---|---|
|
||||
| `/setbankbalance`, `/setbankbal` | 🟠 negative balance accepted | **Any negative shard count is stored, and nothing downstream expects one.** `SetBankBalance.kt:68` parses with `toLong()` and `74` stores it. With `-900`: `/withdraw all` takes the negative balance as the amount (`Withdraw.kt:99-104`), `subtractFromBankShards` negates it into a credit (`Withdraw.kt:144`, `CachingBalanceManager.kt:67`, `85-86`), the inventory change is skipped because `addItem` ignores non-positive stacks (`Withdraw.kt:123-131`), the bank returns to 0 and the player reads "Successfully withdrew -100.0 Diamonds" (`Withdraw.kt:161-169`); `/pay` and `playerPayPlayer` demand `shards + 900` from the inventory (`CommonOperations.kt:29-41`), and if it covers that, the negative amount hits `require(shardsToSubtractFromSender >= 0)` (`CachingBalanceManager.kt:94`) with any excess-change credit (`InventorySnapshotUtils.kt:41`, `67`, `74`) already committed. A negative receiver also arms the half-transfer in `PostgreSQL.internalAddToPlayerShards`. Repro: `/setbankbal Alice -900`, then Alice runs `/withdraw all`. *Fix: reject `balance < 0` after the parse at `SetBankBalance.kt:68`.* |
|
||||
| `/withdraw`, `/pay` | 🟠 economy disabled on a refusal | **A balance refusal from the database pre-check is handled as a database error, so `/withdraw` and `/pay` disable the economy whenever the cached balance is higher than the stored one.** `Withdraw.kt:144-145` and `Pay.kt:141-142` pass every failure of `subtractFromBankShards` and `transferBankShards` to `handleError`, including the `InsufficientBalanceException` that `CachingBalanceManager.kt:67-68` and `104-105` return when the stored balance does not cover the amount; K and J treat that exception as a caller error (`DiamondBankAPIKotlin.kt:76`, `183`, `250`, `DiamondBankAPIJava.kt:82`, `244`, `308`). The commands' own check reads the cached balance (`Withdraw.kt:89-90`, `CommonOperations.kt:25`), which is higher than the stored one after a reload (the commands keep the old manager), after a failed transfer leaves the sender's cache stale, or when `/setbankbal`, which takes no transaction lock (`SetBankBalance.kt:74`), lowers the balance during the command. Repro: after `/diamondbankreload`, another plugin debits 90 of a player's 100 bank shards through `J.subtractFromPlayerBankShards`; the player runs `/withdraw 5`: the economy is disabled. *Fix: at `Withdraw.kt:144` and `Pay.kt:141`, answer `InsufficientBalanceException` with the insufficient-funds reply and refresh the cached balance instead of calling `handleError`.* |
|
||||
| `/compress`, `AutoCompress.compress` (from `onEntityPickupItem`, `onInventoryClose`) | 🟡 wrong refusal | **The space check runs before the shards it will convert are removed, so it ignores the slots they free.** `Compress.kt:131-147` (diamonds) and `149-165` (blocks) check before the removals at `169-183` and `198-212`; `AutoCompress.kt:65-99` repeats the same check before `101-157`. Repro: 35 slots of anything, 18 Diamond Shards in one slot, occupied offhand: "You do not have enough space" although the freed slot fits 2 diamonds; with auto-compress on, the message repeats on every item pickup and container close. *Fix: in both places compute free space after applying the pending removals to a copy, or run the `addItem` calls first and abort on leftover; share the routine.* |
|
||||
| `/deposit all`, `/withdraw all` | 🟡 wrong audit value | **Both log `-1`, the `all` sentinel, as the transferred amount.** `Deposit.kt:148` passes `shards` (`50`); `Withdraw.kt:171` passes `shards` (`53`). Repro: `/deposit all` with 5 diamonds: the audit row reads `-1`. *Fix: pass `removedInShards` at `Deposit.kt:148` and `shardsToWithdraw` at `Withdraw.kt:171`.* |
|
||||
| `/balancetop <page>`, `/baltop <page>` | 🟡 no reply or wrong page | **Unchecked page bounds and Int offset arithmetic cause exceptions or nonsensical negative-page replies.** `Balancetop.kt:58-59` parses the page and computes `offset = 9 * (index - 1)` in `Int`. Page 0, ordinary negative pages, and positive pages whose offsets wrap negative, such as 238609296, reach `require(offset >= 0)` (`CachingBalanceManager.kt:180`) through `Balancetop.kt:113-114`, throw `IllegalArgumentException` and kill the coroutine without a reply. Other negative pages wrap to positive offsets: -238609295 produces 2147483632, passes the offset check and the upper-page check (`Balancetop.kt:133`), and displays a negative page number. Repro: `/baltop 0`: no reply, a stack trace in the console. *Fix: at `Balancetop.kt:58-59` reject `index < 1` with the existing page message, compute the offset as `Long`, and refuse pages beyond the last before querying.* |
|
||||
| `/pay` | 🟡 documented `all` refused | **`README.md:19` promises `/pay <player> all`; `Pay.kt:69` parses with `toFloat()` and answers "Invalid argument".** Repro: `/pay Bob all`. *Fix: accept `all` at `69` (pay the full bank balance) or drop it from the README.* |
|
||||
|
||||
## 4. Event listeners and automatic features
|
||||
|
||||
| Entry point | Severity | Note |
|
||||
|---|---|---|
|
||||
| `AutoDeposit.deposit` via `onEntityPickupItem` | 🟠 item loss on database error | **The picked-up stack is deleted before the credit, so a failed credit loses it.** `AutoDeposit.kt:39-40` empties the item entity on the main thread (the entity is discarded on its next tick, `ItemEntity.java:130-131`), and `42-53` credit the bank later on a coroutine; when `addToBankShards` fails (`AutoDeposit.kt:44`), `handleError` disables the economy and the player is told to notify staff, but the stack is never given back. Repro: auto-deposit on, stop PostgreSQL, walk over 64 diamonds: the economy disables and the 64 diamonds are gone. *Fix: keep a copy of the stack before `39` and, in the `getOrElse` at `44-52`, drop it at the player through `runOnMainThread`.* |
|
||||
| `onEntityPickupItem`, `onPlayerDropItem`, `onInventoryClose` | 🟠 interactions cancelled, commands refused, per-pickup load | **The currency test now guards only the disabled-economy branch, so every pickup or drop of any item runs the auto features, a main-thread Redis read and a locked recount, and with auto-compress on every pickup locks the whole inventory.** `Events.kt:154-173` and `205-224` test the item type only while `economyDisabled` is set; otherwise `Events.kt:180-197` and `231-239` run for cobblestone as for diamonds, including the synchronous Redis read at `Events.kt:180` (a new connection per call, see the Redis row). Each recount holds the transaction lock across a main-thread hop and a database write (`Events.kt:184-197`, `231-239`, `381-400`), and `/deposit`, `/withdraw`, `/pay` and `/compress` take that lock with `tryWithLockSuspend` (`Deposit.kt:78`, `Withdraw.kt:81`, `Pay.kt:92`, `Compress.kt:50`), so a player who is picking up items is told "You are currently blocked from using /deposit." (`Deposit.kt:155`, `Withdraw.kt:178`, `Pay.kt:198`, `Compress.kt:260`). With auto-compress on, each pickup (and each container close, `Events.kt:376-379`) runs `AutoCompress.compress`, which takes both locks and waits a tick for the snapshot even when nothing converts (`AutoCompress.kt:37-39`, `159`); while the inventory lock is held, `Events.kt:175`, `226`, `271`, `314`, `355` cancel every pickup, drop, click and block or entity interaction, and a cancelled `LEFT_CLICK_BLOCK` makes the server resend the block (`ServerPlayerGameMode.java:198-209`), so the dig is lost. A player with auto-deposit or auto-compress on who lacks `diamondbank-og.deposit` or `diamondbank-og.compress` is messaged on every pickup (`AutoDeposit.kt:22-25` before the type check at `28-37`; `AutoCompress.kt:30-34`). Repro: `/autocompress` on, mine a row of stone with an Efficiency V pickaxe: blocks whose break starts while the previous drop is being picked up reappear. *Fix: repeat the four-type currency test after the lock checks at `Events.kt:178` and `229` and return for any other item; in `AutoCompress.compress` decide on the main thread, from the live inventory, whether anything can be compressed, and take no lock when nothing can.* |
|
||||
| `onPlayerJoin` | 🟠 legacy balance lost, false message, migration rounding | **Migration runs only for a player without a database row, so a legacy balance is skipped for good once anything creates that row, every new player is told a balance was migrated, and a fractional legacy balance is truncated.** (a) `Events.kt:56` gates the migration on `!hasEntry`. Paying an offline legacy player passes `hasPlayedBefore()` (`Pay.kt:58`) and the transfer's upsert inserts the receiver row (`PostgreSQL.kt:136-137` via `transferBankShards`), as does an API credit; at that player's next join `hasEntry` is true and `diamondbank:balance` is never read. (b) The key removal and the message at `Events.kt:70-75` sit outside `if (legacyBalance != null)` (`62-69`), so every first join prints "Your old balance has been successfully migrated". (c) The legacy value is a `DOUBLE` (`Events.kt:57-60`) converted with `legacyBalance.toLong() * 9` (`64`), so `10.7` becomes 90 shards, not 97 (`CommonOperations.kt:48-53`). Repro: for a player whose data still holds `diamondbank:balance` and who has not joined since the migration shipped, `/pay <them> 0.1`; after they join their bank holds 1 shard. *Fix: at `56` gate on the presence of the legacy key instead of `hasEntry`, credit it with `addToBankShards` instead of `setPlayerShards`, convert at `64` with `diamondsToShards`, and move the removal and the message inside the `legacyBalance != null` branch.* |
|
||||
|
||||
## 5. Startup, shutdown, reload, configuration, and persistence setup
|
||||
|
||||
| Entry point | Severity | Note |
|
||||
|---|---|---|
|
||||
| `DiamondBankReload` (`/diamondbankreload`) and the executors it leaves behind (`/deposit`, `/withdraw`, `/pay`, `/compress`, `/balance`, `/bal`, `/balancetop`, `/baltop`, `/setbankbalance`, `/setbankbal`) | 🟠 stale balances after reload, pool leak, partial reload | **Reload leaves command executors bound to stale state, leaks the previous pool, and keeps the old Redis configuration.** (a) Seven command executors, behind ten command labels, keep the manager and config that existed at enable, while events, auto features and both APIs move to the new manager. `Deposit.kt:22-23`, `Withdraw.kt:25-26`, `Pay.kt:23-24`, `Compress.kt:33-34`, `Balance.kt:18-19`, `Balancetop.kt:20-21`, `SetBankBalance.kt:20-21` take `DiamondBankOG.config` and `DiamondBankOG.balanceManager` as constructor defaults, evaluated once by `setUpCommands` (`DiamondBankOG.kt:103`, `124-134`). The reload swaps the companion (`DiamondBankReload.kt:23`, `35`) without calling `setUpCommands` or `shutdown()`, so the two caches never see each other's writes and the old pool lives until restart, one more per run. Repro: a player online since before the reload has 100 bank shards; another plugin debits 90 through `J.subtractFromPlayerBankShards`; the player runs `/balance`: it still shows 100, and `/withdraw 5` then passes the stale check at `Withdraw.kt:106` (see the `/withdraw`, `/pay` row). (b) Redis is never rebuilt. `redis` is created once (`DiamondBankOG.kt:86`) and reads the config only at construction (`Redis.kt:9-15`); after editing `config.yml:14-17` the command still says "Successfully reloaded the config." (`DiamondBankReload.kt:47`). *Fix: after the swap at `DiamondBankReload.kt:35-45`, call `plugin.setUpCommands()` so the executors bind the new manager and config; shut the previous manager down only after the transactions running against it finish; rebuild `redis` (shutting down the old client) in the same command.* |
|
||||
| `Redis.getValue`, `Redis.setValue` via `onEntityPickupItem`, `onInventoryClose`, `/autocompress`, `/autodeposit` | 🟠 main-thread stall per pickup, exception on outage, half-applied toggle | **Every call opens a new connection and catches nothing, the pickup handler makes that call on the main thread, and the toggle commands write their two keys separately.** `Redis.kt:28-33` and `36-41` connect, handshake, `SELECT`, run one command and close, on every call. Main-thread callers: `Events.kt:180` (each pickup that reaches the auto-deposit check), `command/AutoCompress.kt:33-47`, `command/AutoDeposit.kt:33-47`. Async callers: `Events.kt:185`, `377`. When Redis is down, `RedisConnectionException` escapes the pickup handler (the pickup completes and the recount at `184-197` is skipped), kills the `onInventoryClose` recount coroutine (`376-401`), and reaches the two commands as "An internal error occurred"; a failure between the two `setValue` calls (`:45`, `:47` in both commands) clears one toggle without setting the other. When the host is unreachable, each pickup costs a 10 s connect timeout on the main thread (`SocketOptions.java:37`). A throw after `connect()` leaks the connection. Repro: drop routing to the Redis host, then pick up a diamond: the server stalls 10 s. *Fix: use a managed long-lived connection and return explicit command failures separately from missing keys; stop and report a failed toggle at `command/AutoCompress.kt:33-48` and `command/AutoDeposit.kt:33-48` instead of continuing to a success message; serve the read at `Events.kt:180` from a cache or off the main thread; move both toggle commands' Redis work into `scope.launch` and write the mutually exclusive key pair in one `MSET`.* |
|
||||
| Recipes across `/minecraft:reload`, and the craft guard | 🟠 shard recipes lost, currency crafted away | **Recipes are registered only in `onEnable`, so a datapack reload removes them until restart, and the craft guard misses prismarine bricks.** `Shard.kt:33`, `39` add both recipes once (`DiamondBankOG.kt:105`); `/minecraft:reload` installs a fresh `RecipeManager` (`MinecraftServer.java:2086`) without the plugin recipes and nothing re-registers them, so diamonds no longer split into shards and 9 Diamond Shards in a 3x3 craft vanilla prismarine bricks, which `onPrepareItemCraft` does not block (`Events.kt:408-410` list only prismarine, dark prismarine, sea lantern). The bricks recipe also wins without a reload whenever one shard in the grid was renamed, because the plugin recipe's exact ingredient compares full NBT (`Ingredient.java:85-86`). Repro: `/minecraft:reload`, then 9 Diamond Shards in a crafting table: prismarine bricks. *Fix: re-register both recipes on `ServerResourcesReloadedEvent`; add `PRISMARINE_BRICKS` to the list at `Events.kt:408-410`.* |
|
||||
| `onEnable` and `DiamondBankReload` configuration loading (`YamlConfig.create`, `PostgreSQL.create`) | 🟠 plugin disabled with valid configuration | **Configuration loading omits upgrade defaults and environment expansion, rejects numeric credentials, and refuses pre-Flyway databases.** (a) `saveDefaultConfig` (`YamlConfig.kt:23`) leaves existing files untouched (`JavaPlugin.java:194-197`), and `YamlConfig.kt:25` loads without defaults. A deployed config predating `postgresMaxActiveConnections` lacks that key; `parseKeyAs<Int>` returns null (`YamlConfig.kt:71-76`), the guard at `47` rejects it, and `onEnable` disables the plugin (`DiamondBankOG.kt:70-75`). (b) `YamlConfig.kt:25`, `72` never resolves the `^{ENV}` placeholders at `config.yml:9-10`, `17`. Bukkit reads and constructs the scalars unchanged (`FileConfiguration.java:123-128`, `152-160`, `YamlConfiguration.java:149-164`, `YamlConstructor.java:25-27`), so even defined environment variables reach PostgreSQL and Redis as literal placeholder credentials (`PostgreSQL.kt:37-38`, `49`, `Redis.kt:12`), causing authentication failure and plugin disable. Numeric literal credentials are also rejected by `as? String` (`YamlConfig.kt:72`); an all-digit password parsed as an Integer becomes null and the connection is attempted without it. (c) `PostgreSQL.kt:40-41` configures Flyway without a baseline; a database created by a pre-Flyway build has the existing `diamond` and `diamond_log` tables but no schema history, so migration fails and `PostgreSQL.create` returns null (`53-59`). Repro: start with the bundled placeholder credentials and the corresponding environment variables set: the literal placeholder is used as the PostgreSQL user. *Fix: attach the bundled YAML as defaults after `YamlConfig.kt:25`, use typed scalar getters that stringify credentials, and resolve `^{ENV}` in parsed string values before constructing `YamlConfig`; report missing variables explicitly; at `PostgreSQL.kt:40` baseline at version 1 only after validating the known existing schema and confirming no history table exists.* |
|
||||
| `plugin.yml` | 🟡 wrong help text | **The debug command help names a nonexistent command.** `plugin.yml:56` gives `/dbtoggledebug` as the usage for `dbogtoggledebug`, so `/help dbogtoggledebug` shows the wrong command. Repro: `/help dbogtoggledebug`. *Fix: write `/dbogtoggledebug` at `56`.* |
|
||||
|
||||
## 6. Shared internals
|
||||
|
||||
| Component | Severity | Note |
|
||||
|---|---|---|
|
||||
| `TransactionLock.withLockSuspend` | 🔴 economy freeze, shutdown hang | Reached from: every K balance method except `getBaltop`, every J balance method except `getBaltop` when called from a `Dispatchers.Default` thread (including J calls made inside a `PlayerBalanceChangedListener`, `EventManager.kt:23-27`), `/deposit`, `/withdraw`, `/pay`, `/compress` (holders through `tryWithLockSuspend`), `onPlayerJoin`, `onPlayerQuit`, `onPlayerRespawn`, `onEntityPickupItem`, `onPlayerDropItem`, `onInventoryClose`, `AutoCompress.compress`, `AutoDeposit.deposit`, `onDisable`. **Waiting parks a `Dispatchers.Default` thread, so enough waiters starve the holder and nothing in the scope ever runs again.** `TransactionLock.kt:26-34` calls the blocking `Semaphore.acquire()` inside a suspend function; the scope is `Dispatchers.Default` (`DiamondBankOG.kt:36`), capped at `max(2, nCPU)` workers (`Tasks.kt:29-33`), and a parked thread counts as busy. Once `nCPU` waiters for one UUID are parked while the holder is suspended (next-tick `runOnMainThread` or a database await), the holder never resumes and never releases: every command, recount, K call and the jasync pool actor (also on `Dispatchers.Default`, `ActorBasedObjectPool.kt:147-148`) hang; `economyDisabled` stays false; when the stuck holder owns the inventory lock, the guards at `Events.kt:175`, `226`, `271`, `314`, `355` cancel every pickup, drop, click and interaction of that player until restart; and auto-deposit keeps emptying picked-up stacks on the main thread (`AutoDeposit.kt:39-40`) while their credits wait forever (`AutoDeposit.kt:43`). The pickup and drop handlers launch a waiter for every item of any type (`Events.kt:184-197`, `231-239`), two per pickup with auto-compress on (`Events.kt:186`, `AutoCompress.kt:24`, `37`). `/stop` then hangs: `onDisable` joins the scope (`DiamondBankOG.kt:150`), a waiter blocked in `acquire()` ignores cancellation, the watchdog is already stopped (`MinecraftServer.java:931`), and players are never saved (`:971`); the join hangs even with a single waiter when the holder is an outside K or J call suspended in `runOnMainThread`, because the main thread is inside `onDisable`. Repro on 8 cores: break a chest holding 27 different stacks and stand in the drops so that nine are picked up in one tick; afterwards no economy command replies and `/stop` never finishes. *Fix: replace `java.util.concurrent.Semaphore` with `kotlinx.coroutines.sync.Semaphore` or `Mutex` at `26-34` and `49-60` so waiting suspends and honors cancellation; J's `runBlocking` keeps its blocking behavior on top.* |
|
||||
| `InventorySnapshot.restoreTo` and the lock guards in `Events.kt` | 🔴 item dup and loss | Reached from: `K`/`J` `consumeFromPlayer`, `K`/`J` `playerPayPlayer`, `/deposit`, `/withdraw`, `/pay`, `/compress`, `AutoCompress.compress` (from every item pickup and container close). **The restore overwrites all 41 live slots from a snapshot that can span ticks, while incomplete guards allow intervening item changes.** `InventorySnapshot.kt:81` (`player.inventory.contents = newContents`) never compares the live inventory with what `from` copied. Commands and auto features take and restore snapshots across main-thread hops (`MainThreadBlock.kt:14-26`); bank-backed operations restore after their bank write, while compression rewrites without one (`Compress.kt:242`, `AutoCompress.kt:159`). The guards cancel pickups, drops, clicks and block and entity interactions while locked (`Events.kt:175`, `226`, `271`, `314`, `355`), but: `onPlayerInteract` returns at `283` before the lock check when no block was clicked, so a right-click at air still throws ender pearls, snowballs, splash potions and experience bottles (`ServerGamePacketListenerImpl.java:2098-2101`); armor stand swaps fire `PlayerInteractAtEntityEvent`, which keeps its own handler list (`PlayerInteractAtEntityEvent.java:21`, `ServerGamePacketListenerImpl.java:2955-2959`); a recipe book click moves ingredients into the crafting grid and fires only `PlayerRecipeBookClickEvent` (`ServerGamePacketListenerImpl.java:3397-3401`); releasing a bow or trident, finishing food or a potion, a totem pop and dying are not cancelled, and a death clears the same `ServerPlayer` that the restore then refills and respawn reuses (`ServerPlayer.java:992-998`, `PlayerList.java:815`); every handler returns before its lock check outside the three vanilla worlds (`Events.kt:152`, `203`, `246`, `281`, `324`). Each leaves the item both in the world or the grid and back in its slot. `InventoryDragEvent` is unguarded, so a stack already on the cursor dragged into the inventory is overwritten; arrow and trident pickups fire only `PlayerPickupArrowEvent`, which extends `PlayerPickupItemEvent` rather than `EntityPickupItemEvent` (`AbstractArrow.java:565-580`), so the guard at `Events.kt:175` never sees them and the restore deletes the picked-up item; items another plugin adds in the window are deleted the same way. For offline `consumeFromPlayer` the restore writes the stored profile through `UtilitiesOG.setInventoryData` (`InventorySnapshot.kt:78-79`), which throws when TrueOG Purpur rejects the RocksDB write (`PlayerDataStorage.java:181-192`): the bank debit and any excess-change credit stay committed, the profile keeps every removed item, and the audit row (`DiamondBankAPIKotlin.kt:195`, `DiamondBankAPIJava.kt:252`) is skipped. With `players.disable-saving` enabled (`SpigotConfig.java:406-408`), the same offline restore silently writes nothing (`PlayerDataStorage.java:183`), although Utilities-OG returns true (`UtilitiesOG.java:627-628`); consuming from a previously saved offline inventory can therefore report success without removing its currency. Repro: spam `/deposit 0.1` while a repeating command block runs `/kill <player>`: the death pile and the restored inventory both exist. *Fix: replace the unconditional write at `InventorySnapshot.kt:78-82` with live-slot reconciliation and apply only changed slots on the main thread; for bank-backed operations apply the item change before the bank write and safely return the removed items if that write fails, preserving intervening slot changes; refuse offline rewriting when playerdata saving is disabled, write the profile before the bank transaction, and confirm the persisted inventory instead of trusting the helper boolean; skip unchanged snapshots.* |
|
||||
| `CommonOperations.getPlayerUsingUuidOrName` and its callers' existence check | 🔴 wrong recipient, new players unpayable, main-thread stall | Reached from: `/pay`, `/balance <player>`, `/bal <player>`, `/balancetop <player>`, `/baltop <player>`, `/setbankbalance`, `/setbankbal`. **Name resolution prefix-matches online players, then treats a first-session player as nonexistent, and `/pay` runs it on the main thread.** (a) `CommonOperations.kt:68` calls `Bukkit.getPlayer(String)`, which returns the online player whose name merely starts with the argument (`CraftServer.java:619-641`). Repro: Bob offline, Bobby online, `/pay Bob 10` pays Bobby; `/setbankbal Bob 0` zeroes Bobby. (b) Each caller then uses `hasPlayedBefore()` as the existence test (`Pay.kt:58`, `Balance.kt:66`, `Balancetop.kt:62`, `SetBankBalance.kt:61`); for an online player that is `CraftPlayer.hasPlayedBefore`, a field set only by `readExtraData` when saved data existed at login (`CraftPlayer.java:2110-2111`, `ServerPlayer.java:497`), so a player in their first session gets "That player doesn't exist" from all four commands until they relog. (c) `Pay.kt:52` resolves before `scope.launch` (`90`); on a user-cache miss `Bukkit.getOfflinePlayer(String)` performs a synchronous Mojang profile lookup (`CraftServer.java:1914-1940`), stalling the server for an unknown name. *Fix: use `Bukkit.getPlayerExact(value)` at `68` before the `getOfflinePlayer` fallback; in the four callers accept the target when it `is Player` and apply `hasPlayedBefore()` only to the `OfflinePlayer` fallback; move `Pay.kt:52` into the `scope.launch` block.* |
|
||||
| `PostgreSQL.internalAddToPlayerShards` | 🟠 half-transfer loss | Reached from: `/pay`, `K`/`J` `playerPayPlayer` (via `transferBankShards`). **A nested `inTransaction` on the caller's connection commits the outer transaction early.** When the guarded upsert (`PostgreSQL.kt:136-137`) returns no row, `:143` calls `internalGetShardTypeShards(uuid, type, conn)`, which runs `conn.inTransaction` (`:290`); jasync sends `BEGIN`, the query and `COMMIT` on that connection (`ConcreteConnectionBase.kt:22-31`). PostgreSQL 16 warns "there is already a transaction in progress", commits the outer work, and answers the outer `ROLLBACK` with "there is no transaction in progress". In `transferBankShards` the writes are ordered by UUID (`PostgreSQL.kt:191`) and only the sender is pre-checked (`CachingBalanceManager.kt:100-106`), so a receiver with a negative bank fails the guard after the sender's debit is committed: the sender loses the amount, the receiver gets nothing, the sender's cache keeps the old balance (`CachingBalanceManager.kt:113` runs only on success), `/pay` disables the economy (`Pay.kt:142`), and `K.playerPayPlayer` returns `InsufficientBalanceException`, which reads as "nothing moved". A negative bank can be set through `/setbankbalance` (`SetBankBalance.kt:68`, `74`). Repro: `/setbankbal Bob -900`, then `/pay Bob 1` from a sender whose UUID sorts first: the sender's bank drops by 9 and Bob's stays at -900. *Fix: at `143` run the balance `SELECT` on `conn` with `sendPreparedStatement` instead of a nested `inTransaction`, so the guard failure rolls back inside the outer transaction; optionally pre-check the receiver in `CachingBalanceManager.transferBankShards`.* |
|
||||
| `PostgreSQL.getBaltopWithUuid` | 🟠 dead feature | Reached from: `/balancetop <player>`, `/baltop <player>`. **The query is a syntax error.** `PostgreSQL.kt:356-361`: the `target AS (...)` CTE is closed at `357`, `paged AS` follows at `358` with no comma and no opening parenthesis, and a stray `)` remains; PostgreSQL 16 answers `syntax error at or near "paged"`. Every call returns `DatabaseException`, and the command replies "Something went wrong while trying to get the information for balancetop." (`Balancetop.kt:75-80`). Repro: `/baltop Steve` for any player who has joined. *Fix: write `target AS (...), paged AS (...)` at `357-358`.* |
|
||||
| `InventoryExtensions.removeItemWithSameKey` on an `InventorySnapshot` | 🟠 transactions fail with offhand shards | Reached from: `/deposit all`, `/deposit <amount>`, `/pay`, `/compress`, `AutoCompress.compress`, `K`/`J` `consumeFromPlayer`, `K`/`J` `playerPayPlayer`. **The shard remover finds the slot in the 41-slot `getContents()` layout but clears it through the snapshot's 37-slot array, so emptying an offhand shard stack throws.** `InventoryExtensions.kt:44`, `51` and `67-71` read `contents`, which on `InventorySnapshot` is `getContents()` with armor at 36 to 39 and the offhand at 40 (`InventorySnapshot.kt:259-261`); `InventoryExtensions.kt:57-58` then calls `setItem(firstSlot, null)`, which writes the snapshot's own 37-slot array with the offhand at 36 (`InventorySnapshot.kt:51`, `128`). Emptying the offhand stack throws `ArrayIndexOutOfBoundsException: Index 40 out of bounds for length 37`: the coroutine dies before any bank write or restore, the player gets no reply, K throws instead of returning a `Result`, and J throws an undeclared exception. `/deposit all` always empties it (`InventorySnapshotUtils.kt:105-107`; the count includes the offhand, `PlayerInventoryExtensions.kt:10`); `/deposit <amount>`, `/pay`, the API, `/compress` and auto-compress do when the part they take from the offhand empties that stack (`InventorySnapshotUtils.kt:29`, `Compress.kt:171`, `AutoCompress.kt:103`). Repro: hold 5 Diamond Shards in the offhand, `/deposit all`: no reply, nothing deposited, a stack trace in the console. *Fix: read `storageContents` instead of `contents` at `InventoryExtensions.kt:51` and `68`; on `InventorySnapshot` that is the same 37-slot array `setItem` writes (`InventorySnapshot.kt:267`), and on a shulker inventory it is all 27 slots.* |
|
||||
| `InventorySnapshotUtils.removeShards` | 🟠 excess dup on failure | Reached from: `K`/`J` `consumeFromPlayer`, `K`/`J` `playerPayPlayer`, `/pay`, `/deposit <amount>`. **Excess change is credited in its own transaction before the operation's bank write.** `InventorySnapshotUtils.kt:41`, `67` and `74` call `addToBankShards` for change that does not fit in the snapshot. If the later write fails, `restoreTo` is skipped, so the inventory keeps every item while up to 8 shards (80 after a broken diamond block) stay credited, unlogged. The later write is `Deposit.kt:121`, the transfer at `Pay.kt:134-149`, or the debit or transfer at `DiamondBankAPIKotlin.kt:182-186`, `247-252` (J at `DiamondBankAPIJava.kt:243-246`, `306-310`); it fails through a negative receiver, a stale cache after reload, or a database error. Repro: `/setbankbal Bob -900`; Alice has an empty bank and a full inventory with 2 diamonds in one slot; `/pay Bob 0.1`: Alice keeps both diamonds and her bank gains 8 shards. *Fix: return the excess together with the removed count, and let the caller's single bank write carry it.* |
|
||||
| Submitted bank writes and `runOnMainThread { restoreTo }` completion | 🟠 committed money without cache or inventory completion | Reached from: caller cancellation of K `addToPlayerBankShards`, `subtractFromPlayerBankShards`, `consumeFromPlayer`, `playerPayPlayer`; interruption of the same J calls; `onDisable` during those API calls, `/deposit`, `/withdraw`, `/pay`, `/setbankbalance`, `/setbankbal`, `onPlayerJoin` migration and `AutoDeposit.deposit`. **Cancellation or shutdown can abandon cache and inventory completion after a submitted bank write still commits.** `PostgreSQL.kt:73-94`, `109-110` and `169-195` await transactions whose completion jasync owns; addition and transfer bodies also use DiamondBank's `scope.future`. Cancelling the outer awaited future does not cancel the independent inner completion chain (`AsyncObjectPool.kt:66-84`, `ConcreteConnectionBase.kt:23-34`, coroutines `future/Future.kt:168-173`). Once submitted, a successful write can commit while the caller receives failure and skips cache updates (`CachingBalanceManager.kt:50-53`, `72-76`, `108-113`). Inventory-backed commands then have a further cancellable hop (`Deposit.kt:131`, `Withdraw.kt:154`, `Pay.kt:151`), as do K/J consume and pay when called off the main thread (`DiamondBankAPIKotlin.kt:188`, `255`, `DiamondBankAPIJava.kt:249`, `313`, `MainThreadBlock.kt:11-26`). (a) `onDisable` cancels and joins the plugin scope (`DiamondBankOG.kt:148-150`); pending command restores are abandoned, and Paper cancels queued tasks after that (`PaperPluginInstanceManager.java:244`). External K/J calls are outside that scope, so a dropped scheduled task can leave them suspended or blocked holding their locks. Plugins disable before players are saved (`MinecraftServer.java:960`, `971`), so a committed deposit can leave both bank credit and the original items, while a withdrawal can debit the bank without adding items. Auto-deposit has already emptied the entity before launching its credit (`AutoDeposit.kt:39-44`), which cancellation before submission can also abandon. (b) Caller cancellation at a queued restore releases the inventory lock before the task runs; its later lock check fails (`InventorySnapshot.kt:67-69`), and the audit is skipped (`DiamondBankAPIKotlin.kt:195`, `263`, `DiamondBankAPIJava.kt:252`, `317`). Repro: run `/withdraw 10` and stop the server while its committed debit is awaiting the main-thread restore: after restart the bank is lower without the diamonds. *Fix: track in-flight plugin and API transactions, stop accepting new work at shutdown, and protect each accepted write through database outcome, cache, inventory reconciliation and audit with `withContext(NonCancellable)` beginning before submission; include auto-deposit's entity removal and credit in the tracked operation; drain pending main-thread work from a plugin-owned queue in `DiamondBankOG.kt:148-150` until those transactions finish, then close persistence and cancel idle work.* |
|
||||
| Material-based currency counts versus meta-sensitive removal | 🟡 valid currency refused | Reached from: `/compress`, `/compress yes`, `AutoCompress.compress` (every item pickup and container close), `/deposit <amount>`, `/pay`, `K`/`J` `consumeFromPlayer`, `K`/`J` `playerPayPlayer`. **Currency counts include renamed Diamonds and Diamond Blocks that the removal rule refuses when they are needed for the conversion or payment.** `PlayerInventoryExtensions.kt:26`, `31` count by material, while `InventorySnapshot.kt:308-309` matches only stacks similar to the plain `ItemStack` passed to `removeItem`. If ordinary stacks cannot cover the requested removal, compression aborts (`Compress.kt:200-207`, `AutoCompress.kt:125-133`) and payment removal returns too little (`InventorySnapshotUtils.kt:36`, `62`) despite the higher count. `/compress yes` selects the shulker inventory (`Compress.kt:85-88`, `103-109`), whose counters also ignore metadata (`InventoryExtensions.kt:26-32`), while Bukkit removal uses similarity (`CraftInventory.java:232-242`, `359-383`, `ItemStack.java:302-310`). A renamed surplus item does not prevent a conversion covered by plain stacks. No value moves. Repro: rename one Diamond, hold nine Diamonds including it, and run `/compress`: the command reports "Something went wrong"; the same nine items in a held undyed shulker fail `/compress yes`. *Fix: share a material-based remover across snapshots and shulker inventories at `InventorySnapshot.kt:308-309` and `Compress.kt:200`, or make both counter implementations count only stacks accepted by the corresponding removal rule.* |
|
||||
| `PostgreSQL.insertTransactionLog` and `fifo_limit_trigger` | 🟡 audit exceeds 500 rows | Reached from: K/J `addToPlayerBankShards`, `subtractFromPlayerBankShards`, `consumeFromPlayer`, `playerPayPlayer`, `/deposit`, `/withdraw`, `/pay`, `/setbankbalance`, `/setbankbal`. **Concurrent audit inserts can commit more than the guaranteed 500 rows because each trim sees only its own transaction and already committed rows.** `PostgreSQL.kt:416-424` submits each log INSERT in a separate transaction with no shared serialization; `V1__init.sql:17-22` trims using the visible row at offset 500. With 499 committed rows, two concurrent inserts each see 500 and delete nothing, leaving 501 after both commit; the configured pool permits ten concurrent connections (`config.yml:11`, `PostgreSQL.kt:50`). Repro: seed 499 audit rows, start two transactions that each insert a log row before either commits, then commit both: PostgreSQL 16 retains 501. *Fix: at `PostgreSQL.kt:417-419` acquire one common transaction-scoped advisory lock in a separate statement before INSERT, then execute INSERT and trimming in that transaction, so each INSERT starts with a snapshot taken after the previous log transaction commits.* |
|
||||
| `EventManager.queueFor` | 🟡 events lost for a player | Reached from: `K`/`J` `registerEventListener`, and every balance change that emits an event (`PostgreSQL.kt:95`, `112`, `198`). **A listener that throws an `Error` ends that player's consumer coroutine, so no listener receives another event for that player until restart.** `EventManager.kt:28` catches only `Exception`; an `Error` (for example `NoClassDefFoundError` from a listener whose plugin was unloaded, since listeners cannot be unregistered, `:41-43`) ends the `for` loop at `:24`, the `SupervisorJob` absorbs it, `queueFor` keeps returning the dead channel from `computeIfAbsent` (`:21`), and `trySend` (`:38`) keeps buffering into the unlimited channel. Repro: register a listener that calls `TODO()` once, change a player's balance twice: no listener sees the second event. *Fix: at `EventManager.kt:28` catch `Throwable` and rethrow `CancellationException`, or remove the queue from `queues` when its consumer completes.* |
|
||||
| `CommonOperations.diamondsToShards` | 🟡 crash on NaN, wrong or refused amounts | Reached from: `K`/`J` `diamondsToShards` (`DiamondBankAPIKotlin.kt:276`, `279`; `DiamondBankAPIJava.kt:335`, `343`), `/deposit`, `/withdraw`, `/pay`. **Diamond conversion throws on non-finite input and misreads or refuses other representable amounts.** At `CommonOperations.kt:49-53`, conversion goes through `Float.toString()`; `NaN` and `Infinity` contain no `.`, so `split[1]` at `:50` throws; both pass the `amount <= 0` checks (`Deposit.kt:55`, `Withdraw.kt:58`, `Pay.kt:70`) and are converted before `scope.launch` (`Deposit.kt:66`, `Withdraw.kt:69`, `Pay.kt:81`), so `/deposit NaN` reaches the player as "An internal error occurred", and `K.diamondsToShards(NaN)` throws instead of returning a `Result`. Amounts of 1e7 and above format as `1.0E7` and are refused as "more than one decimal digit". From 1048576 up, Float spacing changes the tenth digit silently: `/pay Bob 1048576.3` pays 1048576.2. Negative fractions convert wrongly through the API: `-0.5` yields `+5` and `-1.5` yields `-4`. The Double overloads narrow to Float first (`DiamondBankAPIKotlin.kt:276`, `DiamondBankAPIJava.kt:335`). Repro: `/withdraw NaN`. *Fix: parse numerically at `49-53`: reject non-finite input, take the whole part and the tenth digit from the original string or by `BigDecimal`, apply the sign to both, and have the Double overloads parse the Double directly.* |
|
||||
|
||||
Verified against commit `3df19f636773d113bcb6b58d715d3e6c2df852d3`.
|
||||
@@ -1,121 +0,0 @@
|
||||
Use Cavekit 3.1 with Codex adversarial review, falling back to the Cavekit inspector agent when Codex is unavailable, to produce the follow-up defect chart for DiamondBank-OG after the maintainer's fix pass.
|
||||
|
||||
## Objective
|
||||
|
||||
`DEFECT-CHART.md` in the repository root describes 22 defects verified at commit `0f2c29acdec4160f3db0fb477edbe38115063e6d`. The maintainer has since committed fixes. Produce two files:
|
||||
|
||||
1. A new `DEFECT-CHART.md` containing only the defects that still exist at the current commit, plus any defect the fixes introduced, in the same format as the current file.
|
||||
2. `DEFECT-CHART-DELTA.md`, one line per row of the old chart, stating `fixed`, `partially fixed`, `unfixed` or `regressed`, with the commit and lines that changed it.
|
||||
|
||||
Every claim in both files is settled by reading the source at the current commit. The old chart is a set of hypotheses, not facts: a row is carried forward only if its wrong outcome still reproduces from the current code, and a row is marked fixed only if the causing lines no longer produce it.
|
||||
|
||||
## Inputs
|
||||
|
||||
* `DEFECT-CHART.md` at the start of the run. Read it first. Do not edit it in place; write the new chart to a scratch file and replace it at the end.
|
||||
* `git log 0f2c29a..HEAD` and `git diff 0f2c29a HEAD`. Every file the diff touches is in scope for a fresh audit, not only the lines the old chart named.
|
||||
* Dependency source, read rather than recalled: the Purpur fork at `~/eclipse-workspace/Purpur` (`Purpur-Server/src/main/java`, `Purpur-API/src/main/java`; CraftBukkit under `org/bukkit/craftbukkit/`, Paper DataConverter under `ca/spottedleaf/dataconverter/`), `libs/Utilities-OG/src/main/java`, and the jasync 2.2.4, kotlinx-coroutines 1.10.2 and Lettuce 7.5.1 sources jars in `~/.gradle/caches/modules-2/files-2.1/` (unzip to the scratchpad). A `postgres:16-alpine` podman image is available for validating SQL; use it for any query the fixes changed.
|
||||
|
||||
## Scope
|
||||
|
||||
The six surfaces of the original audit, unchanged:
|
||||
|
||||
1. Public API offline-player compatibility (`api/DiamondBankAPIKotlin` as K, `api/DiamondBankAPIJava` as J)
|
||||
2. Public API general correctness
|
||||
3. Commands (every executor registered in `DiamondBankOG.onEnable`, `util/CommonCommandInterlude`)
|
||||
4. Event listeners and automatic features (`Events.kt`, `autocompress/AutoCompress`, `autodeposit/AutoDeposit`)
|
||||
5. Startup, shutdown, reload, configuration, persistence setup (`onEnable`, `onDisable`, `DiamondBankReload`, `config`, `config.yml`, `plugin.yml`, `Redis.kt`, `PostgreSQL.create`, `CachingBalanceManager.create` and `shutdown`, migrations, service registration)
|
||||
6. Shared internals (everything the first five reach)
|
||||
|
||||
Out of scope: tests under `src/test` (read them for intent only), external API consumers, build scripts.
|
||||
|
||||
## Contracts
|
||||
|
||||
A finding may say the code violates a contract only when the contract comes from: `api/README.md`; KDoc and `@Throws` on public API methods; K versus J parity; the root `README.md` command usage lines and permission list; `plugin.yml` usage and description strings; `config.yml` keys, comments and the `^{ENV}` placeholder syntax; Bukkit, Paper and Purpur runtime guards as found in the dependency source, only where a violation produces a demonstrated wrong outcome; the Guarantees below.
|
||||
|
||||
## Maintainer Requirements
|
||||
|
||||
Guarantees are contracts. Intent items are never contracts and never remediation targets.
|
||||
|
||||
1. Guarantee. Any database error may disable the economy. Do not recommend changing that; distinguish a concrete code defect from the intended disable.
|
||||
2. Intent. `TransactionLock` may block the calling thread in both APIs. The blocking design itself is not a defect.
|
||||
3. Intent. Per-UUID cache, lock and channel state may remain allocated for offline UUIDs.
|
||||
4. Guarantee. The audit log is limited to 500 rows, written after money movement, and its failure may disable the economy.
|
||||
5. Guarantee. Database causes are hidden from API callers.
|
||||
6. Intent. The absence of schema constraints is accepted unless current code demonstrably relies on a missing constraint and produces a live failure.
|
||||
7. Guarantee. `getBaltop` uses a page size of nine.
|
||||
8. Intent. `playerPayPlayer` is intended for an online payer; do not recommend an online check.
|
||||
9. Guarantee. Offline inventory and ender-chest shard values are display-oriented and approximate; ordinary staleness is not a defect.
|
||||
10. Guarantee. Integer overflow may disable the economy.
|
||||
11. Guarantee. Event delivery is fire-and-forget; dropped shutdown events are not defects.
|
||||
12. Guarantee. `subtractFromPlayerBankShards` subtracts only bank shards.
|
||||
13. Intent. A funded database row without playerdata is not assumed realistic.
|
||||
14. Guarantee. `economyDisabled` is the safety net; a money-moving or inventory-rewriting path that starts while the flag is set is reportable. A flag that flips after a path passed its entry check is not.
|
||||
15. Intent. `CommonCommandInterlude` restricts player commands to `world`, `world_nether`, `world_the_end`.
|
||||
16. Intent. Auto-compress and auto-deposit toggles live in Redis, are mutually exclusive, and a Redis failure at startup disables the plugin.
|
||||
17. Intent. Player commands are refused for console and command-block senders.
|
||||
18. Intent. Aliases share one executor.
|
||||
19. Intent. Only the undyed `SHULKER_BOX` material is treated as a currency container, plugin-wide.
|
||||
|
||||
## Phase 1: Re-verify every old row
|
||||
|
||||
For each row of the old chart, in order:
|
||||
|
||||
1. Locate every causing line the row names in the current tree (the numbers will have moved; find the code, not the number).
|
||||
2. Determine whether the fix commit changed it, and whether the wrong outcome still reproduces. Trace the whole path again; do not stop at the changed line.
|
||||
3. Classify: `fixed` (the wrong outcome cannot occur), `partially fixed` (some listed findings or entry points remain), `unfixed` (untouched or the change does not remove the outcome), `regressed` (the change removes the outcome but creates a new wrong outcome, which becomes a new row).
|
||||
4. For a fix, check it against the pitfalls the old chart already flagged, and against these known-bad fix shapes:
|
||||
* `AutoDeposit`: cancelling the pickup event and removing the entity later re-fires `playerTouch` and credits again; refusing on the main thread in J contradicts its KDoc; a restore-time divergence check leaves the money moved; stamping the current `DataVersion` while writing only the `Inventory` list leaves every other field unconverted; a suspending semaphore in `TransactionLock` does not fix `runBlocking` on the main thread.
|
||||
* A fix that replaces `require` with a `Result.failure` must not route that failure into `handleError`, or every bad argument disables the economy.
|
||||
* A fix that moves work off the main thread must keep every Bukkit inventory, entity and scheduler call on the main thread; check each moved line against the async guards in the dependency source.
|
||||
* A fix that adds a synchronous main-thread wait inside a coroutine reintroduces the dispatcher starvation the `TransactionLock` row describes.
|
||||
* A fix that shuts down the previous balance manager on reload must not cut off transactions in flight against it.
|
||||
|
||||
## Phase 2: Fresh audit of changed code
|
||||
|
||||
For every file in `git diff 0f2c29a HEAD`, audit the changed functions and everything they call or are called by as if no chart existed, with the same finding discipline as the original audit: every finding names a wrong outcome (incorrect value, lost or duplicated or misdirected value or items, escaped exception, wrong state, blocked operation), the line that causes it, and a fix that changes that line. Pay particular attention to:
|
||||
|
||||
* New locks, queues, dispatchers or `runBlocking` calls: trace deadlock cycles between the main thread, `Dispatchers.Default` and jasync callbacks.
|
||||
* New inventory write paths: which slots, on which thread, against which snapshot, and what the handlers block in the window.
|
||||
* New SQL: run it against the migration schema in the podman image.
|
||||
* New Redis usage: connection lifetime, exception handling, thread.
|
||||
* Changed `plugin.yml`, `config.yml`, `YamlConfig`: required keys that an existing deployed config lacks (an upgrade must not disable the plugin), and dependency ordering.
|
||||
* Any new recipe, event handler or listener registration: lifecycle across disable, re-enable and `/minecraft:reload`.
|
||||
|
||||
A defect the fixes introduced is a row in the new chart regardless of whether the old chart covered its area.
|
||||
|
||||
## Phase 3: Merge and self-check
|
||||
|
||||
Build the new chart: carried-forward rows rewritten from the current trace (current line numbers, current mechanism), plus new rows. Then check every row against the three questions: what is the wrong outcome, which line causes it, does the fix change that line. Merge rows whose fixes overlap, as the current chart does; a row that only restates another row's cause through a different entry point is deleted.
|
||||
|
||||
## Adversarial Review
|
||||
|
||||
Hand the merged draft to Codex:
|
||||
|
||||
```
|
||||
codex exec --skip-git-repo-check --model gpt-5.6-sol -c model_reasoning_effort=xhigh -o <scratch>/codex-review.md "<review instructions followed by the full draft>"
|
||||
```
|
||||
|
||||
Confirm the output header shows `model: gpt-5.6-sol` and `reasoning effort: xhigh` before accepting any findings. Give the reviewer the draft, the six surfaces, the Contracts, the Maintainer Requirements, the finding discipline, and the source locations; do not give it the old chart, the delta, or the fix pitfalls list. Ask it to falsify the draft in six buckets: wrong claims, missing entry points or defects (including regressions the fixes introduced), overstated triggers or impact, stale line references, derivative or invented findings, misplaced findings.
|
||||
|
||||
Codex is unavailable when `command -v codex` finds nothing, when `codex exec` exits with an authentication, network, quota or unknown-model error and one retry fails the same way, or when the header shows another model or effort after one rerun. A quota error names the next reset time; if that time is within the session, wait for it and retry once. Otherwise run the Cavekit 3 inspector (`ck:inspector`, opus) with the same inputs and record the exact Codex error. Never skip the review; if both reviewers are unavailable, stop and report that.
|
||||
|
||||
Verify every reviewer finding against the current source before accepting it. Record the counts of accepted and rejected findings.
|
||||
|
||||
## Output format
|
||||
|
||||
`DEFECT-CHART.md`:
|
||||
|
||||
* Title line.
|
||||
* The severity key as a three-row table, verbatim from the current chart: 🔴 critical, 🟠 serious, 🟡 minor, with their meanings.
|
||||
* Six sections in surface order, each a three-column table `Entry point | Severity | Note` (surface 6: `Component | Severity | Note`). Rows ordered by severity within a section. A section with no rows has the heading and no table.
|
||||
* Each Note: a bold one-sentence statement of what is wrong; the mechanism with current `File.kt:line` references (write `command/AutoCompress.kt` and `command/AutoDeposit.kt` for the command classes, which share basenames with the feature classes); a one-line repro where one exists; then the fix in italics as `*Fix: ...*`. Surface 6 notes begin with `Reached from:` listing every entry point.
|
||||
* Severity cell holds the glyph and a short plain-language impact tag (for example `🔴 item dup`, `🟠 dead feature`, `🟡 wrong audit value`). No verdict words, no priority codes.
|
||||
* No header sentences listing what is correct, no methodology, no reviewer discussion, no mention of the previous chart inside rows. Close with one line: `Verified against commit <full hash>.`
|
||||
* Concise prose, complete sentences, no em dashes or en dashes, no rows for intentional limitations or clean refusals.
|
||||
|
||||
`DEFECT-CHART-DELTA.md`:
|
||||
|
||||
* One line per old row: the old row's first cell, its status, and the commit hash plus the lines that changed it (or `no change`). For `partially fixed`, name what remains. For `regressed`, name the new row.
|
||||
* One closing line: the current commit, the reviewer used (`gpt-5.6-sol` at `xhigh`, or `ck:inspector` with the exact Codex error), and the accepted and rejected reviewer finding counts.
|
||||
|
||||
Replace the old `DEFECT-CHART.md` only after both files pass the self-check and the review.
|
||||
@@ -1,24 +1,674 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
Preamble
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,13 +1,13 @@
|
||||
> [!NOTE]
|
||||
> An experimental branch that implements Shards is available [here](https://github.com/SKBotNL/DiamondBank-OG/tree/shards).
|
||||
# DiamondBank-OG
|
||||
|
||||
A FOSS and reliable Diamond Economy plugin for Spigot inspired by [The Diamond Bank](https://www.spigotmc.org/resources/the-diamond-bank.72020/).\
|
||||
Maintained for TrueOG Network.
|
||||
|
||||
**For developers: see the [API README](src/main/kotlin/net/trueog/diamondbankog/api/README.md).**
|
||||
A free and open source Diamond Economy plugin for Spigot inspired by [The Diamond Bank](https://www.spigotmc.org/resources/the-diamond-bank.72020/).\
|
||||
Maintained for TrueOG.
|
||||
|
||||
## PostgreSQL
|
||||
This plugin uses PostgreSQL which needs to be set up first, the [PostgreSQL page](https://wiki.archlinux.org/title/PostgreSQL#Initial_configuration) on the Arch Linux wiki or the [readme of the Postgres Docker container](https://hub.docker.com/_/postgres) can be used to see how.\
|
||||
The PostgreSQL URL, user and password that should be used are configurable in the config.
|
||||
This plugin uses PostgreSQL which needs to be set up first. The Arch Linux wiki has a [pretty good guide](https://wiki.archlinux.org/title/PostgreSQL#Initial_configuration).\
|
||||
The PostgreSQL URL, user, password and the table that should be used are configurable in the config.
|
||||
|
||||
## User Commands
|
||||
|
||||
@@ -20,8 +20,7 @@ The PostgreSQL URL, user and password that should be used are configurable in th
|
||||
|
||||
## Admin Commands
|
||||
|
||||
/setbankbalance \<player name or player uuid> \<balance>\
|
||||
/dbogtoggledebug
|
||||
/setbankbalance \<player name or player uuid> \<balance>
|
||||
|
||||
## Permissions
|
||||
diamondbank-og.balance\
|
||||
@@ -30,12 +29,4 @@ diamondbank-og.balancetop\
|
||||
diamondbank-og.deposit\
|
||||
diamondbank-og.withdraw\
|
||||
diamondbank-og.pay\
|
||||
diamondbank-og.compress\
|
||||
diamondbank-og.setbankbalance\
|
||||
diamondbank-og.admin\
|
||||
diamondbank-og.debug
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of **The Unlicense** (public domain dedication). See `LICENSE`.
|
||||
The Unlicense is used with permission of the original author.<sup>[1](https://github.com/andromdaa/UnityTemplate/issues/1#issuecomment-3342032195)</sup> <sup>[2](https://web.archive.org/web/20250927211153/https://github.com/andromdaa/UnityTemplate/issues/1#issuecomment-3342032195)</sup> <sup>[3](https://git.trueog.net/trueog/DiamondBank-OG/pulls/13#issuecomment-28)</sup> <sup>[4](https://web.archive.org/web/20260119131305/https://github.com/true-og/DiamondBank-OG/pull/13#issuecomment-3678843384)</sup>
|
||||
diamondbank-og.setbankbalance
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
# Fetch all submodule content.
|
||||
git submodule update --force --recursive --init
|
||||
+72
-114
@@ -1,123 +1,81 @@
|
||||
import java.io.BufferedReader
|
||||
|
||||
/* ------------------------------ Plugins ------------------------------ */
|
||||
plugins {
|
||||
id("java") // Import Java plugin.
|
||||
id("java-library") // Import Java Library plugin.
|
||||
id("com.diffplug.spotless") version "8.1.0" // Import Spotless plugin.
|
||||
id("com.gradleup.shadow") version "8.3.9" // Import Shadow plugin.
|
||||
eclipse // Import Eclipse plugin.
|
||||
kotlin("jvm") version "2.1.21" // Import Kotlin JVM plugin.
|
||||
id("org.jetbrains.kotlin.jvm") version "2.1.21"
|
||||
id("com.gradleup.shadow") version "8.3.6"
|
||||
id("maven-publish")
|
||||
id("eclipse")
|
||||
}
|
||||
|
||||
group = "net.trueog.diamondbankog"
|
||||
version = "1.1"
|
||||
|
||||
val apiVersion = "1.19"
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenPublication") {
|
||||
groupId = "net.trueog.diamondbankog"
|
||||
artifactId = "DiamondBankOG"
|
||||
version = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<ProcessResources>("processResources") {
|
||||
val props = mapOf(
|
||||
"version" to version,
|
||||
"apiVersion" to apiVersion,
|
||||
)
|
||||
|
||||
filesMatching("plugin.yml") {
|
||||
expand(props)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
maven {
|
||||
url = uri("https://repo.purpurmc.org/snapshots")
|
||||
}
|
||||
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT")
|
||||
implementation("com.github.christianniehaus:Utilities-OG:e9ebc26c1f")
|
||||
implementation("com.github.jasync-sql:jasync-postgresql:2.2.4")
|
||||
|
||||
implementation("io.sentry:sentry:8.8.0")
|
||||
implementation("io.sentry:sentry-kotlin-extensions:8.9.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
}
|
||||
|
||||
tasks.shadowJar {
|
||||
minimize()
|
||||
}
|
||||
|
||||
tasks.shadowJar.configure {
|
||||
archiveClassifier.set("")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn("shadowJar")
|
||||
}
|
||||
|
||||
tasks.jar.configure {
|
||||
archiveClassifier.set("part")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
/* ---------------------------- Java / Kotlin -------------------------- */
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
vendor = JvmVendorSpec.GRAAL_VM
|
||||
}
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(17) }
|
||||
|
||||
/* ----------------------------- Metadata ------------------------------ */
|
||||
val commitHash =
|
||||
Runtime.getRuntime().exec(arrayOf("git", "rev-parse", "--short=10", "HEAD")).let { process ->
|
||||
process.waitFor()
|
||||
val output = process.inputStream.use { it.bufferedReader().use(BufferedReader::readText) }
|
||||
process.destroy()
|
||||
output.trim()
|
||||
}
|
||||
|
||||
group = "net.trueog.diamondbankog" // Declare bundle identifier.
|
||||
|
||||
val apiVersion = "1.19" // Declare plugin version (will be in .jar).
|
||||
|
||||
version = "$apiVersion-$commitHash" // Declare minecraft server target version.
|
||||
|
||||
/* ----------------------------- Resources ----------------------------- */
|
||||
tasks.named<ProcessResources>("processResources") {
|
||||
val props = mapOf("version" to version, "apiVersion" to apiVersion)
|
||||
inputs.properties(props) // Indicates to rerun if version changes.
|
||||
filesMatching("plugin.yml") { expand(props) }
|
||||
from("LICENSE") { into("/") } // Bundle licenses into jarfiles.
|
||||
}
|
||||
|
||||
/* ---------------------------- Repos ---------------------------------- */
|
||||
repositories {
|
||||
mavenCentral() // Import the Maven Central Maven Repository.
|
||||
gradlePluginPortal() // Import the Gradle Plugin Portal Maven Repository.
|
||||
maven { url = uri("https://repo.purpurmc.org/snapshots") } // Import the PurpurMC Maven Repository.
|
||||
maven { url = uri("https://jitpack.io") } // Import the Jitpack Maven Repository.
|
||||
}
|
||||
|
||||
/* ---------------------- Java project deps ---------------------------- */
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged.
|
||||
compileOnly("net.luckperms:api:5.5") // Import the LuckPerms API.
|
||||
|
||||
compileOnlyApi(project(":libs:Utilities-OG")) // Import TrueOG Network Utilities-OG Java API (from source).
|
||||
|
||||
implementation("io.lettuce:lettuce-core:7.5.1.RELEASE") // Import the Lettuce API for keydb.
|
||||
|
||||
implementation("org.flywaydb:flyway-core:12.3.0")
|
||||
implementation("org.flywaydb:flyway-database-postgresql:12.3.0")
|
||||
implementation("org.postgresql:postgresql:42.7.9")
|
||||
implementation("com.github.jasync-sql:jasync-postgresql:2.2.4") // Import the jasync Postgres API.
|
||||
|
||||
implementation("it.unimi.dsi:fastutil-core:8.5.18")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") // Import the Kotlin standard library.
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
implementation("org.slf4j:slf4j-api:2.0.17") // Bundle a relocated SLF4J API for shaded dependencies.
|
||||
implementation("org.slf4j:slf4j-nop:2.0.17") // Provide a relocated no-op SLF4J backend to avoid provider warnings.
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.3")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params:6.0.3")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.3")
|
||||
testImplementation("io.mockk:mockk:1.14.9")
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT")
|
||||
testImplementation("net.luckperms:api:5.5")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
|
||||
}
|
||||
|
||||
tasks.test { useJUnitPlatform() }
|
||||
|
||||
/* ---------------------- Reproducible jars ---------------------------- */
|
||||
tasks.withType<AbstractArchiveTask>().configureEach { // Ensure reproducible .jars
|
||||
isPreserveFileTimestamps = false
|
||||
isReproducibleFileOrder = true
|
||||
}
|
||||
|
||||
/* ----------------------------- Shadow -------------------------------- */
|
||||
tasks.shadowJar {
|
||||
archiveClassifier.set("") // Use empty string instead of null.
|
||||
isEnableRelocation = true
|
||||
relocationPrefix = "${project.group}.shadow"
|
||||
mergeServiceFiles()
|
||||
}
|
||||
|
||||
tasks.jar { archiveClassifier.set("part") } // Applies to root jarfile only.
|
||||
|
||||
tasks.build { dependsOn(tasks.spotlessApply, tasks.shadowJar) } // Build depends on spotless and shadow.
|
||||
|
||||
/* --------------------------- Javac opts ------------------------------- */
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.compilerArgs.add("-Xlint:deprecation") // Trigger deprecation warning messages.
|
||||
options.encoding = "UTF-8" // Use UTF-8 file encoding.
|
||||
}
|
||||
|
||||
/* ----------------------------- Auto Formatting ------------------------ */
|
||||
spotless {
|
||||
kotlin { ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } }
|
||||
kotlinGradle {
|
||||
ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } // JetBrains Kotlin formatting.
|
||||
target("build.gradle.kts", "settings.gradle.kts") // Gradle files to format.
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("spotlessCheck") {
|
||||
dependsOn("spotlessApply") // Run spotless before checking if spotless ran.
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
jdk:
|
||||
- 17.0.9
|
||||
before_install:
|
||||
- sdk install java 17.0.9-graalce
|
||||
- sdk use java 17.0.9-graalce
|
||||
install:
|
||||
- ./gradlew build publishToMavenLocal
|
||||
Submodule libs/Utilities-OG deleted from 8b5c86cc9c.
+3
-13
@@ -1,15 +1,5 @@
|
||||
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version ("1.0.0") }
|
||||
|
||||
rootProject.name = "DiamondBank-OG"
|
||||
|
||||
ProcessBuilder("sh", "bootstrap.sh").directory(rootDir).inheritIO().start().let {
|
||||
if (it.waitFor() != 0) throw GradleException("bootstrap.sh failed")
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0")
|
||||
}
|
||||
|
||||
file("libs")
|
||||
.listFiles()
|
||||
?.filter { it.isDirectory && !it.name.startsWith(".") }
|
||||
?.forEach { dir ->
|
||||
include(":libs:${dir.name}")
|
||||
project(":libs:${dir.name}").projectDir = dir
|
||||
}
|
||||
rootProject.name = "DiamondBank-OG"
|
||||
@@ -0,0 +1,75 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration
|
||||
import java.io.File
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
object Config {
|
||||
lateinit var prefix: String
|
||||
var sentryEnabled by Delegates.notNull<Boolean>()
|
||||
lateinit var sentryDsn: String
|
||||
lateinit var postgresUrl: String
|
||||
lateinit var postgresUser: String
|
||||
lateinit var postgresPassword: String
|
||||
lateinit var postgresTable: String
|
||||
|
||||
fun load(): Boolean {
|
||||
val file = File(DiamondBankOG.plugin.dataFolder, "config.yml")
|
||||
if (!file.exists()) {
|
||||
DiamondBankOG.plugin.saveDefaultConfig()
|
||||
}
|
||||
val config = YamlConfiguration.loadConfiguration(file)
|
||||
config.save(file)
|
||||
|
||||
try {
|
||||
prefix = config.get("prefix") as String
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"prefix\" as a string")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
sentryEnabled = config.get("sentryEnabled") as Boolean
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"sentryEnabled\" as a boolean")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
sentryDsn = config.get("sentryDsn") as String
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"sentryDsn\" as a string")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
postgresUrl = config.get("postgresUrl") as String
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"postgresUrl\" as a string")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
postgresUser = config.get("postgresUser") as String
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"postgresUser\" as a string")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
postgresPassword = config.get("postgresPassword") as String
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"postgresPassword\" as a string")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
postgresTable = config.get("postgresTable") as String
|
||||
} catch (_: Exception) {
|
||||
DiamondBankOG.plugin.logger.severe("Failed to parse config option \"postgresTable\" as a string")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.future.future
|
||||
import org.bukkit.Bukkit
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class DiamondBankAPI(private var postgreSQL: PostgreSQL) {
|
||||
@Suppress("unused")
|
||||
fun addToPlayerBankBalance(uuid: UUID, amount: Int): CompletableFuture<Boolean?> {
|
||||
if (DiamondBankOG.economyDisabled) return GlobalScope.future { null }
|
||||
|
||||
return GlobalScope.future { postgreSQL.addToPlayerBalance(uuid, amount, PostgreSQL.BalanceType.BANK_BALANCE) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun subtractFromPlayerBankBalance(uuid: UUID, amount: Int): CompletableFuture<Boolean?> {
|
||||
if (DiamondBankOG.economyDisabled) return GlobalScope.future { null }
|
||||
|
||||
return GlobalScope.future {
|
||||
postgreSQL.subtractFromPlayerBalance(
|
||||
uuid,
|
||||
amount,
|
||||
PostgreSQL.BalanceType.BANK_BALANCE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getPlayerBalance(uuid: UUID, type: PostgreSQL.BalanceType): CompletableFuture<PostgreSQL.PlayerBalance?> {
|
||||
if (DiamondBankOG.economyDisabled) return GlobalScope.future { null }
|
||||
|
||||
return GlobalScope.future { postgreSQL.getPlayerBalance(uuid, type) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getPlayerTotalBalance(uuid: UUID): CompletableFuture<Int?> {
|
||||
if (DiamondBankOG.economyDisabled) return GlobalScope.future { null }
|
||||
|
||||
return GlobalScope.future {
|
||||
val balance = postgreSQL.getPlayerBalance(uuid, PostgreSQL.BalanceType.ALL)
|
||||
balance.bankBalance!! + balance.inventoryBalance!! + balance.enderChestBalance!!
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun withdrawFromPlayer(uuid: UUID, amount: Int): CompletableFuture<Boolean?> {
|
||||
if (DiamondBankOG.economyDisabled) return GlobalScope.future { null }
|
||||
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
if (!player.hasPlayedBefore()) return GlobalScope.future { true }
|
||||
if (!player.isOnline) return GlobalScope.future { true }
|
||||
val playerPlayer = player.player ?: return GlobalScope.future { true }
|
||||
|
||||
return GlobalScope.future { Helper.withdrawFromPlayer(playerPlayer, amount) == null }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun payPlayer(senderUuid: UUID, receiverUuid: UUID, amount: Int): CompletableFuture<Boolean?> {
|
||||
if (DiamondBankOG.economyDisabled) return GlobalScope.future { null }
|
||||
|
||||
val sender = Bukkit.getPlayer(senderUuid) ?: Bukkit.getOfflinePlayer(senderUuid)
|
||||
if (!sender.hasPlayedBefore()) return GlobalScope.future { true }
|
||||
if (!sender.isOnline) return GlobalScope.future { true }
|
||||
val senderPlayer = sender.player ?: return GlobalScope.future { true }
|
||||
|
||||
val receiver = Bukkit.getPlayer(receiverUuid) ?: Bukkit.getOfflinePlayer(receiverUuid)
|
||||
if (!receiver.hasPlayedBefore()) return GlobalScope.future { true }
|
||||
|
||||
return GlobalScope.future {
|
||||
Helper.withdrawFromPlayer(senderPlayer, amount) ?: GlobalScope.future { true }
|
||||
|
||||
val error = postgreSQL.addToPlayerBalance(
|
||||
receiver.uniqueId,
|
||||
amount,
|
||||
PostgreSQL.BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
sender.uniqueId,
|
||||
Helper.PostgresFunction.ADD_TO_PLAYER_BALANCE, amount, PostgreSQL.BalanceType.BANK_BALANCE,
|
||||
null, "pay"
|
||||
)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
sealed class DiamondBankException(message: String) : Exception(message) {
|
||||
class EconomyDisabledException : DiamondBankException("Economy is disabled")
|
||||
|
||||
class InvalidPlayerException : DiamondBankException("Invalid player")
|
||||
|
||||
class SenderEqualToReceiverException : DiamondBankException("Sender is equal to the receiver")
|
||||
|
||||
class DatabaseException(message: String) : DiamondBankException(message)
|
||||
|
||||
class InsufficientBalanceException(val balance: Long) : DiamondBankException("Insufficient balance")
|
||||
|
||||
class InsufficientFundsException(val short: Long) : DiamondBankException("Insufficient funds")
|
||||
|
||||
class InvalidArgumentException : Exception()
|
||||
}
|
||||
|
||||
sealed class DiamondBankRuntimeException(message: String) : RuntimeException(message) {
|
||||
class MoreThanOneDecimalDigitRuntimeException : DiamondBankException("More than one decimal digit")
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class DiamondBankHelp : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"<gray>---- ${config.prefix}<reset> Help <gray>----<reset>\n" +
|
||||
"<bold>How does the <aqua>Diamonds<white> currency work?<reset>\n" +
|
||||
"The currency on TrueOG Network is based on Diamonds. The smallest form is Diamond Shards, followed by Diamonds, and the largest form is Diamond Blocks. 1 Diamond Block equals 9 Diamonds, and 1 Diamond equals 9 Diamond Shards. When converting Diamond Shards to Diamonds, any amount ending with .9 is rounded up to the next whole Diamond. Some examples:\n" +
|
||||
"- 1.8 Diamonds = 1 Diamond + 8 Shards\n" +
|
||||
"- 1.9 Diamonds = 1 Diamonds + 9 Shards (equal to 2 Diamonds)\n" +
|
||||
"- 2.0 Diamonds = 2 Diamonds\n" +
|
||||
"- 2.1 Diamonds = 2 Diamonds + 1 Shard"
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,127 +1,65 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import io.sentry.Sentry
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver
|
||||
import net.kyori.adventure.text.minimessage.tag.standard.StandardTags
|
||||
import net.luckperms.api.LuckPerms
|
||||
import net.trueog.diamondbankog.api.DiamondBankAPIJava
|
||||
import net.trueog.diamondbankog.api.DiamondBankAPIKotlin
|
||||
import net.trueog.diamondbankog.api.EventManager
|
||||
import net.trueog.diamondbankog.autocompress.command.AutoCompress
|
||||
import net.trueog.diamondbankog.autodeposit.command.AutoDeposit
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.CachingBalanceManager
|
||||
import net.trueog.diamondbankog.balance.command.Balance
|
||||
import net.trueog.diamondbankog.balance.command.Balancetop
|
||||
import net.trueog.diamondbankog.balance.command.SetBankBalance
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.config.YamlConfig
|
||||
import net.trueog.diamondbankog.config.command.DiamondBankReload
|
||||
import net.trueog.diamondbankog.economy.command.DisableEconomy
|
||||
import net.trueog.diamondbankog.economy.command.EnableEconomy
|
||||
import net.trueog.diamondbankog.persistence.Redis
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.transaction.command.Compress
|
||||
import net.trueog.diamondbankog.transaction.command.Deposit
|
||||
import net.trueog.diamondbankog.transaction.command.Pay
|
||||
import net.trueog.diamondbankog.transaction.command.Withdraw
|
||||
import net.trueog.diamondbankog.commands.*
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.plugin.ServicePriority
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.util.*
|
||||
|
||||
internal class DiamondBankOG : JavaPlugin() {
|
||||
class DiamondBankOG : JavaPlugin() {
|
||||
companion object {
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
lateinit var plugin: DiamondBankOG
|
||||
lateinit var config: Config
|
||||
lateinit var eventManager: EventManager
|
||||
lateinit var balanceManager: BalanceManager
|
||||
lateinit var redis: Redis
|
||||
lateinit var luckPerms: LuckPerms
|
||||
|
||||
var debug: Boolean = false
|
||||
|
||||
fun isRedisInitialized() = ::redis.isInitialized
|
||||
|
||||
fun isBalanceManagerInitialized() = ::balanceManager.isInitialized
|
||||
|
||||
var mm =
|
||||
MiniMessage.builder()
|
||||
.tags(
|
||||
TagResolver.builder()
|
||||
.resolver(StandardTags.color())
|
||||
.resolver(StandardTags.decorations())
|
||||
.resolver(StandardTags.rainbow())
|
||||
.resolver(StandardTags.reset())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val transactionLock = TransactionLock()
|
||||
lateinit var postgreSQL: PostgreSQL
|
||||
fun isPostgreSQLInitialised() = ::postgreSQL.isInitialized
|
||||
var mm = MiniMessage.builder()
|
||||
.tags(
|
||||
TagResolver.builder()
|
||||
.resolver(StandardTags.color())
|
||||
.resolver(StandardTags.reset())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
val blockInventoryFor = mutableListOf<UUID>()
|
||||
val blockCommandsWithInventoryActionsFor = mutableListOf<UUID>()
|
||||
var sentryEnabled: Boolean = false
|
||||
var economyDisabled: Boolean = false
|
||||
}
|
||||
|
||||
override fun onEnable() {
|
||||
plugin = this
|
||||
|
||||
Companion.config =
|
||||
YamlConfig.create()
|
||||
?: run {
|
||||
Bukkit.getPluginManager().disablePlugin(this)
|
||||
return
|
||||
}
|
||||
|
||||
eventManager = EventManager()
|
||||
|
||||
balanceManager =
|
||||
CachingBalanceManager.create()
|
||||
?: run {
|
||||
Bukkit.getPluginManager().disablePlugin(this)
|
||||
return
|
||||
}
|
||||
|
||||
redis = Redis()
|
||||
if (redis.testConnection()) {
|
||||
logger.severe("Could not connect to Redis")
|
||||
if (Config.load()) {
|
||||
Bukkit.getPluginManager().disablePlugin(this)
|
||||
return
|
||||
}
|
||||
|
||||
val luckPermsProvider = Bukkit.getServicesManager().getRegistration(LuckPerms::class.java)
|
||||
if (luckPermsProvider == null) {
|
||||
logger.severe("Luckperms API is null, quitting...")
|
||||
if (Config.sentryEnabled) {
|
||||
try {
|
||||
Sentry.init { options ->
|
||||
options.dsn = Config.sentryDsn
|
||||
}
|
||||
sentryEnabled = true
|
||||
} catch (_: Exception) {
|
||||
sentryEnabled = false
|
||||
this.logger.severe("Could not initialise Sentry. The Sentry(-compatible) DSN in your config might be invalid.")
|
||||
}
|
||||
}
|
||||
|
||||
postgreSQL = PostgreSQL()
|
||||
try {
|
||||
postgreSQL.initDB()
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.info(e.toString())
|
||||
Bukkit.getPluginManager().disablePlugin(this)
|
||||
return
|
||||
}
|
||||
luckPerms = luckPermsProvider.provider
|
||||
|
||||
this.server.pluginManager.registerEvents(Events(), this)
|
||||
|
||||
setUpCommands()
|
||||
|
||||
Shard.createCraftingRecipes()
|
||||
|
||||
val diamondBankAPIJava = DiamondBankAPIJava()
|
||||
this.server.servicesManager.register(
|
||||
DiamondBankAPIJava::class.java,
|
||||
diamondBankAPIJava,
|
||||
this,
|
||||
ServicePriority.Normal,
|
||||
)
|
||||
|
||||
val diamondBankAPIKotlin = DiamondBankAPIKotlin()
|
||||
this.server.servicesManager.register(
|
||||
DiamondBankAPIKotlin::class.java,
|
||||
diamondBankAPIKotlin,
|
||||
this,
|
||||
ServicePriority.Normal,
|
||||
)
|
||||
}
|
||||
|
||||
fun setUpCommands() {
|
||||
this.getCommand("deposit")?.setExecutor(Deposit())
|
||||
this.getCommand("withdraw")?.setExecutor(Withdraw())
|
||||
this.getCommand("setbankbalance")?.setExecutor(SetBankBalance())
|
||||
@@ -131,32 +69,15 @@ internal class DiamondBankOG : JavaPlugin() {
|
||||
this.getCommand("baltop")?.setExecutor(Balancetop())
|
||||
this.getCommand("balance")?.setExecutor(Balance())
|
||||
this.getCommand("bal")?.setExecutor(Balance())
|
||||
this.getCommand("compress")?.setExecutor(Compress())
|
||||
this.getCommand("autocompress")?.setExecutor(AutoCompress())
|
||||
this.getCommand("autodeposit")?.setExecutor(AutoDeposit())
|
||||
|
||||
this.getCommand("diamondbankreload")?.setExecutor(DiamondBankReload())
|
||||
this.getCommand("diamondbankhelp")?.setExecutor(DiamondBankHelp())
|
||||
|
||||
this.getCommand("enableeconomy")?.setExecutor(EnableEconomy())
|
||||
this.getCommand("disableeconomy")?.setExecutor(DisableEconomy())
|
||||
|
||||
this.getCommand("dbogtoggledebug")?.setExecutor(ToggleDebug())
|
||||
val diamondBankAPI = DiamondBankAPI(postgreSQL)
|
||||
this.server.servicesManager.register(DiamondBankAPI::class.java, diamondBankAPI, this,
|
||||
ServicePriority.Normal)
|
||||
}
|
||||
|
||||
override fun onDisable() {
|
||||
scope.cancel()
|
||||
|
||||
runBlocking { scope.coroutineContext[Job]?.join() }
|
||||
|
||||
if (isRedisInitialized()) {
|
||||
redis.shutdown()
|
||||
}
|
||||
|
||||
if (isBalanceManagerInitialized()) {
|
||||
balanceManager.shutdown()
|
||||
}
|
||||
|
||||
transactionLock.removeAllLocks()
|
||||
if (isPostgreSQLInitialised()) postgreSQL.pool.disconnect().get()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +1,11 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.redis
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
|
||||
import net.trueog.diamondbankog.autocompress.AutoCompress.compress
|
||||
import net.trueog.diamondbankog.autodeposit.AutoDeposit.deposit
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.isInventoryLocked
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countTotal
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countTotal
|
||||
import net.trueog.diamondbankog.Helper.PostgresFunction
|
||||
import net.trueog.diamondbankog.Helper.countDiamonds
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.NamespacedKey
|
||||
import org.bukkit.entity.Player
|
||||
@@ -24,396 +13,273 @@ import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.EventPriority
|
||||
import org.bukkit.event.Listener
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent
|
||||
import org.bukkit.event.inventory.InventoryClickEvent
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent
|
||||
import org.bukkit.event.inventory.InventoryType
|
||||
import org.bukkit.event.inventory.PrepareItemCraftEvent
|
||||
import org.bukkit.event.player.*
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.event.player.PlayerDropItemEvent
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.persistence.PersistentDataType
|
||||
import org.bukkit.scheduler.BukkitRunnable
|
||||
|
||||
internal class Events : Listener {
|
||||
private val vanillaWorlds = arrayOf("world", "world_nether", "world_the_end")
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class Events : Listener {
|
||||
@EventHandler
|
||||
fun onPlayerJoin(event: PlayerJoinEvent) {
|
||||
if (economyDisabled) {
|
||||
event.player.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member.")
|
||||
)
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
event.player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return
|
||||
}
|
||||
|
||||
val worldName = event.player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") return
|
||||
|
||||
scope.launch {
|
||||
val hasEntry =
|
||||
balanceManager.hasEntry(event.player.uniqueId).getOrElse {
|
||||
handleError(it)
|
||||
return@launch
|
||||
}
|
||||
if (!hasEntry) {
|
||||
val legacyBalance =
|
||||
event.player.persistentDataContainer.get(
|
||||
NamespacedKey.fromString("diamondbank:balance")!!,
|
||||
PersistentDataType.DOUBLE,
|
||||
)
|
||||
if (legacyBalance != null) {
|
||||
balanceManager
|
||||
.setPlayerShards(event.player.uniqueId, legacyBalance.toLong() * 9, ShardType.BANK)
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
event.player.persistentDataContainer.remove(NamespacedKey.fromString("diamondbank:balance")!!)
|
||||
event.player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>Your old balance has been successfully migrated to DiamondBank-OG!"
|
||||
)
|
||||
GlobalScope.launch {
|
||||
if (!DiamondBankOG.postgreSQL.entryExists(event.player.uniqueId)) {
|
||||
val oldBalance = event.player.persistentDataContainer.get(
|
||||
NamespacedKey.fromString("diamondbank:balance")!!,
|
||||
PersistentDataType.DOUBLE
|
||||
)
|
||||
}
|
||||
|
||||
transactionLock.withLockSuspend(event.player.uniqueId) {
|
||||
val (inventoryShards, enderChestShards) =
|
||||
runOnMainThread { Pair(event.player.inventory.countTotal(), event.player.enderChest.countTotal()) }
|
||||
balanceManager.setPlayerShards(event.player.uniqueId, inventoryShards, ShardType.INVENTORY).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
}
|
||||
balanceManager
|
||||
.setPlayerShards(event.player.uniqueId, enderChestShards, ShardType.ENDER_CHEST)
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
if (oldBalance != null) {
|
||||
val error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
event.player.uniqueId,
|
||||
oldBalance.toInt(),
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
event.player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
oldBalance.toInt(),
|
||||
BalanceType.BANK_BALANCE,
|
||||
null,
|
||||
"onPlayerJoin"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
balanceManager.cacheForPlayer(event.player.uniqueId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
fun onPlayerQuit(event: PlayerQuitEvent) {
|
||||
val worldName = event.player.world.name
|
||||
val isVanillaWorld = vanillaWorlds.contains(worldName)
|
||||
scope.launch {
|
||||
transactionLock.withLockSuspend(event.player.uniqueId) {
|
||||
if (isVanillaWorld) {
|
||||
val (inventoryShards, enderChestShards) =
|
||||
runOnMainThread {
|
||||
Pair(event.player.inventory.countTotal(), event.player.enderChest.countTotal())
|
||||
}
|
||||
balanceManager
|
||||
.setPlayerShards(event.player.uniqueId, inventoryShards, ShardType.INVENTORY)
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
}
|
||||
balanceManager
|
||||
.setPlayerShards(event.player.uniqueId, enderChestShards, ShardType.ENDER_CHEST)
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
}
|
||||
event.player.persistentDataContainer.remove(NamespacedKey.fromString("diamondbank:balance")!!)
|
||||
event.player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>Your old balance has successfully been migrated to DiamondBank-OG!"))
|
||||
}
|
||||
|
||||
balanceManager.removeCacheForPlayer(event.player.uniqueId)
|
||||
// We don't need to set the player bank balance when we don't migrate since an entry will be created with the code below
|
||||
}
|
||||
|
||||
val inventoryDiamonds = event.player.inventory.countDiamonds()
|
||||
var error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
event.player.uniqueId,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
event.player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE,
|
||||
null,
|
||||
"onPlayerJoin"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val enderChestDiamonds = event.player.enderChest.countDiamonds()
|
||||
error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
event.player.uniqueId,
|
||||
enderChestDiamonds,
|
||||
BalanceType.ENDER_CHEST_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
event.player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
enderChestDiamonds,
|
||||
BalanceType.ENDER_CHEST_BALANCE,
|
||||
null,
|
||||
"onPlayerJoin"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
fun onPlayerRespawn(event: PlayerRespawnEvent) {
|
||||
if (!vanillaWorlds.contains(event.respawnLocation.world.name)) return
|
||||
scope.launch {
|
||||
transactionLock.withLockSuspend(event.player.uniqueId) {
|
||||
val (inventoryShards, enderChestShards) =
|
||||
runOnMainThread { Pair(event.player.inventory.countTotal(), event.player.enderChest.countTotal()) }
|
||||
balanceManager.setPlayerShards(event.player.uniqueId, inventoryShards, ShardType.INVENTORY).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
}
|
||||
balanceManager
|
||||
.setPlayerShards(event.player.uniqueId, enderChestShards, ShardType.ENDER_CHEST)
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@EventHandler
|
||||
fun onEntityPickupItem(event: EntityPickupItemEvent) {
|
||||
val player = event.entity as? Player ?: return
|
||||
val worldName = player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
|
||||
if (economyDisabled) {
|
||||
val itemStack = event.item.itemStack
|
||||
val itemType = itemStack.type
|
||||
if (
|
||||
itemType != Material.DIAMOND &&
|
||||
itemType != Material.DIAMOND_BLOCK &&
|
||||
itemType != Material.SHULKER_BOX &&
|
||||
!(itemType == Material.PRISMARINE_SHARD && Shard.isShardItem(itemStack))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.isCancelled = true
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You cannot pick up any economy-related items while the economy is disabled."
|
||||
)
|
||||
)
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (player.uniqueId.isInventoryLocked()) {
|
||||
val worldName = event.entity.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") return
|
||||
|
||||
val itemType = event.item.itemStack.type
|
||||
if (itemType != Material.DIAMOND && itemType != Material.DIAMOND_BLOCK && itemType != Material.SHULKER_BOX) return
|
||||
|
||||
if (DiamondBankOG.blockInventoryFor.contains(event.entity.uniqueId)) {
|
||||
event.isCancelled = true
|
||||
return
|
||||
}
|
||||
|
||||
if (redis.getValue("diamondbankog:${player.uniqueId}:autodeposit") == "true") {
|
||||
deposit(player, event.item, event.remaining)
|
||||
}
|
||||
val player = event.entity
|
||||
if (player !is Player) return
|
||||
|
||||
scope.launch {
|
||||
if (redis.getValue("diamondbankog:${player.uniqueId}:autocompress") == "true") {
|
||||
compress(player)
|
||||
}
|
||||
|
||||
transactionLock.withLockSuspend(player.uniqueId) {
|
||||
val inventoryShards = runOnMainThread { player.inventory.countTotal() }
|
||||
|
||||
balanceManager.setPlayerShards(player.uniqueId, inventoryShards, ShardType.INVENTORY).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
object : BukkitRunnable() {
|
||||
override fun run() {
|
||||
GlobalScope.launch {
|
||||
val inventoryDiamonds = player.inventory.countDiamonds()
|
||||
val error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
player.uniqueId,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE,
|
||||
null,
|
||||
"onEntityPickupItem"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.runTaskLater(DiamondBankOG.plugin, 1)
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@EventHandler
|
||||
fun onPlayerDropItem(event: PlayerDropItemEvent) {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
val worldName = event.player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") return
|
||||
|
||||
if (economyDisabled) {
|
||||
val itemStack = event.itemDrop.itemStack
|
||||
val itemType = itemStack.type
|
||||
if (
|
||||
itemType != Material.DIAMOND &&
|
||||
itemType != Material.DIAMOND_BLOCK &&
|
||||
itemType != Material.SHULKER_BOX &&
|
||||
!(itemType == Material.PRISMARINE_SHARD && Shard.isShardItem(itemStack))
|
||||
) {
|
||||
return
|
||||
}
|
||||
val itemType = event.itemDrop.itemStack.type
|
||||
if (itemType != Material.DIAMOND && itemType != Material.DIAMOND_BLOCK && itemType != Material.SHULKER_BOX) return
|
||||
|
||||
event.isCancelled = true
|
||||
event.player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You cannot drop any economy-related items while the economy is disabled."
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.player.uniqueId.isInventoryLocked()) {
|
||||
if (DiamondBankOG.blockInventoryFor.contains(event.player.uniqueId)) {
|
||||
event.isCancelled = true
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
transactionLock.withLockSuspend(event.player.uniqueId) {
|
||||
val inventoryShards = runOnMainThread { event.player.inventory.countTotal() }
|
||||
balanceManager.setPlayerShards(event.player.uniqueId, inventoryShards, ShardType.INVENTORY).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
object : BukkitRunnable() {
|
||||
override fun run() {
|
||||
GlobalScope.launch {
|
||||
val inventoryDiamonds = event.player.inventory.countDiamonds()
|
||||
val error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
event.player.uniqueId,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
event.player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE,
|
||||
null,
|
||||
"onPlayerDropItem"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
fun onInventoryClickEvent(event: InventoryClickEvent) {
|
||||
val player = event.whoClicked as? Player ?: return
|
||||
val worldName = player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
|
||||
if (economyDisabled) {
|
||||
val itemStack = event.currentItem ?: return
|
||||
val itemType = itemStack.type
|
||||
|
||||
if (
|
||||
itemType != Material.DIAMOND &&
|
||||
itemType != Material.DIAMOND_BLOCK &&
|
||||
itemType != Material.SHULKER_BOX &&
|
||||
!(itemType == Material.PRISMARINE_SHARD &&
|
||||
itemStack.persistentDataContainer.has(Shard.namespacedKey))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.isCancelled = true
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You cannot move any economy-related items while the economy is disabled."
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (player.uniqueId.isInventoryLocked()) {
|
||||
event.isCancelled = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
fun onPlayerInteract(event: PlayerInteractEvent) {
|
||||
val player = event.player
|
||||
val worldName = player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
|
||||
val clickedBlock = event.clickedBlock ?: return
|
||||
if (clickedBlock.type == Material.AIR) return
|
||||
|
||||
if (economyDisabled) {
|
||||
if (event.action.isLeftClick) {
|
||||
if (clickedBlock.type != Material.DIAMOND_BLOCK && clickedBlock.type != Material.SHULKER_BOX) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
val itemStack = event.item ?: return
|
||||
val itemType = itemStack.type
|
||||
|
||||
if (
|
||||
itemType != Material.DIAMOND &&
|
||||
itemType != Material.DIAMOND_BLOCK &&
|
||||
itemType != Material.SHULKER_BOX &&
|
||||
!(itemType == Material.PRISMARINE_SHARD && Shard.isShardItem(itemStack))
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
event.isCancelled = true
|
||||
event.player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You cannot interact with any economy-related items while the economy is disabled."
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (player.uniqueId.isInventoryLocked()) {
|
||||
event.isCancelled = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
fun onPlayerInteractEntity(event: PlayerInteractEntityEvent) {
|
||||
val player = event.player
|
||||
val worldName = player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
|
||||
if (economyDisabled) {
|
||||
val itemStackInMainHand = player.inventory.itemInMainHand
|
||||
val itemTypeInMainHand = itemStackInMainHand.type
|
||||
|
||||
val itemStackInOffHand = player.inventory.itemInOffHand
|
||||
val itemTypeInOffHand = itemStackInOffHand.type
|
||||
|
||||
if (
|
||||
(itemTypeInMainHand != Material.DIAMOND &&
|
||||
itemTypeInMainHand != Material.DIAMOND_BLOCK &&
|
||||
itemTypeInMainHand != Material.SHULKER_BOX &&
|
||||
!(itemTypeInMainHand == Material.PRISMARINE_SHARD && Shard.isShardItem(itemStackInMainHand)) &&
|
||||
(itemTypeInOffHand != Material.DIAMOND &&
|
||||
itemTypeInOffHand != Material.DIAMOND_BLOCK &&
|
||||
itemTypeInOffHand != Material.SHULKER_BOX &&
|
||||
!(itemTypeInOffHand == Material.PRISMARINE_SHARD && Shard.isShardItem(itemStackInOffHand))))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.isCancelled = true
|
||||
event.player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You cannot interact with any economy-related items while the economy is disabled."
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (player.uniqueId.isInventoryLocked()) {
|
||||
event.isCancelled = true
|
||||
return
|
||||
}
|
||||
}.runTaskLater(DiamondBankOG.plugin, 1)
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
fun onInventoryClose(event: InventoryCloseEvent) {
|
||||
if (economyDisabled) {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
val player = event.player as? Player ?: return
|
||||
val worldName = event.player.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") return
|
||||
|
||||
if (player.uniqueId.isInventoryLocked()) {
|
||||
return
|
||||
}
|
||||
|
||||
val worldName = player.world.name
|
||||
if (!vanillaWorlds.contains(worldName)) return
|
||||
|
||||
scope.launch {
|
||||
if (redis.getValue("diamondbankog:${player.uniqueId}:autocompress") == "true") {
|
||||
compress(player)
|
||||
}
|
||||
|
||||
transactionLock.withLockSuspend(player.uniqueId) {
|
||||
val (inventoryShards, enderChestShards) =
|
||||
runOnMainThread {
|
||||
Pair(
|
||||
player.inventory.countTotal(),
|
||||
if (event.inventory.type == InventoryType.ENDER_CHEST) player.enderChest.countTotal()
|
||||
else null,
|
||||
object : BukkitRunnable() {
|
||||
override fun run() {
|
||||
GlobalScope.launch {
|
||||
val inventoryDiamonds = event.player.inventory.countDiamonds()
|
||||
var error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
event.player.uniqueId,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
event.player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
inventoryDiamonds,
|
||||
BalanceType.INVENTORY_BALANCE,
|
||||
null,
|
||||
"onPlayerJoin"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
balanceManager.setPlayerShards(player.uniqueId, inventoryShards, ShardType.INVENTORY).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
}
|
||||
|
||||
if (enderChestShards == null) return@withLockSuspend
|
||||
balanceManager.setPlayerShards(player.uniqueId, enderChestShards, ShardType.ENDER_CHEST).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend
|
||||
if (event.inventory.type != InventoryType.ENDER_CHEST) return@launch
|
||||
val enderChestDiamonds = event.player.enderChest.countDiamonds()
|
||||
error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
event.player.uniqueId,
|
||||
enderChestDiamonds,
|
||||
BalanceType.ENDER_CHEST_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
event.player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
enderChestDiamonds,
|
||||
BalanceType.ENDER_CHEST_BALANCE,
|
||||
null,
|
||||
"onInventoryClose"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.runTaskLater(DiamondBankOG.plugin, 1)
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
fun onPrepareItemCraft(event: PrepareItemCraftEvent) {
|
||||
val resultType = event.recipe?.result?.type
|
||||
if (
|
||||
resultType != Material.PRISMARINE &&
|
||||
resultType != Material.DARK_PRISMARINE &&
|
||||
resultType != Material.SEA_LANTERN
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (!event.inventory.any { it?.persistentDataContainer?.has(Shard.namespacedKey) == true }) {
|
||||
return
|
||||
}
|
||||
event.inventory.result = ItemStack(Material.AIR)
|
||||
}
|
||||
// @EventHandler
|
||||
// fun onPlayerCommandPreprocess(event: PlayerCommandPreprocessEvent) {
|
||||
// val args = event.message.split(" ", limit = 3)
|
||||
// handleGive(args)
|
||||
// }
|
||||
//
|
||||
// @EventHandler
|
||||
// fun onServerCommand(event: ServerCommandEvent) {
|
||||
// val args = event.command.split(" ", limit = 3)
|
||||
// handleGive(args)
|
||||
// }
|
||||
//
|
||||
// private fun handleGive(args: List<String>) {
|
||||
// GlobalScope.launch {
|
||||
// if (args[0] != "/give" && args[0] != "/essentials:give" && args[0] != "/minecraft:give") return@launch
|
||||
// if (args.size != 3 && args.size != 4) return@launch
|
||||
// if (args[2] != "diamond" && args[2] != "minecraft:diamond") return@launch
|
||||
//
|
||||
// val player = Bukkit.getOfflinePlayer(args[1])
|
||||
// val playerPlayer = player.player ?: return@launch
|
||||
//
|
||||
// if (player.isOnline) {
|
||||
// val inventoryDiamonds = playerPlayer.inventory.countDiamondsInInventory()
|
||||
// val error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
// player.uniqueId,
|
||||
// inventoryDiamonds,
|
||||
// INVENTORY_BALANCE
|
||||
// )
|
||||
// if (error) {
|
||||
// Helper.handleError(
|
||||
// playerPlayer.uniqueId,
|
||||
// SET_PLAYER_BALANCE,
|
||||
// inventoryDiamonds,
|
||||
// INVENTORY_BALANCE,
|
||||
// null,
|
||||
// "handleGive"
|
||||
// )
|
||||
// return@launch
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import io.sentry.Sentry
|
||||
import io.sentry.SentryEvent
|
||||
import io.sentry.protocol.Message
|
||||
import io.sentry.protocol.User
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.block.ShulkerBox
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.inventory.InventoryType
|
||||
import org.bukkit.inventory.Inventory
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.BlockStateMeta
|
||||
import java.util.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
object Helper {
|
||||
enum class PostgresFunction(val string: String) {
|
||||
SET_PLAYER_BALANCE("setPlayerBalance"),
|
||||
ADD_TO_PLAYER_BALANCE("addToPlayerBalance"),
|
||||
SUBTRACT_FROM_PLAYER_BALANCE("subtractFromPlayerBalance"),
|
||||
}
|
||||
|
||||
suspend fun withdrawFromPlayer(player: Player, amount: Int): Int? {
|
||||
val somethingWentWrongMessage =
|
||||
DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong.")
|
||||
|
||||
val playerBalance = DiamondBankOG.postgreSQL.getPlayerBalance(player.uniqueId, BalanceType.ALL)
|
||||
if (playerBalance.bankBalance == null || playerBalance.inventoryBalance == null || playerBalance.enderChestBalance == null) {
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
// Withdraw everything
|
||||
if (amount == -1) {
|
||||
var error = DiamondBankOG.postgreSQL.subtractFromPlayerBalance(
|
||||
player.uniqueId,
|
||||
playerBalance.bankBalance,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SUBTRACT_FROM_PLAYER_BALANCE, playerBalance.bankBalance, BalanceType.BANK_BALANCE,
|
||||
playerBalance, "withdrawFromPlayer"
|
||||
)
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
error = player.inventory.withdraw(
|
||||
playerBalance.inventoryBalance,
|
||||
playerBalance
|
||||
)
|
||||
if (error) {
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
error = player.enderChest.withdraw(
|
||||
playerBalance.enderChestBalance,
|
||||
playerBalance
|
||||
)
|
||||
if (error) {
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
return playerBalance.bankBalance + playerBalance.inventoryBalance + playerBalance.enderChestBalance
|
||||
}
|
||||
|
||||
if (amount > playerBalance.bankBalance + playerBalance.inventoryBalance + playerBalance.enderChestBalance) {
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Cannot withdraw <yellow>$amount <aqua>${if (amount == 1) "Diamond" else "Diamonds"} <red>because you only have <yellow>${playerBalance.bankBalance + playerBalance.inventoryBalance} <aqua>${if (playerBalance.bankBalance + playerBalance.inventoryBalance == 1) "Diamond" else "Diamonds"}<red>."))
|
||||
return null
|
||||
}
|
||||
|
||||
if (amount <= playerBalance.bankBalance) {
|
||||
val error = DiamondBankOG.postgreSQL.subtractFromPlayerBalance(
|
||||
player.uniqueId,
|
||||
amount,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SUBTRACT_FROM_PLAYER_BALANCE, amount, BalanceType.BANK_BALANCE,
|
||||
playerBalance, "withdrawFromPlayer"
|
||||
)
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
if (amount <= playerBalance.bankBalance + playerBalance.inventoryBalance) {
|
||||
var error = DiamondBankOG.postgreSQL.subtractFromPlayerBalance(
|
||||
player.uniqueId,
|
||||
playerBalance.bankBalance,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SUBTRACT_FROM_PLAYER_BALANCE, playerBalance.bankBalance, BalanceType.BANK_BALANCE,
|
||||
playerBalance, "withdrawFromPlayer"
|
||||
)
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
error = player.inventory.withdraw(
|
||||
amount - playerBalance.bankBalance,
|
||||
playerBalance
|
||||
)
|
||||
if (error) {
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
var error = DiamondBankOG.postgreSQL.subtractFromPlayerBalance(
|
||||
player.uniqueId,
|
||||
playerBalance.bankBalance,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SUBTRACT_FROM_PLAYER_BALANCE, playerBalance.bankBalance, BalanceType.BANK_BALANCE,
|
||||
playerBalance, "withdrawFromPlayer"
|
||||
)
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
error = player.inventory.withdraw(playerBalance.inventoryBalance, playerBalance)
|
||||
if (error) {
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
error = player.enderChest.withdraw(
|
||||
amount - (playerBalance.bankBalance + playerBalance.inventoryBalance),
|
||||
playerBalance
|
||||
)
|
||||
if (error) {
|
||||
player.sendMessage(somethingWentWrongMessage)
|
||||
return null
|
||||
}
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
private suspend fun Inventory.withdrawDiamondBlocks(
|
||||
player: Player,
|
||||
toBeRemoved: Int,
|
||||
balanceType: BalanceType,
|
||||
isShulkerBox: Boolean
|
||||
): Int? {
|
||||
val diamondBlocks =
|
||||
this.contents.filterNotNull().filter { it.type == Material.DIAMOND_BLOCK }.sumOf { it.amount }
|
||||
if (diamondBlocks == 0) return 0
|
||||
|
||||
val blocksNeeded = ceil(toBeRemoved.toDouble() / 9).toInt()
|
||||
val wholeBlocks = if (blocksNeeded > diamondBlocks) diamondBlocks else blocksNeeded
|
||||
val remainder = (toBeRemoved) % 9
|
||||
val change = if (remainder == 0) 0 else 9 - remainder
|
||||
|
||||
this.removeItem(ItemStack(Material.DIAMOND_BLOCK, wholeBlocks))
|
||||
|
||||
val emptySlots = this.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpace = this.storageContents.filterNotNull().filter { it.type == Material.DIAMOND }
|
||||
.sumOf { 64 - it.amount }
|
||||
|
||||
if (change > (emptySlots + leftOverSpace)) {
|
||||
if (isShulkerBox) {
|
||||
val emptySlotsInInventory = player.inventory.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpaceInInventory =
|
||||
player.inventory.storageContents.filterNotNull().filter { it.type == Material.DIAMOND }
|
||||
.sumOf { 64 - it.amount }
|
||||
|
||||
if (change > (emptySlotsInInventory + leftOverSpaceInInventory)) {
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: You don't have enough inventory space in your shulker box to store <yellow>$change <aqua>${if (change == 1) "Diamond" else "Diamonds"} <reset>of change from converting diamond blocks into <aqua>Diamonds<reset>, so the <aqua>${if (change == 1) "Diamond" else "Diamonds"} <reset>has been deposited into your bank."))
|
||||
val error = DiamondBankOG.postgreSQL.addToPlayerBalance(
|
||||
player.uniqueId,
|
||||
change,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.ADD_TO_PLAYER_BALANCE,
|
||||
change,
|
||||
balanceType,
|
||||
null,
|
||||
"Inventory.withdrawDiamondBlocks"
|
||||
)
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong."))
|
||||
DiamondBankOG.blockInventoryFor.remove(player.uniqueId)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
player.inventory.addItem(ItemStack(Material.DIAMOND, change))
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: You don't have enough inventory space in your shulker box to store <yellow>$change <aqua>${if (change == 1) "Diamond" else "Diamonds"} <reset>of change from converting diamond blocks into <aqua>Diamonds<reset>, so the <aqua>${if (change == 1) "Diamond" else "Diamonds"} <reset>have been added to your inventory."))
|
||||
}
|
||||
} else {
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: You don't have enough inventory space to store <yellow>$change <aqua>${if (change == 1) "Diamond" else "Diamonds"} <reset>of change from converting diamond blocks into <aqua>Diamonds<reset>, so the <aqua>${if (change == 1) "Diamond" else "Diamonds"} <reset>has been deposited into your bank."))
|
||||
val error = DiamondBankOG.postgreSQL.addToPlayerBalance(
|
||||
player.uniqueId,
|
||||
change,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.ADD_TO_PLAYER_BALANCE,
|
||||
change,
|
||||
balanceType,
|
||||
null,
|
||||
"Inventory.withdrawDiamondBlocks"
|
||||
)
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong."))
|
||||
DiamondBankOG.blockInventoryFor.remove(player.uniqueId)
|
||||
return null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.addItem(ItemStack(Material.DIAMOND, change))
|
||||
}
|
||||
|
||||
return if (blocksNeeded > diamondBlocks) diamondBlocks else (wholeBlocks * 9) - change
|
||||
}
|
||||
|
||||
suspend fun Inventory.withdraw(
|
||||
amount: Int,
|
||||
playerBalance: PostgreSQL.PlayerBalance
|
||||
): Boolean {
|
||||
if (this.holder !is Player) return true
|
||||
|
||||
val player = this.holder as Player
|
||||
DiamondBankOG.blockInventoryFor.add(player.uniqueId)
|
||||
|
||||
val balance = if (this.type == InventoryType.PLAYER) {
|
||||
playerBalance.inventoryBalance!!
|
||||
} else {
|
||||
playerBalance.enderChestBalance!!
|
||||
}
|
||||
|
||||
val balanceType = if (this.type == InventoryType.PLAYER) {
|
||||
BalanceType.INVENTORY_BALANCE
|
||||
} else {
|
||||
BalanceType.ENDER_CHEST_BALANCE
|
||||
}
|
||||
|
||||
val inventoryDiamonds = this.countDiamonds()
|
||||
if (playerBalance.inventoryBalance != inventoryDiamonds) {
|
||||
val error = DiamondBankOG.postgreSQL.setPlayerBalance(
|
||||
player.uniqueId,
|
||||
balance - (amount - inventoryDiamonds),
|
||||
balanceType
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SET_PLAYER_BALANCE,
|
||||
amount,
|
||||
balanceType,
|
||||
playerBalance,
|
||||
"Inventory.withdraw"
|
||||
)
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong."))
|
||||
DiamondBankOG.blockInventoryFor.remove(player.uniqueId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val error = DiamondBankOG.postgreSQL.subtractFromPlayerBalance(
|
||||
player.uniqueId,
|
||||
amount,
|
||||
balanceType
|
||||
)
|
||||
if (error) {
|
||||
handleError(
|
||||
player.uniqueId,
|
||||
PostgresFunction.SUBTRACT_FROM_PLAYER_BALANCE,
|
||||
amount,
|
||||
balanceType,
|
||||
playerBalance,
|
||||
"Inventory.withdraw"
|
||||
)
|
||||
player.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong."))
|
||||
DiamondBankOG.blockInventoryFor.remove(player.uniqueId)
|
||||
return true
|
||||
}
|
||||
|
||||
val removeMap = this.removeItem(ItemStack(Material.DIAMOND, amount))
|
||||
if (removeMap.isNotEmpty()) {
|
||||
var toBeRemoved = removeMap[0]!!.amount
|
||||
|
||||
val removed = this.withdrawDiamondBlocks(player, toBeRemoved, balanceType, false) ?: return true
|
||||
toBeRemoved -= removed
|
||||
|
||||
if (toBeRemoved != 0) {
|
||||
val itemStacks = this.contents.filterNotNull().filter { it.type == Material.SHULKER_BOX }
|
||||
for (itemStack in itemStacks) {
|
||||
val blockStateMeta = (itemStack.itemMeta as BlockStateMeta)
|
||||
itemStack.itemMeta
|
||||
val shulkerBox = blockStateMeta.blockState as ShulkerBox
|
||||
val shulkerRemoveMap = shulkerBox.inventory.removeItem(ItemStack(Material.DIAMOND, toBeRemoved))
|
||||
|
||||
blockStateMeta.blockState = shulkerBox
|
||||
itemStack.itemMeta = blockStateMeta
|
||||
|
||||
toBeRemoved -= if (shulkerRemoveMap.isEmpty()) toBeRemoved else toBeRemoved - shulkerRemoveMap[0]!!.amount
|
||||
if (toBeRemoved == 0) break
|
||||
}
|
||||
|
||||
if (toBeRemoved != 0) {
|
||||
for (itemStack in itemStacks) {
|
||||
val blockStateMeta = (itemStack.itemMeta as BlockStateMeta)
|
||||
itemStack.itemMeta
|
||||
val shulkerBox = blockStateMeta.blockState as ShulkerBox
|
||||
|
||||
val shulkerRemoved =
|
||||
shulkerBox.inventory.withdrawDiamondBlocks(player, toBeRemoved, balanceType, true)
|
||||
?: return true
|
||||
|
||||
blockStateMeta.blockState = shulkerBox
|
||||
itemStack.itemMeta = blockStateMeta
|
||||
|
||||
toBeRemoved -= shulkerRemoved
|
||||
if (toBeRemoved == 0) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DiamondBankOG.blockInventoryFor.remove(player.uniqueId)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun Inventory.countDiamonds(): Int {
|
||||
val inventoryDiamonds = this.all(Material.DIAMOND).values.sumOf { it.amount }
|
||||
val shulkerBoxDiamonds = this.all(Material.SHULKER_BOX).values.sumOf { itemStack ->
|
||||
((itemStack.itemMeta as BlockStateMeta).blockState as ShulkerBox).inventory.all(Material.DIAMOND).values.sumOf { it.amount }
|
||||
}
|
||||
val inventoryDiamondBlocks = this.all(Material.DIAMOND_BLOCK).values.sumOf { it.amount * 9 }
|
||||
val shulkerBoxDiamondBlocks = this.all(Material.SHULKER_BOX).values.sumOf { itemStack ->
|
||||
((itemStack.itemMeta as BlockStateMeta).blockState as ShulkerBox).inventory.all(Material.DIAMOND_BLOCK).values.sumOf { it.amount * 9 }
|
||||
}
|
||||
return inventoryDiamonds + shulkerBoxDiamonds + inventoryDiamondBlocks + shulkerBoxDiamondBlocks
|
||||
}
|
||||
|
||||
fun handleError(
|
||||
uuid: UUID,
|
||||
function: PostgresFunction,
|
||||
amount: Int,
|
||||
balanceType: BalanceType,
|
||||
playerBalance: PostgreSQL.PlayerBalance?,
|
||||
inFunction: String
|
||||
) {
|
||||
DiamondBankOG.economyDisabled = true
|
||||
if (DiamondBankOG.sentryEnabled) {
|
||||
val sentryUser = User()
|
||||
sentryUser.id = uuid.toString()
|
||||
|
||||
val sentryEvent = SentryEvent()
|
||||
sentryEvent.user = sentryUser
|
||||
sentryEvent.setExtra("Function", "${function.string}(amount = $amount, type = $balanceType)")
|
||||
if (playerBalance != null) {
|
||||
if (playerBalance.bankBalance != null) sentryEvent.setExtra("Bank Balance", playerBalance.bankBalance)
|
||||
if (playerBalance.inventoryBalance != null) sentryEvent.setExtra(
|
||||
"Inventory Balance",
|
||||
playerBalance.inventoryBalance
|
||||
)
|
||||
if (playerBalance.enderChestBalance != null) sentryEvent.setExtra(
|
||||
"Ender Chest Balance",
|
||||
playerBalance.enderChestBalance
|
||||
)
|
||||
}
|
||||
|
||||
val message = Message()
|
||||
message.message = "${function.string} failed in $inFunction"
|
||||
sentryEvent.message = message
|
||||
|
||||
Sentry.captureEvent(sentryEvent)
|
||||
}
|
||||
DiamondBankOG.plugin.logger.severe(
|
||||
"""
|
||||
${function.string} failed in $inFunction
|
||||
Player UUID: $uuid
|
||||
Function: ${function.string}(amount = $amount, type = $balanceType)
|
||||
${
|
||||
if (playerBalance != null) {
|
||||
if (playerBalance.bankBalance != null) "Bank Balance: ${playerBalance.bankBalance}" else ""
|
||||
} else ""
|
||||
}
|
||||
${
|
||||
if (playerBalance != null) {
|
||||
if (playerBalance.inventoryBalance != null) "Inventory Balance: ${playerBalance.inventoryBalance}" else ""
|
||||
} else ""
|
||||
}
|
||||
${
|
||||
if (playerBalance != null) {
|
||||
if (playerBalance.enderChestBalance != null) "Ender Chest Balance: ${playerBalance.enderChestBalance}" else ""
|
||||
} else ""
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import com.github.jasync.sql.db.asSuspending
|
||||
import com.github.jasync.sql.db.general.ArrayRowData
|
||||
import com.github.jasync.sql.db.pool.ConnectionPool
|
||||
import com.github.jasync.sql.db.postgresql.PostgreSQLConnection
|
||||
import com.github.jasync.sql.db.postgresql.PostgreSQLConnectionBuilder
|
||||
import kotlinx.coroutines.future.await
|
||||
import org.bukkit.Bukkit
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
|
||||
class PostgreSQL {
|
||||
lateinit var pool: ConnectionPool<PostgreSQLConnection>
|
||||
|
||||
enum class BalanceType {
|
||||
BANK_BALANCE, INVENTORY_BALANCE, ENDER_CHEST_BALANCE, ALL
|
||||
}
|
||||
|
||||
@Throws(SQLException::class, ClassNotFoundException::class)
|
||||
fun initDB() {
|
||||
try {
|
||||
pool =
|
||||
PostgreSQLConnectionBuilder.createConnectionPool("${Config.postgresUrl}?user=${Config.postgresUser}&password=${Config.postgresPassword}")
|
||||
val createTable =
|
||||
pool.sendPreparedStatement("CREATE TABLE IF NOT EXISTS ${Config.postgresTable}(uuid TEXT, bank_balance integer, inventory_balance integer, ender_chest_balance integer, unique(uuid))")
|
||||
createTable.join()
|
||||
val createIndex =
|
||||
pool.sendPreparedStatement("CREATE INDEX IF NOT EXISTS idx_balance ON ${Config.postgresTable}(bank_balance, inventory_balance, ender_chest_balance)")
|
||||
createIndex.join()
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.economyDisabled = true
|
||||
DiamondBankOG.plugin.logger.severe("ECONOMY DISABLED! Something went wrong while trying to initialise PostgreSQL. Is PostgreSQL running? Are the PostgreSQL config variables correct?")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setPlayerBalance(uuid: UUID, balance: Int, type: BalanceType): Boolean {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
val balanceType = when (type) {
|
||||
BalanceType.BANK_BALANCE -> "bank_balance"
|
||||
BalanceType.INVENTORY_BALANCE -> "inventory_balance"
|
||||
BalanceType.ENDER_CHEST_BALANCE -> "ender_chest_balance"
|
||||
else -> return true
|
||||
}
|
||||
|
||||
val preparedStatement =
|
||||
connection.sendPreparedStatement("INSERT INTO ${Config.postgresTable}(uuid, $balanceType) VALUES('$uuid', $balance) ON CONFLICT (uuid) DO UPDATE SET $balanceType = excluded.$balanceType")
|
||||
preparedStatement.await()
|
||||
} catch (e: Exception) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun addToPlayerBalance(uuid: UUID, amount: Int, type: BalanceType): Boolean {
|
||||
val playerBalance = getPlayerBalanceWrapper(uuid, type) ?: return true
|
||||
|
||||
val error = setPlayerBalance(uuid, playerBalance + amount, type)
|
||||
return error
|
||||
}
|
||||
|
||||
suspend fun subtractFromPlayerBalance(uuid: UUID, amount: Int, type: BalanceType): Boolean {
|
||||
val playerBalance = getPlayerBalanceWrapper(uuid, type) ?: return true
|
||||
|
||||
val error = setPlayerBalance(uuid, playerBalance - amount, type)
|
||||
return error
|
||||
}
|
||||
|
||||
private suspend fun getPlayerBalanceWrapper(uuid: UUID, type: BalanceType): Int? {
|
||||
val playerBalance = getPlayerBalance(uuid, type)
|
||||
|
||||
return when (type) {
|
||||
BalanceType.BANK_BALANCE -> playerBalance.bankBalance
|
||||
BalanceType.INVENTORY_BALANCE -> playerBalance.inventoryBalance
|
||||
BalanceType.ENDER_CHEST_BALANCE -> playerBalance.enderChestBalance
|
||||
BalanceType.ALL -> if (playerBalance.bankBalance != null && playerBalance.inventoryBalance != null && playerBalance.enderChestBalance != null) playerBalance.bankBalance + playerBalance.inventoryBalance + playerBalance.enderChestBalance else null
|
||||
}
|
||||
}
|
||||
|
||||
data class PlayerBalance(val bankBalance: Int?, val inventoryBalance: Int?, val enderChestBalance: Int?)
|
||||
|
||||
suspend fun getPlayerBalance(uuid: UUID, type: BalanceType): PlayerBalance {
|
||||
var bankBalance: Int? = null
|
||||
var inventoryBalance: Int? = null
|
||||
var enderChestBalance: Int? = null
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
val balanceType = when (type) {
|
||||
BalanceType.BANK_BALANCE -> "bank_balance"
|
||||
BalanceType.INVENTORY_BALANCE -> "inventory_balance"
|
||||
BalanceType.ENDER_CHEST_BALANCE -> "ender_chest_balance"
|
||||
BalanceType.ALL -> "bank_balance, inventory_balance, ender_chest_balance"
|
||||
}
|
||||
|
||||
val preparedStatement =
|
||||
connection.sendPreparedStatement("SELECT $balanceType FROM ${Config.postgresTable} WHERE uuid = '$uuid' LIMIT 1")
|
||||
val result = preparedStatement.await()
|
||||
|
||||
if (result.rows.size != 0) {
|
||||
val rowData = result.rows[0] as ArrayRowData
|
||||
|
||||
when (type) {
|
||||
BalanceType.BANK_BALANCE -> {
|
||||
bankBalance = if (rowData.columns[0] != null) {
|
||||
rowData.columns[0] as Int
|
||||
} else 0
|
||||
}
|
||||
|
||||
BalanceType.INVENTORY_BALANCE -> {
|
||||
inventoryBalance = if (rowData.columns[0] != null) {
|
||||
rowData.columns[0] as Int
|
||||
} else 0
|
||||
}
|
||||
|
||||
BalanceType.ENDER_CHEST_BALANCE -> {
|
||||
enderChestBalance = if (rowData.columns[0] != null) {
|
||||
rowData.columns[0] as Int
|
||||
} else 0
|
||||
}
|
||||
|
||||
BalanceType.ALL -> {
|
||||
bankBalance = if (rowData.columns[0] != null) {
|
||||
rowData.columns[0] as Int
|
||||
} else 0
|
||||
inventoryBalance = if (rowData.columns[1] != null) {
|
||||
rowData.columns[1] as Int
|
||||
} else 0
|
||||
enderChestBalance = if (rowData.columns[2] != null) {
|
||||
rowData.columns[2] as Int
|
||||
} else 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bankBalance = 0
|
||||
inventoryBalance = 0
|
||||
enderChestBalance = 0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.plugin.logger.info(e.toString())
|
||||
}
|
||||
return PlayerBalance(bankBalance, inventoryBalance, enderChestBalance)
|
||||
}
|
||||
|
||||
suspend fun getBaltop(offset: Int): MutableMap<String?, Int>? {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val preparedStatement =
|
||||
connection.sendPreparedStatement("SELECT * FROM ${Config.postgresTable} ORDER BY bank_balance DESC, inventory_balance DESC, ender_chest_balance DESC OFFSET $offset LIMIT 10")
|
||||
val result = preparedStatement.await()
|
||||
val baltop = mutableMapOf<String?, Int>()
|
||||
result.rows.forEach {
|
||||
val rowData = it as ArrayRowData
|
||||
val bankBalance = if (rowData.columns[1] != null) {
|
||||
rowData.columns[1] as Int
|
||||
} else 0
|
||||
val inventoryBalance = if (rowData.columns[2] != null) {
|
||||
rowData.columns[2] as Int
|
||||
} else 0
|
||||
val enderChestBalance = if (rowData.columns[3] != null) {
|
||||
rowData.columns[3] as Int
|
||||
} else 0
|
||||
|
||||
val player = Bukkit.getPlayer(UUID.fromString(rowData.columns[0] as String)) ?: Bukkit.getOfflinePlayer(
|
||||
UUID.fromString(rowData.columns[0] as String)
|
||||
)
|
||||
baltop[player.name] = bankBalance + inventoryBalance + enderChestBalance
|
||||
}
|
||||
return baltop
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.plugin.logger.info(e.toString())
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun getNumberOfRows(): Long? {
|
||||
var number: Long? = null
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val preparedStatement =
|
||||
connection.sendPreparedStatement("SELECT count(*) AS exact_count FROM ${Config.postgresTable}")
|
||||
val result = preparedStatement.await()
|
||||
|
||||
if (result.rows.size != 0) {
|
||||
val rowData = result.rows[0] as ArrayRowData
|
||||
number = if (rowData.columns[0] != null) {
|
||||
rowData.columns[0] as Long
|
||||
} else 0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.plugin.logger.info(e.toString())
|
||||
}
|
||||
return number
|
||||
}
|
||||
|
||||
suspend fun entryExists(uuid: UUID): Boolean {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val preparedStatement =
|
||||
connection.sendPreparedStatement("SELECT 1 FROM ${Config.postgresTable} WHERE uuid = '${uuid}';")
|
||||
val result = preparedStatement.await()
|
||||
|
||||
if (result.rows.size == 1) {
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.plugin.logger.info(e.toString())
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
class ToggleDebug : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (!sender.hasPermission("diamondbank-og.debug")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
DiamondBankOG.debug = !DiamondBankOG.debug
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: Debug mode is now turned ${if (DiamondBankOG.debug) "<green>on" else "<red>off"}<reset>."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
package net.trueog.diamondbankog.api
|
||||
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.trueog.diamondbankog.DiamondBankException
|
||||
import net.trueog.diamondbankog.DiamondBankException.*
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.eventManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
class DiamondBankAPIJava {
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released.
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class, InvalidPlayerException::class)
|
||||
@Suppress("unused")
|
||||
fun addToPlayerBankShards(uuid: UUID, shards: Long, transactionReason: String, notes: String?) {
|
||||
require(shards >= 0) { "shards must not be negative" }
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
|
||||
return runBlocking {
|
||||
transactionLock.withLockSuspend(uuid) {
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
if (player !is Player && !player.hasPlayedBefore()) throw InvalidPlayerException()
|
||||
|
||||
balanceManager.addToBankShards(uuid, shards).getOrElse {
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
|
||||
balanceManager.insertTransactionLog(uuid, shards, null, transactionReason, notes).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.InsufficientBalanceException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class, InsufficientBalanceException::class, InvalidPlayerException::class)
|
||||
@Suppress("unused")
|
||||
fun subtractFromPlayerBankShards(uuid: UUID, shards: Long, transactionReason: String, notes: String?) {
|
||||
require(shards >= 0) { "shards must not be negative" }
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
|
||||
return runBlocking {
|
||||
transactionLock.withLockSuspend(uuid) {
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
if (player !is Player && !player.hasPlayedBefore()) throw InvalidPlayerException()
|
||||
|
||||
balanceManager.subtractFromBankShards(uuid, shards).getOrElse {
|
||||
if (it is InsufficientBalanceException) throw it
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
|
||||
balanceManager.insertTransactionLog(uuid, shards, null, transactionReason, notes).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class)
|
||||
@Suppress("unused")
|
||||
fun getBankShards(uuid: UUID): Long = getShardTypeShards(uuid, ShardType.BANK)
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class)
|
||||
@Suppress("unused")
|
||||
fun getInventoryShards(uuid: UUID): Long = getShardTypeShards(uuid, ShardType.INVENTORY)
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class)
|
||||
@Suppress("unused")
|
||||
fun getEnderChestShards(uuid: UUID): Long = getShardTypeShards(uuid, ShardType.ENDER_CHEST)
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class)
|
||||
@Suppress("unused")
|
||||
fun getTotalShards(uuid: UUID): Long = getShardTypeShards(uuid, ShardType.TOTAL)
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class)
|
||||
@Suppress("unused")
|
||||
fun getAllShards(uuid: UUID): PlayerShards {
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
|
||||
return runBlocking {
|
||||
transactionLock.withLockSuspend(uuid) {
|
||||
balanceManager.getAllShards(uuid).getOrElse {
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(EconomyDisabledException::class)
|
||||
private fun getShardTypeShards(uuid: UUID, type: ShardType): Long {
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
|
||||
return runBlocking {
|
||||
transactionLock
|
||||
.withLockSuspend(uuid) {
|
||||
when (type) {
|
||||
ShardType.BANK -> balanceManager.getBankShards(uuid)
|
||||
ShardType.INVENTORY -> balanceManager.getInventoryShards(uuid)
|
||||
ShardType.ENDER_CHEST -> balanceManager.getEnderChestShards(uuid)
|
||||
ShardType.TOTAL -> balanceManager.getTotalShards(uuid)
|
||||
}
|
||||
}
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class)
|
||||
@Suppress("unused")
|
||||
fun getBaltop(offset: Int): Map<UUID?, Long> {
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
|
||||
return runBlocking {
|
||||
balanceManager.getBaltop(offset).getOrElse {
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
* @throws DiamondBankException.InsufficientFundsException
|
||||
*/
|
||||
@Throws(EconomyDisabledException::class, InvalidPlayerException::class, InsufficientFundsException::class)
|
||||
@Suppress("unused")
|
||||
fun consumeFromPlayer(uuid: UUID, shards: Long, transactionReason: String, notes: String?) {
|
||||
require(shards >= 0) { "shards must not be negative" }
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
|
||||
return runBlocking {
|
||||
transactionLock.withLockSuspend(uuid) {
|
||||
val player = Bukkit.getOfflinePlayer(uuid)
|
||||
if (player !is Player && !player.hasPlayedBefore()) throw InvalidPlayerException()
|
||||
|
||||
player.uniqueId.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread { InventorySnapshot.from(player.uniqueId, balanceManager) }
|
||||
|
||||
val toSubtract =
|
||||
CommonOperations.consume(player.uniqueId, shards, inventorySnapshot).getOrElse {
|
||||
if (it is DatabaseException) {
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
balanceManager.subtractFromBankShards(player.uniqueId, toSubtract).getOrElse {
|
||||
if (it is InsufficientBalanceException) throw it
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(player.uniqueId) }
|
||||
}
|
||||
|
||||
balanceManager.insertTransactionLog(uuid, shards, null, transactionReason, notes).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.SenderEqualToReceiverException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
* @throws DiamondBankException.InsufficientFundsException
|
||||
*/
|
||||
@Throws(
|
||||
EconomyDisabledException::class,
|
||||
SenderEqualToReceiverException::class,
|
||||
InvalidPlayerException::class,
|
||||
InsufficientFundsException::class,
|
||||
)
|
||||
@Suppress("unused")
|
||||
fun playerPayPlayer(senderUuid: UUID, receiverUuid: UUID, shards: Long, transactionReason: String, notes: String?) {
|
||||
require(shards >= 0) { "shards must not be negative" }
|
||||
if (economyDisabled) throw EconomyDisabledException()
|
||||
if (senderUuid == receiverUuid) throw SenderEqualToReceiverException()
|
||||
|
||||
return runBlocking {
|
||||
transactionLock.withLockSuspend(senderUuid) {
|
||||
val sender = Bukkit.getPlayer(senderUuid) ?: Bukkit.getOfflinePlayer(senderUuid)
|
||||
if (sender !is Player && !sender.hasPlayedBefore()) throw InvalidPlayerException()
|
||||
|
||||
val receiver = Bukkit.getPlayer(receiverUuid) ?: Bukkit.getOfflinePlayer(receiverUuid)
|
||||
if (receiver !is Player && !receiver.hasPlayedBefore()) throw InvalidPlayerException()
|
||||
|
||||
sender.uniqueId.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread { InventorySnapshot.from(sender.uniqueId, balanceManager) }
|
||||
|
||||
val shardsToSubtractFromSender =
|
||||
CommonOperations.consume(sender.uniqueId, shards, inventorySnapshot).getOrElse {
|
||||
if (it is DatabaseException) {
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
balanceManager
|
||||
.transferBankShards(senderUuid, receiverUuid, shardsToSubtractFromSender, shards)
|
||||
.getOrElse {
|
||||
if (it is InsufficientBalanceException) throw it
|
||||
handleError(it)
|
||||
throw EconomyDisabledException()
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(sender.uniqueId) }
|
||||
}
|
||||
|
||||
balanceManager
|
||||
.insertTransactionLog(senderUuid, shards, receiverUuid, transactionReason, notes)
|
||||
.getOrElse { handleError(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Register a PlayerBalanceChangedListener to listen for player balance changes */
|
||||
@Suppress("unused")
|
||||
fun registerEventListener(eventListener: PlayerBalanceChangedListener) {
|
||||
eventManager.register(eventListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: This function can throw a MoreThanOneDecimalDigitRuntimeException, make sure it's checked when handling
|
||||
* arbitrary input
|
||||
*
|
||||
* Converts a Diamond double to Shards
|
||||
*/
|
||||
@Suppress("unused") fun diamondsToShards(diamonds: Double) = diamondsToShards(diamonds.toFloat())
|
||||
|
||||
/**
|
||||
* WARNING: This function can throw a MoreThanOneDecimalDigitRuntimeException, make sure it's checked when handling
|
||||
* arbitrary input
|
||||
*
|
||||
* Converts a Diamond float to Shards
|
||||
*/
|
||||
@Suppress("unused") fun diamondsToShards(diamonds: Float) = CommonOperations.diamondsToShards(diamonds).getOrThrow()
|
||||
|
||||
/** Converts Shards into a formatted Diamonds string */
|
||||
@Suppress("unused") fun shardsToDiamonds(shards: Long) = CommonOperations.shardsToDiamonds(shards)
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package net.trueog.diamondbankog.api
|
||||
|
||||
import java.util.*
|
||||
import net.trueog.diamondbankog.DiamondBankException.*
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.eventManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
class DiamondBankAPIKotlin {
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun addToPlayerBankShards(
|
||||
uuid: UUID,
|
||||
shards: ULong,
|
||||
transactionReason: String,
|
||||
notes: String? = null,
|
||||
): Result<Unit> {
|
||||
require(shards <= Long.MAX_VALUE.toULong()) { "shards must not be above the max value of a Long" }
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return transactionLock.withLockSuspend(uuid) {
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
if (player !is Player && !player.hasPlayedBefore())
|
||||
return@withLockSuspend Result.failure(InvalidPlayerException())
|
||||
|
||||
balanceManager.addToBankShards(uuid, shards.toLong()).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
|
||||
balanceManager.insertTransactionLog(uuid, shards.toLong(), null, transactionReason, notes).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun subtractFromPlayerBankShards(
|
||||
uuid: UUID,
|
||||
shards: ULong,
|
||||
transactionReason: String,
|
||||
notes: String? = null,
|
||||
): Result<Unit> {
|
||||
require(shards <= Long.MAX_VALUE.toULong()) { "shards must not be above the max value of a Long" }
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return transactionLock.withLockSuspend(uuid) {
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
if (player !is Player && !player.hasPlayedBefore())
|
||||
return@withLockSuspend Result.failure(InvalidPlayerException())
|
||||
|
||||
balanceManager.subtractFromBankShards(uuid, shards.toLong()).getOrElse {
|
||||
if (it is InsufficientBalanceException) return@withLockSuspend Result.failure(it)
|
||||
handleError(it)
|
||||
return@withLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
|
||||
balanceManager.insertTransactionLog(uuid, shards.toLong(), null, transactionReason, notes).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/** WARNING: if the player has a transaction lock applied this function will block until its released */
|
||||
@Suppress("unused") suspend fun getBankShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.BANK)
|
||||
|
||||
/** WARNING: if the player has a transaction lock applied this function will block until its released */
|
||||
@Suppress("unused")
|
||||
suspend fun getInventoryShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.INVENTORY)
|
||||
|
||||
/** WARNING: if the player has a transaction lock applied this function will block until its released */
|
||||
@Suppress("unused")
|
||||
suspend fun getEnderChestShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.ENDER_CHEST)
|
||||
|
||||
/** WARNING: if the player has a transaction lock applied this function will block until its released */
|
||||
@Suppress("unused") suspend fun getTotalShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.TOTAL)
|
||||
|
||||
/** WARNING: if the player has a transaction lock applied this function will block until its released */
|
||||
@Suppress("unused")
|
||||
suspend fun getAllShards(uuid: UUID): Result<PlayerShards> {
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return transactionLock.withLockSuspend(uuid) {
|
||||
Result.success(
|
||||
balanceManager.getAllShards(uuid).getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long> {
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return transactionLock.withLockSuspend(uuid) {
|
||||
Result.success(
|
||||
when (type) {
|
||||
ShardType.BANK -> balanceManager.getBankShards(uuid)
|
||||
ShardType.INVENTORY -> balanceManager.getInventoryShards(uuid)
|
||||
ShardType.ENDER_CHEST -> balanceManager.getEnderChestShards(uuid)
|
||||
ShardType.TOTAL -> balanceManager.getTotalShards(uuid)
|
||||
}.getOrElse {
|
||||
handleError(it)
|
||||
return@withLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>> {
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return Result.success(
|
||||
balanceManager.getBaltop(offset).getOrElse {
|
||||
handleError(it)
|
||||
return Result.failure(EconomyDisabledException())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun consumeFromPlayer(
|
||||
uuid: UUID,
|
||||
shards: ULong,
|
||||
transactionReason: String,
|
||||
notes: String? = null,
|
||||
): Result<Unit> {
|
||||
require(shards <= Long.MAX_VALUE.toULong()) { "shards must not be above the max value of a Long" }
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return transactionLock.withLockSuspend(uuid) {
|
||||
val player = Bukkit.getOfflinePlayer(uuid)
|
||||
if (player !is Player && !player.hasPlayedBefore())
|
||||
return@withLockSuspend Result.failure(InvalidPlayerException())
|
||||
|
||||
player.uniqueId
|
||||
.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread { InventorySnapshot.from(player.uniqueId, balanceManager) }
|
||||
|
||||
val toSubtract =
|
||||
CommonOperations.consume(player.uniqueId, shards.toLong(), inventorySnapshot).getOrElse {
|
||||
if (it is DatabaseException) {
|
||||
handleError(it)
|
||||
return@withInventoryLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
return@withInventoryLockSuspend Result.failure(it)
|
||||
}
|
||||
|
||||
balanceManager.subtractFromBankShards(player.uniqueId, toSubtract).getOrElse {
|
||||
if (it is InsufficientBalanceException) return@withInventoryLockSuspend Result.failure(it)
|
||||
handleError(it)
|
||||
return@withInventoryLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(player.uniqueId) }
|
||||
Result.success(Unit)
|
||||
}
|
||||
.getOrElse {
|
||||
return@withLockSuspend Result.failure(it)
|
||||
}
|
||||
|
||||
balanceManager.insertTransactionLog(uuid, shards.toLong(), null, transactionReason, notes).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* WARNING: This function can return a CouldNotRemoveEnoughException, make sure you handle it properly. It has a
|
||||
* field called notRemoved that has the amount of shards not removed, you should continue with the originally
|
||||
* requested amount of shards minus notRemoved
|
||||
*
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun playerPayPlayer(
|
||||
senderUuid: UUID,
|
||||
receiverUuid: UUID,
|
||||
shards: ULong,
|
||||
transactionReason: String,
|
||||
notes: String? = null,
|
||||
): Result<Unit> {
|
||||
require(shards <= Long.MAX_VALUE.toULong()) { "shards must not be above the max value of a Long" }
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
if (senderUuid == receiverUuid) return Result.failure(SenderEqualToReceiverException())
|
||||
|
||||
return transactionLock.withLockSuspend(senderUuid) {
|
||||
val sender = Bukkit.getPlayer(senderUuid) ?: Bukkit.getOfflinePlayer(senderUuid)
|
||||
if (sender !is Player && !sender.hasPlayedBefore())
|
||||
return@withLockSuspend Result.failure(InvalidPlayerException())
|
||||
|
||||
val receiver = Bukkit.getPlayer(receiverUuid) ?: Bukkit.getOfflinePlayer(receiverUuid)
|
||||
if (receiver !is Player && !receiver.hasPlayedBefore())
|
||||
return@withLockSuspend Result.failure(InvalidPlayerException())
|
||||
|
||||
sender.uniqueId
|
||||
.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread { InventorySnapshot.from(sender.uniqueId, balanceManager) }
|
||||
|
||||
val shardsToSubtractFromSender =
|
||||
CommonOperations.consume(sender.uniqueId, shards.toLong(), inventorySnapshot).getOrElse {
|
||||
if (it is DatabaseException) {
|
||||
handleError(it)
|
||||
return@withInventoryLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
return@withInventoryLockSuspend Result.failure(it)
|
||||
}
|
||||
|
||||
balanceManager
|
||||
.transferBankShards(senderUuid, receiverUuid, shardsToSubtractFromSender, shards.toLong())
|
||||
.getOrElse {
|
||||
if (it is InsufficientBalanceException) return@withInventoryLockSuspend Result.failure(it)
|
||||
handleError(it)
|
||||
return@withInventoryLockSuspend Result.failure(EconomyDisabledException())
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(sender.uniqueId) }
|
||||
Result.success(Unit)
|
||||
}
|
||||
.getOrElse {
|
||||
return@withLockSuspend Result.failure(it)
|
||||
}
|
||||
|
||||
balanceManager
|
||||
.insertTransactionLog(senderUuid, shards.toLong(), receiverUuid, transactionReason, notes)
|
||||
.getOrElse { handleError(it) }
|
||||
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun registerEventListener(eventListener: PlayerBalanceChangedListener) {
|
||||
eventManager.register(eventListener)
|
||||
}
|
||||
|
||||
/** Converts a Diamond double to Shards */
|
||||
@Suppress("unused") fun diamondsToShards(diamonds: Double) = diamondsToShards(diamonds.toFloat())
|
||||
|
||||
/** Converts a Diamond float to Shards */
|
||||
@Suppress("unused") fun diamondsToShards(diamonds: Float) = CommonOperations.diamondsToShards(diamonds)
|
||||
|
||||
/** Converts Shards into a formatted Diamonds string */
|
||||
@Suppress("unused") fun shardsToDiamonds(shards: Long) = CommonOperations.shardsToDiamonds(shards)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package net.trueog.diamondbankog.api
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.logging.Level
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
|
||||
internal class EventManager {
|
||||
private var listeners = CopyOnWriteArrayList<PlayerBalanceChangedListener>()
|
||||
private val queues = ConcurrentHashMap<UUID, Channel<PlayerBalanceChangedEvent>>()
|
||||
|
||||
// One channel + one consumer coroutine per uuid. Consumer processes events
|
||||
// strictly in send order, off any lock, so listeners can safely call back
|
||||
// into the API without deadlocking TransactionLock.
|
||||
private fun queueFor(uuid: UUID) =
|
||||
queues.computeIfAbsent(uuid) {
|
||||
Channel<PlayerBalanceChangedEvent>(Channel.UNLIMITED).also { channel ->
|
||||
scope.launch {
|
||||
for (event in channel) {
|
||||
listeners.forEach {
|
||||
try {
|
||||
it.onUpdate(event)
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.log(Level.SEVERE, "PlayerBalanceChangedEvent listener threw", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendUpdate(uuid: UUID, playerShards: PlayerShards) {
|
||||
queueFor(uuid).trySend(PlayerBalanceChangedEvent(uuid, playerShards))
|
||||
}
|
||||
|
||||
fun register(listener: PlayerBalanceChangedListener) {
|
||||
listeners += listener
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package net.trueog.diamondbankog.api
|
||||
|
||||
import java.util.*
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
|
||||
data class PlayerBalanceChangedEvent(val uuid: UUID, val playerShards: PlayerShards)
|
||||
@@ -1,5 +0,0 @@
|
||||
package net.trueog.diamondbankog.api
|
||||
|
||||
interface PlayerBalanceChangedListener {
|
||||
fun onUpdate(event: PlayerBalanceChangedEvent)
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
# DiamondBank-OG API
|
||||
## Getting started
|
||||
In your `onEnable` function:\
|
||||
Kotlin:
|
||||
```kotlin
|
||||
val diamondBankAPIProvider = server.servicesManager.getRegistration(DiamondBankAPIKotlin::class.java)
|
||||
if (diamondBankAPIProvider == null) {
|
||||
logger.severe("DiamondBank-OG API is null")
|
||||
Bukkit.getPluginManager().disablePlugin(this)
|
||||
return
|
||||
}
|
||||
val diamondBankAPI = diamondBankAPIProvider.provider
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
final RegisteredServiceProvider<DiamondBankAPIJava> diamondBankAPIProvider = getServer().getServicesManager().getRegistration(DiamondBankAPIJava.class);
|
||||
if (diamondBankAPIProvider == null) {
|
||||
getLogger().severe("DiamondBank-OG API is null");
|
||||
Bukkit.getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
DiamondBankAPIJava diamondBankAPI = diamondBankAPIProvider.getProvider();
|
||||
```
|
||||
|
||||
## Available functions
|
||||
> [!IMPORTANT]
|
||||
> Always convert Diamonds to Shards before using the API using [`diamondsToShards`](#diamondstoshards).
|
||||
|
||||
Exceptions are only applicable when using the Java API, when using the Kotlin API the function will return a `Result` instead.
|
||||
### addToPlayerShards
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released.
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun addToPlayerBankShards(uuid: UUID, shards: ULong, transactionReason: String, notes: String?): Result<Unit>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public void addToPlayerBankShards(UUID uuid, long shards, String transactionReason, @Nullable String notes)
|
||||
```
|
||||
|
||||
### subtractFromPlayerBankShards
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.InsufficientBalanceException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun subtractFromPlayerBankShards(uuid: UUID, shards: ULong, transactionReason: String, notes: String?): Result<Unit>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public void subtractFromPlayerBankShards(UUID uuid, long shards, String transactionReason, @Nullable String notes)
|
||||
```
|
||||
|
||||
### getBankShards
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun getBankShards(uuid: UUID): Result<Long>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public long getBankShards(UUID uuid)
|
||||
```
|
||||
|
||||
### getInventoryShards
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun getInventoryShards(uuid: UUID): Result<Long>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public long getInventoryShards(UUID uuid)
|
||||
```
|
||||
|
||||
### getEnderChestShards
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun getEnderChestShards(uuid: UUID): Result<Long>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public long getEnderChestShards(UUID uuid)
|
||||
```
|
||||
|
||||
### getTotalShards
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun getTotalShards(uuid: UUID): Result<Long>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public long getTotalShards(UUID uuid)
|
||||
```
|
||||
|
||||
### getBaltop
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public Map<@Nullable UUID, Long> getBaltop(int offset)
|
||||
```
|
||||
|
||||
### consumeFromPlayer
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
* @throws DiamondBankException.InsufficientFundsException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun consumeFromPlayer(uuid: UUID, shards: ULong, transactionReason: String, notes: String?): Result<Unit>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public void consumeFromPlayer(UUID uuid, long shards, String transactionReason, @Nullable String notes)
|
||||
```
|
||||
|
||||
### playerPayPlayer
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: if the player has a transaction lock applied this function will block until its released
|
||||
*
|
||||
* This function also blocks for the database call, this is so you don't have to manually run .get() on a
|
||||
* CompletableFuture
|
||||
*
|
||||
* @param shards must not be negative
|
||||
* @param transactionReason the reason for this transaction for in the transaction log
|
||||
* @param notes any specifics for this transaction that may be nice to know for in the transaction log
|
||||
* @throws DiamondBankException.EconomyDisabledException
|
||||
* @throws DiamondBankException.SenderEqualToReceiverException
|
||||
* @throws DiamondBankException.InvalidPlayerException
|
||||
* @throws DiamondBankException.InsufficientFundsException
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
suspend fun playerPayPlayer(senderUuid: UUID, receiverUuid: UUID, shards: ULong, transactionReason: String, notes: String?): Result<Unit>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public void playerPayPlayer(UUID senderUuid, UUID receiverUuid, long shards, String transactionReason, @Nullable String notes)
|
||||
```
|
||||
|
||||
### registerEventListener
|
||||
```kotlin
|
||||
/**
|
||||
* Register a PlayerBalanceChangedListener to listen for player balance changes
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
fun registerEventListener(eventListener: PlayerBalanceChangedListener)
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public void registerEventListener(PlayerBalanceChangedListener eventListener)
|
||||
```
|
||||
|
||||
### diamondsToShards
|
||||
> [!NOTE]
|
||||
> This function also has an overload for a double.
|
||||
```kotlin
|
||||
/**
|
||||
* WARNING: This function can throw a MoreThanOneDecimalDigitRuntimeException,
|
||||
* make sure it's checked when handling arbitrary input
|
||||
*
|
||||
* Converts a Diamond float to Shards
|
||||
*/
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
fun diamondsToShards(diamonds: Float): Result<Long>
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public long diamondsToShards(float diamonds)
|
||||
```
|
||||
|
||||
### shardsToDiamonds
|
||||
> [!CAUTION]
|
||||
> This function should only be used for visual output, not calculations.
|
||||
```kotlin
|
||||
/** Converts Shards into a formatted Diamonds string */
|
||||
```
|
||||
Kotlin:
|
||||
```kotlin
|
||||
fun shardsToDiamonds(shards: Long): String
|
||||
```
|
||||
Java:
|
||||
```java
|
||||
public String shardsToDiamonds(long shards)
|
||||
```
|
||||
@@ -1,164 +0,0 @@
|
||||
package net.trueog.diamondbankog.autocompress
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.removeItemWithSameKey
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
internal object AutoCompress {
|
||||
fun compress(player: Player) {
|
||||
scope.launch {
|
||||
val worldName = player.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") {
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!player.hasPermission("diamondbank-og.compress")) {
|
||||
player.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to compress.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
transactionLock.withLockSuspend(player.uniqueId) {
|
||||
player.uniqueId.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread { InventorySnapshot.from(player.uniqueId, balanceManager) }
|
||||
|
||||
val shardsInInventory = inventorySnapshot.countShards().toInt()
|
||||
val diamondsInInventory = inventorySnapshot.countDiamonds().toInt()
|
||||
val diamondBlocksInInventory = inventorySnapshot.countDiamondBlocks().toInt()
|
||||
|
||||
var finalShards = shardsInInventory
|
||||
var finalDiamonds = diamondsInInventory
|
||||
var finalDiamondBlocks = diamondBlocksInInventory
|
||||
|
||||
finalDiamonds += finalShards / 9
|
||||
if (finalDiamonds - diamondsInInventory != 0) {
|
||||
val remainder = finalShards % 9
|
||||
finalShards = remainder
|
||||
}
|
||||
|
||||
finalDiamondBlocks += finalDiamonds / 9
|
||||
if (finalDiamondBlocks - diamondBlocksInInventory != 0) {
|
||||
val remainder = finalDiamonds % 9
|
||||
finalDiamonds = remainder
|
||||
}
|
||||
|
||||
val changeInShards = finalShards - shardsInInventory
|
||||
val changeInDiamonds = finalDiamonds - diamondsInInventory
|
||||
val changeInDiamondBlocks = finalDiamondBlocks - diamondBlocksInInventory
|
||||
|
||||
if (changeInDiamonds > 0) {
|
||||
val emptySlots = inventorySnapshot.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpaceDiamonds =
|
||||
inventorySnapshot.storageContents
|
||||
.filterNotNull()
|
||||
.filter { it.type == Material.DIAMOND }
|
||||
.sumOf { 64 - it.amount }
|
||||
|
||||
if (changeInDiamonds > emptySlots + leftOverSpaceDiamonds) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You do not have enough space in your inventory to compress all the Diamond currency items (<green>+$changeInDiamonds <aqua>Diamonds<red>)."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
}
|
||||
|
||||
if (changeInDiamondBlocks > 0) {
|
||||
val emptySlots = inventorySnapshot.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpaceDiamondBlocks =
|
||||
inventorySnapshot.storageContents
|
||||
.filterNotNull()
|
||||
.filter { it.type == Material.DIAMOND_BLOCK }
|
||||
.sumOf { 64 - it.amount }
|
||||
|
||||
if (changeInDiamondBlocks > emptySlots + leftOverSpaceDiamondBlocks) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You do not have enough space in your inventory to compress all the Diamond currency items (<green>+$changeInDiamondBlocks <aqua>Diamond Blocks<red>)."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
}
|
||||
|
||||
if (changeInShards < 0) {
|
||||
val removeMap =
|
||||
inventorySnapshot.removeItemWithSameKey(Shard.createItemStack(abs(changeInShards)))
|
||||
if (removeMap.isNotEmpty()) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your inventory."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
}
|
||||
|
||||
if (changeInDiamonds > 0) {
|
||||
val addMap = inventorySnapshot.addItem(ItemStack(Material.DIAMOND, changeInDiamonds))
|
||||
if (addMap.isNotEmpty()) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your inventory."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
} else if (changeInDiamonds < 0) {
|
||||
val removeMap = inventorySnapshot.removeItem(ItemStack(Material.DIAMOND, abs(changeInDiamonds)))
|
||||
if (removeMap.isNotEmpty()) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your inventory."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
}
|
||||
|
||||
if (changeInDiamondBlocks > 0) {
|
||||
val addMap = inventorySnapshot.addItem(ItemStack(Material.DIAMOND_BLOCK, changeInDiamondBlocks))
|
||||
if (addMap.isNotEmpty()) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your inventory."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
} else if (changeInDiamondBlocks < 0) {
|
||||
val removeMap =
|
||||
inventorySnapshot.removeItem(ItemStack(Material.DIAMOND_BLOCK, abs(changeInDiamondBlocks)))
|
||||
if (removeMap.isNotEmpty()) {
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your inventory."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend
|
||||
}
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(player.uniqueId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package net.trueog.diamondbankog.autocompress.command
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.redis
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
internal class AutoCompress : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (economyDisabled) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (sender !is Player) {
|
||||
sender.sendMessage("You can only execute this command as a player.")
|
||||
return true
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.compress")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (redis.getValue("diamondbankog:${sender.uniqueId}:autocompress") == "true") {
|
||||
redis.setValue("diamondbankog:${sender.uniqueId}:autocompress", "false")
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <green>Successfully turned auto-compress <red>off<green>.")
|
||||
)
|
||||
} else {
|
||||
if (redis.getValue("diamondbankog:${sender.uniqueId}:autodeposit") == "true") {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <#FFA500>You cannot have both auto-compress and auto-deposit on at the same time, so auto-deposit has been turned <red>off<#FFA500>."
|
||||
)
|
||||
)
|
||||
redis.setValue("diamondbankog:${sender.uniqueId}:autodeposit", "false")
|
||||
}
|
||||
redis.setValue("diamondbankog:${sender.uniqueId}:autocompress", "true")
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <green>Successfully turned auto-compress on."))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package net.trueog.diamondbankog.autodeposit
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.util.ErrorHandler
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Item
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
internal object AutoDeposit {
|
||||
fun deposit(player: Player, item: Item, remaining: Int) {
|
||||
val worldName = player.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") {
|
||||
return
|
||||
}
|
||||
|
||||
if (!player.hasPermission("diamondbank-og.deposit")) {
|
||||
player.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to deposit."))
|
||||
return
|
||||
}
|
||||
|
||||
val itemStack = item.itemStack.clone().apply { amount += remaining }
|
||||
val shards =
|
||||
if (itemStack.type == Material.DIAMOND_BLOCK) {
|
||||
(itemStack.amount * 9 * 9).toLong()
|
||||
} else if (itemStack.type == Material.DIAMOND) {
|
||||
(itemStack.amount * 9).toLong()
|
||||
} else if (Shard.isShardItem(itemStack)) {
|
||||
itemStack.amount.toLong()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
itemStack.amount = 0
|
||||
item.itemStack = itemStack
|
||||
|
||||
scope.launch {
|
||||
transactionLock.withLockSuspend(player.uniqueId) {
|
||||
balanceManager.addToBankShards(player.uniqueId, shards).getOrElse {
|
||||
ErrorHandler.handleError(it)
|
||||
player.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>A severe error has occurred. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@withLockSuspend
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package net.trueog.diamondbankog.autodeposit.command
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.redis
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
internal class AutoDeposit : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (economyDisabled) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (sender !is Player) {
|
||||
sender.sendMessage("You can only execute this command as a player.")
|
||||
return true
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.deposit")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (redis.getValue("diamondbankog:${sender.uniqueId}:autodeposit") == "true") {
|
||||
redis.setValue("diamondbankog:${sender.uniqueId}:autodeposit", "false")
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <green>Successfully turned auto-deposit <red>off<green>.")
|
||||
)
|
||||
} else {
|
||||
if (redis.getValue("diamondbankog:${sender.uniqueId}:autocompress") == "true") {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <#FFA500>You cannot have both auto-deposit and auto-compress on at the same time, so auto-compress has been turned <red>off<#FFA500>."
|
||||
)
|
||||
)
|
||||
redis.setValue("diamondbankog:${sender.uniqueId}:autocompress", "false")
|
||||
}
|
||||
redis.setValue("diamondbankog:${sender.uniqueId}:autodeposit", "true")
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <green>Successfully turned auto-deposit on."))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance
|
||||
|
||||
import java.util.*
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
|
||||
interface BalanceManager {
|
||||
suspend fun setPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit>
|
||||
|
||||
suspend fun addToBankShards(uuid: UUID, shards: Long): Result<Unit>
|
||||
|
||||
suspend fun subtractFromBankShards(uuid: UUID, shards: Long): Result<Unit>
|
||||
|
||||
suspend fun transferBankShards(
|
||||
sender: UUID,
|
||||
receiver: UUID,
|
||||
shardsToSubtractFromSender: Long,
|
||||
shardsToAddToReceiver: Long,
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun getBankShards(uuid: UUID): Result<Long>
|
||||
|
||||
suspend fun getInventoryShards(uuid: UUID): Result<Long>
|
||||
|
||||
suspend fun getEnderChestShards(uuid: UUID): Result<Long>
|
||||
|
||||
suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long>
|
||||
|
||||
suspend fun getTotalShards(uuid: UUID): Result<Long>
|
||||
|
||||
suspend fun getAllShards(uuid: UUID): Result<PlayerShards>
|
||||
|
||||
suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>>
|
||||
|
||||
suspend fun getBaltopWithUuid(uuid: UUID): Result<Pair<Map<UUID?, Long>, Long>>
|
||||
|
||||
suspend fun getNumberOfRows(): Result<Long>
|
||||
|
||||
suspend fun insertTransactionLog(
|
||||
playerUuid: UUID,
|
||||
transferredShards: Long,
|
||||
playerToUuid: UUID?,
|
||||
transactionReason: String,
|
||||
notes: String?,
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun hasEntry(uuid: UUID): Result<Boolean>
|
||||
|
||||
suspend fun cacheForPlayer(uuid: UUID): Result<Unit>
|
||||
|
||||
suspend fun removeCacheForPlayer(uuid: UUID)
|
||||
|
||||
fun shutdown()
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance
|
||||
|
||||
interface BalanceManagerFactory {
|
||||
fun create(): BalanceManager?
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
import net.trueog.diamondbankog.DiamondBankException.InvalidArgumentException
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
|
||||
internal class Cache {
|
||||
private val bankBalanceCache = Object2LongOpenHashMap<UUID>().apply { defaultReturnValue(-1L) }
|
||||
private val bankBalanceCacheLock = ReentrantReadWriteLock()
|
||||
private val inventoryBalanceCache = Object2LongOpenHashMap<UUID>().apply { defaultReturnValue(-1L) }
|
||||
private val inventoryBalanceCacheLock = ReentrantReadWriteLock()
|
||||
private val enderChestBalanceCache = Object2LongOpenHashMap<UUID>().apply { defaultReturnValue(-1L) }
|
||||
private val enderChestBalanceCacheLock = ReentrantReadWriteLock()
|
||||
|
||||
fun setBalance(uuid: UUID, value: Long, type: ShardType): Result<Unit> {
|
||||
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
|
||||
when (type) {
|
||||
ShardType.BANK -> {
|
||||
bankBalanceCacheLock.write { bankBalanceCache.put(uuid, value) }
|
||||
}
|
||||
|
||||
ShardType.INVENTORY -> {
|
||||
inventoryBalanceCacheLock.write { inventoryBalanceCache.put(uuid, value) }
|
||||
}
|
||||
|
||||
ShardType.ENDER_CHEST -> {
|
||||
enderChestBalanceCacheLock.write { enderChestBalanceCache.put(uuid, value) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
fun getBalance(uuid: UUID, type: ShardType): Result<Long> {
|
||||
return when (type) {
|
||||
ShardType.BANK -> {
|
||||
bankBalanceCacheLock.read { Result.success(bankBalanceCache.getLong(uuid)) }
|
||||
}
|
||||
|
||||
ShardType.INVENTORY -> {
|
||||
inventoryBalanceCacheLock.read { Result.success(inventoryBalanceCache.getLong(uuid)) }
|
||||
}
|
||||
|
||||
ShardType.ENDER_CHEST -> {
|
||||
enderChestBalanceCacheLock.read { Result.success(enderChestBalanceCache.getLong(uuid)) }
|
||||
}
|
||||
|
||||
else -> Result.failure(InvalidArgumentException())
|
||||
}
|
||||
}
|
||||
|
||||
fun removeBalance(uuid: UUID, type: ShardType): Result<Unit> {
|
||||
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
|
||||
when (type) {
|
||||
ShardType.BANK -> {
|
||||
bankBalanceCacheLock.write { bankBalanceCache.removeLong(uuid) }
|
||||
}
|
||||
|
||||
ShardType.INVENTORY -> {
|
||||
inventoryBalanceCacheLock.write { inventoryBalanceCache.removeLong(uuid) }
|
||||
}
|
||||
|
||||
ShardType.ENDER_CHEST -> {
|
||||
enderChestBalanceCacheLock.write { enderChestBalanceCache.removeLong(uuid) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
fun removeAll(uuid: UUID) {
|
||||
removeBalance(uuid, ShardType.BANK)
|
||||
removeBalance(uuid, ShardType.INVENTORY)
|
||||
removeBalance(uuid, ShardType.ENDER_CHEST)
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.trueog.diamondbankog.DiamondBankException.EconomyDisabledException
|
||||
import net.trueog.diamondbankog.DiamondBankException.InsufficientBalanceException
|
||||
import net.trueog.diamondbankog.DiamondBankException.InvalidArgumentException
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.debug
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.persistence.PostgreSQL
|
||||
|
||||
internal class CachingBalanceManager private constructor() : BalanceManager {
|
||||
private val cache = Cache()
|
||||
private lateinit var postgreSQL: PostgreSQL
|
||||
|
||||
companion object : BalanceManagerFactory {
|
||||
override fun create(): BalanceManager? {
|
||||
val cachingBalanceManager = CachingBalanceManager()
|
||||
cachingBalanceManager.postgreSQL = PostgreSQL.create() ?: return null
|
||||
return cachingBalanceManager
|
||||
}
|
||||
}
|
||||
|
||||
private val mutexMap = ConcurrentHashMap<UUID, ConcurrentHashMap<ShardType, Mutex>>()
|
||||
|
||||
private suspend fun <T> withLock(uuid: UUID, vararg types: ShardType, block: suspend () -> T): T {
|
||||
val shardTypeToMutex = mutexMap.computeIfAbsent(uuid) { ConcurrentHashMap() }
|
||||
val mutexes = types.sorted().map { shardTypeToMutex.computeIfAbsent(it) { Mutex() } }
|
||||
return mutexes.foldRight(block) { mutex, acc -> { mutex.withLock { acc() } } }()
|
||||
}
|
||||
|
||||
private suspend fun <T> withLock(type: ShardType, vararg uuids: UUID, block: suspend () -> T): T {
|
||||
val mutexes =
|
||||
uuids.sorted().map { uuid ->
|
||||
mutexMap.computeIfAbsent(uuid) { ConcurrentHashMap() }.computeIfAbsent(type) { Mutex() }
|
||||
}
|
||||
return mutexes.foldRight(block) { mutex, acc -> { mutex.withLock { acc() } } }()
|
||||
}
|
||||
|
||||
override suspend fun setPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
|
||||
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return withLock(uuid, type) {
|
||||
postgreSQL.setPlayerShards(uuid, shards, type).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
cache.setBalance(uuid, shards, type)
|
||||
return@withLock Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addToPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
|
||||
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return withLock(uuid, type) {
|
||||
val currentBalance =
|
||||
postgreSQL.getShardTypeShards(uuid, type).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
if (currentBalance + shards < 0) {
|
||||
return@withLock Result.failure(InsufficientBalanceException(currentBalance))
|
||||
}
|
||||
|
||||
val newBalance =
|
||||
postgreSQL.addToPlayerShards(uuid, shards, type).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
cache.setBalance(uuid, newBalance, type).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
return@withLock Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addToBankShards(uuid: UUID, shards: Long): Result<Unit> =
|
||||
addToPlayerShards(uuid, shards, ShardType.BANK)
|
||||
|
||||
override suspend fun subtractFromBankShards(uuid: UUID, shards: Long): Result<Unit> =
|
||||
addToPlayerShards(uuid, -shards, ShardType.BANK)
|
||||
|
||||
override suspend fun transferBankShards(
|
||||
sender: UUID,
|
||||
receiver: UUID,
|
||||
shardsToSubtractFromSender: Long,
|
||||
shardsToAddToReceiver: Long,
|
||||
): Result<Unit> {
|
||||
require(shardsToSubtractFromSender >= 0) { "shardsToSubtractFromSender must not be negative" }
|
||||
require(shardsToAddToReceiver >= 0) { "shardsToAddToReceiver must not be negative" }
|
||||
require(sender != receiver) { "sender and receiver must not be equal" }
|
||||
if (economyDisabled) return Result.failure(EconomyDisabledException())
|
||||
|
||||
return withLock(ShardType.BANK, sender, receiver) {
|
||||
val currentBalanceFrom =
|
||||
postgreSQL.getShardTypeShards(sender, ShardType.BANK).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
if (currentBalanceFrom < shardsToSubtractFromSender) {
|
||||
return@withLock Result.failure(InsufficientBalanceException(currentBalanceFrom))
|
||||
}
|
||||
|
||||
postgreSQL
|
||||
.transferBankShards(sender, receiver, shardsToSubtractFromSender, shardsToAddToReceiver)
|
||||
.getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
.forEach { (uuid, shards) -> cache.setBalance(uuid, shards, ShardType.BANK) }
|
||||
return@withLock Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getBankShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.BANK)
|
||||
|
||||
override suspend fun getInventoryShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.INVENTORY)
|
||||
|
||||
override suspend fun getEnderChestShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.ENDER_CHEST)
|
||||
|
||||
override suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long> {
|
||||
val cacheBalance =
|
||||
cache.getBalance(uuid, type).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
if (cacheBalance != -1L) return Result.success(cacheBalance)
|
||||
|
||||
return withLock(uuid, type) {
|
||||
if (debug) {
|
||||
plugin.logger.info("Cache miss for $uuid!")
|
||||
}
|
||||
val dbBalance =
|
||||
postgreSQL.getShardTypeShards(uuid, type).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
cache.setBalance(uuid, dbBalance, type)
|
||||
return@withLock Result.success(dbBalance)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTotalShards(uuid: UUID): Result<Long> = getAllShards(uuid).map { it.total }
|
||||
|
||||
override suspend fun getAllShards(uuid: UUID): Result<PlayerShards> {
|
||||
val cacheBankBalance =
|
||||
cache.getBalance(uuid, ShardType.BANK).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val cacheInventoryBalance =
|
||||
cache.getBalance(uuid, ShardType.INVENTORY).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val cacheEnderChestBalance =
|
||||
cache.getBalance(uuid, ShardType.ENDER_CHEST).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
|
||||
if (cacheBankBalance != -1L && cacheInventoryBalance != -1L && cacheEnderChestBalance != -1L) {
|
||||
return Result.success(PlayerShards(cacheBankBalance, cacheInventoryBalance, cacheEnderChestBalance))
|
||||
}
|
||||
|
||||
return withLock(uuid, ShardType.BANK, ShardType.INVENTORY, ShardType.ENDER_CHEST) {
|
||||
if (debug) {
|
||||
plugin.logger.info("Cache miss for $uuid!")
|
||||
}
|
||||
val dbPlayerShards =
|
||||
postgreSQL.getAllShards(uuid).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
cache.setBalance(uuid, dbPlayerShards.bank, ShardType.BANK)
|
||||
cache.setBalance(uuid, dbPlayerShards.inventory, ShardType.INVENTORY)
|
||||
cache.setBalance(uuid, dbPlayerShards.enderChest, ShardType.ENDER_CHEST)
|
||||
return@withLock Result.success(dbPlayerShards)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>> {
|
||||
require(offset >= 0) { "offset must not be negative" }
|
||||
return postgreSQL.getBaltop(offset)
|
||||
}
|
||||
|
||||
override suspend fun getBaltopWithUuid(uuid: UUID): Result<Pair<Map<UUID?, Long>, Long>> =
|
||||
postgreSQL.getBaltopWithUuid(uuid)
|
||||
|
||||
override suspend fun getNumberOfRows(): Result<Long> = postgreSQL.getNumberOfRows()
|
||||
|
||||
override suspend fun insertTransactionLog(
|
||||
playerUuid: UUID,
|
||||
transferredShards: Long,
|
||||
playerToUuid: UUID?,
|
||||
transactionReason: String,
|
||||
notes: String?,
|
||||
): Result<Unit> =
|
||||
postgreSQL.insertTransactionLog(playerUuid, transferredShards, playerToUuid, transactionReason, notes)
|
||||
|
||||
override suspend fun hasEntry(uuid: UUID): Result<Boolean> = postgreSQL.hasEntry(uuid)
|
||||
|
||||
override suspend fun cacheForPlayer(uuid: UUID) =
|
||||
withLock(uuid, ShardType.BANK, ShardType.INVENTORY, ShardType.ENDER_CHEST) {
|
||||
val playerShards =
|
||||
postgreSQL.getAllShards(uuid).getOrElse {
|
||||
return@withLock Result.failure(it)
|
||||
}
|
||||
cache.setBalance(uuid, playerShards.bank, ShardType.BANK)
|
||||
cache.setBalance(uuid, playerShards.inventory, ShardType.INVENTORY)
|
||||
cache.setBalance(uuid, playerShards.enderChest, ShardType.ENDER_CHEST)
|
||||
return@withLock Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun removeCacheForPlayer(uuid: UUID) =
|
||||
withLock(uuid, ShardType.BANK, ShardType.INVENTORY, ShardType.ENDER_CHEST) { cache.removeAll(uuid) }
|
||||
|
||||
override fun shutdown() {
|
||||
postgreSQL.disconnect().get()
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance.command
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.util.PlayerPrefix.getPrefix
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
internal class Balance(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
scope.launch {
|
||||
if (economyDisabled) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.balance")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null) return@launch
|
||||
|
||||
if (args.isEmpty() && sender !is Player) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Please provide that name or UUID of the player that you want to check the balance of."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val balancePlayer =
|
||||
if (args.isEmpty()) {
|
||||
sender as Player
|
||||
} else {
|
||||
if (!sender.hasPermission("diamondbank-og.balance.others")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You do not have permission to use this command in that way."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val otherPlayer = CommonOperations.getPlayerUsingUuidOrName(args[0])
|
||||
if (!otherPlayer.hasPlayedBefore()) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>That player doesn't exist."))
|
||||
return@launch
|
||||
}
|
||||
otherPlayer
|
||||
}
|
||||
val balance =
|
||||
balanceManager.getAllShards(balancePlayer.uniqueId).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get ${
|
||||
if (if (sender is Player) sender.uniqueId == balancePlayer.uniqueId else false) "your" else "their"
|
||||
} balance."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val totalBalance = balance.bank + balance.inventory + balance.enderChest
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>${
|
||||
if (if (sender is Player) sender.uniqueId == balancePlayer.uniqueId else false) "Your Balance" else "Balance of ${
|
||||
getPrefix(
|
||||
balancePlayer.uniqueId,
|
||||
DiamondBankOG.luckPerms,
|
||||
)
|
||||
}${balancePlayer.name}"
|
||||
}<reset><green>:\n" +
|
||||
"Bank: ${CommonOperations.shardsToDiamondsFull(balance.bank)}\n" +
|
||||
"<green>Inventory: ${CommonOperations.shardsToDiamondsFull(balance.inventory)}\n" +
|
||||
"<green>Ender Chest: ${CommonOperations.shardsToDiamondsFull(balance.enderChest)}\n" +
|
||||
"<bold><green>Total: ${CommonOperations.shardsToDiamondsFull(totalBalance)}"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance.command
|
||||
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.util.PlayerPrefix.getPrefix
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.OfflinePlayer
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class Balancetop(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
scope.launch {
|
||||
if (economyDisabled) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.balancetop")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null) return@launch
|
||||
if (args.size > 1) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Please (only) optionally provide the page number or the name or UUID of the player you want to see the position of."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
var offset = 0
|
||||
var index = 1
|
||||
var player: OfflinePlayer? = null
|
||||
if (args.size == 1) {
|
||||
try {
|
||||
index = args[0].toInt()
|
||||
offset = 9 * (index - 1)
|
||||
} catch (_: Exception) {
|
||||
player = CommonOperations.getPlayerUsingUuidOrName(args[0])
|
||||
if (!player.hasPlayedBefore()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>That player doesn't exist or hasn't joined this server before."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
val baltopWithUuid =
|
||||
balanceManager.getBaltopWithUuid(player.uniqueId).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val (baltop, offset) = baltopWithUuid
|
||||
|
||||
var baltopMessage = "${config.prefix} <aqua>Top Balances:<reset>"
|
||||
baltop.forEach {
|
||||
val uuid = it.key
|
||||
if (uuid == null) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
val playerName = player.name ?: player.uniqueId.toString()
|
||||
|
||||
val diamonds = CommonOperations.shardsToDiamonds(it.value)
|
||||
baltopMessage +=
|
||||
"\n<green>${baltopMessage.lines().size + offset}<reset>. ${if (playerName == player.name) "<italic>" else ""}${
|
||||
getPrefix(
|
||||
uuid,
|
||||
DiamondBankOG.luckPerms,
|
||||
)
|
||||
}$playerName<reset>: <yellow>$diamonds <aqua>Diamonds"
|
||||
}
|
||||
sender.sendMessage(mm.deserialize(baltopMessage))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val baltop =
|
||||
balanceManager.getBaltop(offset).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val numberOfRows =
|
||||
balanceManager.getNumberOfRows().getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (index > ceil(numberOfRows / 9.0)) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>The amount of pages only goes up to and including ${
|
||||
ceil(
|
||||
numberOfRows / 9.0
|
||||
).toLong()
|
||||
}."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
var baltopMessage =
|
||||
"${config.prefix} <aqua>Top Balances <#2ec2ae>(Page $index/${ceil(numberOfRows / 9.0).toLong()})<aqua>:<reset>"
|
||||
baltop.forEach {
|
||||
val uuid = it.key
|
||||
if (uuid == null) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val player = Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
val playerName = player.name ?: player.uniqueId.toString()
|
||||
|
||||
val diamonds = CommonOperations.shardsToDiamonds(it.value)
|
||||
baltopMessage +=
|
||||
"\n<green>${baltopMessage.lines().size + (9 * (index - 1))}<reset>. ${if (playerName == player.name) "<italic>" else ""}${
|
||||
getPrefix(
|
||||
uuid,
|
||||
DiamondBankOG.luckPerms,
|
||||
)
|
||||
}$playerName<reset>: <yellow>$diamonds <aqua>Diamonds"
|
||||
}
|
||||
|
||||
sender.sendMessage(mm.deserialize(baltopMessage))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance.command
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.PlayerPrefix.getPrefix
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
internal class SetBankBalance(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
scope.launch {
|
||||
if (economyDisabled) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.setbankbalance")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args.isNullOrEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You did not provide the name or the UUID of a player and the amount of <aqua>Shards<red>."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (args.size != 2) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Incorrect syntax. Usage: /setbankbal(ance) <player name or player uuid> <shards>."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val player = CommonOperations.getPlayerUsingUuidOrName(args[0])
|
||||
if (!player.hasPlayedBefore()) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>That player doesn't exist."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val balance: Long
|
||||
try {
|
||||
balance = args[1].toLong()
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>Invalid argument."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
balanceManager.setPlayerShards(player.uniqueId, balance, ShardType.BANK).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to set that player's balance."
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>Successfully set the balance of ${
|
||||
getPrefix(
|
||||
player.uniqueId,
|
||||
DiamondBankOG.luckPerms,
|
||||
)
|
||||
}${player.name}<reset> <green>to <yellow>$balance <aqua>${if (balance == 1L) "Shard" else "Shards"}<green>."
|
||||
)
|
||||
)
|
||||
|
||||
balanceManager
|
||||
.insertTransactionLog(
|
||||
player.uniqueId,
|
||||
balance,
|
||||
null,
|
||||
"Set Bank Balance",
|
||||
"Bank Balance set by ${if (sender is Player) "${sender.uniqueId}" else "console"}",
|
||||
)
|
||||
.getOrElse { handleError(it) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance.shard
|
||||
|
||||
data class PlayerShards(val bank: Long, val inventory: Long, val enderChest: Long) {
|
||||
val total = bank + inventory + enderChest
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance.shard
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.NamespacedKey
|
||||
import org.bukkit.enchantments.Enchantment
|
||||
import org.bukkit.inventory.ItemFlag
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.ShapedRecipe
|
||||
import org.bukkit.inventory.ShapelessRecipe
|
||||
import org.bukkit.persistence.PersistentDataType
|
||||
|
||||
internal object Shard {
|
||||
val namespacedKey = NamespacedKey("diamondbank-og", "diamondshard")
|
||||
|
||||
/** amount defaults to 1 */
|
||||
fun createItemStack(amount: Int = 1): ItemStack {
|
||||
val shard = ItemStack(Material.PRISMARINE_SHARD, amount)
|
||||
val shardMeta = shard.itemMeta
|
||||
shardMeta.displayName(mm.deserialize("<aqua>Diamond Shard"))
|
||||
shardMeta.persistentDataContainer.set(namespacedKey, PersistentDataType.STRING, "")
|
||||
shardMeta.addEnchant(Enchantment.DURABILITY, 1, false)
|
||||
shardMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS)
|
||||
shard.itemMeta = shardMeta
|
||||
return shard
|
||||
}
|
||||
|
||||
fun createCraftingRecipes() {
|
||||
val diamondToShardKey = NamespacedKey("diamondbank-og", "diamond_to_shard")
|
||||
val diamondToShardRecipe = ShapelessRecipe(diamondToShardKey, createItemStack(9))
|
||||
diamondToShardRecipe.addIngredient(Material.DIAMOND)
|
||||
Bukkit.addRecipe(diamondToShardRecipe)
|
||||
|
||||
val shardToDiamondKey = NamespacedKey("diamondbank-og", "shard_to_diamond")
|
||||
val shardToDiamondRecipe = ShapedRecipe(shardToDiamondKey, ItemStack(Material.DIAMOND))
|
||||
shardToDiamondRecipe.shape("SSS", "SSS", "SSS")
|
||||
shardToDiamondRecipe.setIngredient('S', createItemStack())
|
||||
Bukkit.addRecipe(shardToDiamondRecipe)
|
||||
}
|
||||
|
||||
fun isShardItem(it: ItemStack): Boolean {
|
||||
return it.itemMeta.persistentDataContainer.has(namespacedKey)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package net.trueog.diamondbankog.balance.shard
|
||||
|
||||
enum class ShardType(val string: String) {
|
||||
BANK("bank_shards"),
|
||||
INVENTORY("inventory_shards"),
|
||||
ENDER_CHEST("ender_chest_shards"),
|
||||
TOTAL("bank_shards, inventory_shards, ender_chest_shards"),
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
import java.util.*
|
||||
|
||||
class Balance : CommandExecutor {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
GlobalScope.launch {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.balance")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null) return@launch
|
||||
|
||||
if (sender !is Player) {
|
||||
if (args.isEmpty()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Please provide that name or UUID of the player that you want to check the balance of."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val otherPlayer = try {
|
||||
Bukkit.getPlayer(UUID.fromString(args[0])) ?: Bukkit.getOfflinePlayer(UUID.fromString(args[0]))
|
||||
} catch (_: Exception) {
|
||||
Bukkit.getPlayer(args[0]) ?: Bukkit.getOfflinePlayer(args[0])
|
||||
}
|
||||
if (!otherPlayer.hasPlayedBefore()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>That player doesn't exist."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val balance = DiamondBankOG.postgreSQL.getPlayerBalance(
|
||||
otherPlayer.uniqueId,
|
||||
BalanceType.ALL
|
||||
)
|
||||
if (balance.bankBalance == null || balance.inventoryBalance == null || balance.enderChestBalance == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get your balance."))
|
||||
return@launch
|
||||
}
|
||||
val totalBalance = balance.bankBalance + balance.inventoryBalance + balance.enderChestBalance
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("<green>Balance of <red>${otherPlayer.name}<green>: <yellow>$totalBalance <aqua>${if (totalBalance == 1) "Diamond" else "Diamonds"} <white>(<red>Bank: <yellow>${balance.bankBalance}<white>, <red>Inventory: <yellow>${balance.inventoryBalance}<white>, <red>Ender Chest: <yellow>${balance.enderChestBalance}<white>)."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val balancePlayer = if (args.isEmpty()) {
|
||||
sender
|
||||
} else {
|
||||
if (!sender.hasPermission("diamondbank-og.balance.others")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val otherPlayer = try {
|
||||
Bukkit.getPlayer(UUID.fromString(args[0])) ?: Bukkit.getOfflinePlayer(UUID.fromString(args[0]))
|
||||
} catch (_: Exception) {
|
||||
Bukkit.getPlayer(args[0]) ?: Bukkit.getOfflinePlayer(args[0])
|
||||
}
|
||||
if (!otherPlayer.hasPlayedBefore()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>That player doesn't exist."))
|
||||
return@launch
|
||||
}
|
||||
otherPlayer
|
||||
}
|
||||
val balance = DiamondBankOG.postgreSQL.getPlayerBalance(
|
||||
balancePlayer.uniqueId,
|
||||
BalanceType.ALL
|
||||
)
|
||||
if (balance.bankBalance == null || balance.inventoryBalance == null || balance.enderChestBalance == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get your balance."))
|
||||
return@launch
|
||||
}
|
||||
val totalBalance = balance.bankBalance + balance.inventoryBalance + balance.enderChestBalance
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("<green>Balance${if (balancePlayer.uniqueId != sender.uniqueId) " of <red>${balancePlayer.name}" else ""}<green>: <yellow>$totalBalance <aqua>${if (totalBalance == 1) "Diamond" else "Diamonds"} <white>(<red>Bank: <yellow>${balance.bankBalance}<white>, <red>Inventory: <yellow>${balance.inventoryBalance}<white>, <red>Ender Chest: <yellow>${balance.enderChestBalance}<white>)."))
|
||||
return@launch
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import kotlin.math.ceil
|
||||
|
||||
class Balancetop : CommandExecutor {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
GlobalScope.launch {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.balancetop")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null) return@launch
|
||||
if (args.size > 1) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Please (only) optionally provide the page number."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
var offset = 0
|
||||
var index = 1
|
||||
if (args.size == 1) {
|
||||
try {
|
||||
index = args[0].toInt()
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage("Invalid argument")
|
||||
return@launch
|
||||
}
|
||||
offset = 10 * (index - 1)
|
||||
}
|
||||
|
||||
val baltop = DiamondBankOG.postgreSQL.getBaltop(offset)
|
||||
if (baltop == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."))
|
||||
return@launch
|
||||
}
|
||||
val numberOfRows = DiamondBankOG.postgreSQL.getNumberOfRows()
|
||||
if (numberOfRows == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (index > ceil(numberOfRows / 10.0)) {
|
||||
sender.sendMessage("The amount of pages only goes up to $numberOfRows")
|
||||
return@launch
|
||||
}
|
||||
var baltopMessage =
|
||||
("<yellow>---- <gold>Balancetop <yellow>-- <gold>Page <red>$index<gold>/<red>${ceil(numberOfRows / 10.0).toLong()} <yellow>----<reset>")
|
||||
baltop.forEach {
|
||||
if (it.key == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get the information for balancetop."))
|
||||
return@launch
|
||||
}
|
||||
baltopMessage += "\n<red>${baltopMessage.lines().size + (10 * (index - 1))}<reset>. ${if (it.key == sender.name) "<red>" else ""}${it.key}<reset>, ${it.value}"
|
||||
}
|
||||
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize(baltopMessage))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.Helper
|
||||
import net.trueog.diamondbankog.Helper.PostgresFunction
|
||||
import net.trueog.diamondbankog.Helper.withdraw
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
class Deposit : CommandExecutor {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
GlobalScope.launch {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (sender !is Player) {
|
||||
sender.sendMessage("You can only execute this command as a player.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (DiamondBankOG.blockCommandsWithInventoryActionsFor.contains(sender.uniqueId)) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You are currently blocked from using /deposit."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val worldName = sender.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You cannot use /deposit <aqua>Diamonds <red>when in a minigame."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.deposit")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null || args.isEmpty()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to deposit."))
|
||||
return@launch
|
||||
}
|
||||
if (args.size != 1) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to deposit. Either a number or \"all\"."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val playerBalance =
|
||||
DiamondBankOG.postgreSQL.getPlayerBalance(sender.uniqueId, BalanceType.INVENTORY_BALANCE)
|
||||
if (playerBalance.inventoryBalance == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get your balance."))
|
||||
return@launch
|
||||
}
|
||||
if (playerBalance.inventoryBalance == 0) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You don't have any <aqua>Diamonds <red>to deposit."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
var amount = playerBalance.inventoryBalance
|
||||
if (args[0] != "all") {
|
||||
try {
|
||||
amount = args[0].toInt()
|
||||
if (amount < 0) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You cannot deposit a negative amount."))
|
||||
return@launch
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Invalid argument."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (amount > playerBalance.inventoryBalance) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have <yellow>$amount <aqua>${if (amount == 1) "Diamond" else "Diamonds"} <red>in your inventory."))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
var error = sender.inventory.withdraw(amount, playerBalance)
|
||||
if (error) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to deposit."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
error = DiamondBankOG.postgreSQL.addToPlayerBalance(
|
||||
sender.uniqueId,
|
||||
amount,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
sender.uniqueId,
|
||||
PostgresFunction.ADD_TO_PLAYER_BALANCE,
|
||||
amount,
|
||||
BalanceType.BANK_BALANCE,
|
||||
playerBalance,
|
||||
"deposit"
|
||||
)
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to deposit."))
|
||||
DiamondBankOG.blockInventoryFor.remove(sender.uniqueId)
|
||||
return@launch
|
||||
}
|
||||
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>Successfully deposited <yellow>$amount <aqua>${if (amount == 1) "Diamond" else "Diamonds"} <green>into your bank account."))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.sentry.Sentry
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.PostgreSQL
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
class DiamondBankReload : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (Config.load()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("<red>Failed to reload the config. Check the console for more information."))
|
||||
Bukkit.getPluginManager().disablePlugin(DiamondBankOG.plugin)
|
||||
return true
|
||||
}
|
||||
|
||||
if (Config.sentryEnabled) {
|
||||
try {
|
||||
Sentry.init { options ->
|
||||
options.dsn = Config.sentryDsn
|
||||
}
|
||||
DiamondBankOG.sentryEnabled = true
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.sentryEnabled = false
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Could not initialise Sentry. The Sentry(-compatible) DSN in your config might be invalid."))
|
||||
}
|
||||
}
|
||||
|
||||
DiamondBankOG.postgreSQL = PostgreSQL()
|
||||
try {
|
||||
DiamondBankOG.postgreSQL.initDB()
|
||||
} catch (e: Exception) {
|
||||
DiamondBankOG.plugin.logger.info(e.toString())
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("<red>Failed to reload the config. Check the console for more information."))
|
||||
Bukkit.getPluginManager().disablePlugin(DiamondBankOG.plugin)
|
||||
return true
|
||||
}
|
||||
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>Successfully reloaded the config."))
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.Helper
|
||||
import net.trueog.diamondbankog.Helper.PostgresFunction
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
import java.util.*
|
||||
|
||||
class Pay : CommandExecutor {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
GlobalScope.launch {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (sender !is Player) {
|
||||
sender.sendMessage("You can only execute this command as a player.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (DiamondBankOG.blockCommandsWithInventoryActionsFor.contains(sender.uniqueId)) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You are currently blocked from using /pay."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val worldName = sender.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You cannot use /pay <aqua>Diamonds <red>when in a minigame."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.pay")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null || args.isEmpty()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You did not provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."))
|
||||
return@launch
|
||||
}
|
||||
if (args.size != 2) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val receiver = try {
|
||||
Bukkit.getPlayer(UUID.fromString(args[0])) ?: Bukkit.getOfflinePlayer(UUID.fromString(args[0]))
|
||||
} catch (_: Exception) {
|
||||
Bukkit.getPlayer(args[0]) ?: Bukkit.getOfflinePlayer(args[0])
|
||||
}
|
||||
|
||||
if (!receiver.hasPlayedBefore()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>That player doesn't exist."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
var amount = -1
|
||||
if (args[1] != "all") {
|
||||
try {
|
||||
amount = args[1].toInt()
|
||||
if (amount < 0) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You cannot pay a negative amount."))
|
||||
return@launch
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Invalid argument."))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val withdrawnAmount = Helper.withdrawFromPlayer(sender, amount) ?: return@launch
|
||||
|
||||
val error = DiamondBankOG.postgreSQL.addToPlayerBalance(
|
||||
receiver.uniqueId,
|
||||
amount,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
sender.uniqueId,
|
||||
PostgresFunction.ADD_TO_PLAYER_BALANCE, amount, BalanceType.BANK_BALANCE,
|
||||
null, "pay"
|
||||
)
|
||||
}
|
||||
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>Successfully paid <yellow>$withdrawnAmount <aqua>${if (withdrawnAmount == 1) "Diamond" else "Diamonds"} <green>to <red>${receiver.name}<green>."))
|
||||
|
||||
if (receiver.isOnline) {
|
||||
val receiverPlayer = receiver.player ?: return@launch
|
||||
receiverPlayer.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>${sender.name} has paid you <yellow>$withdrawnAmount <aqua>${if (withdrawnAmount == 1) "Diamond" else "Diamonds"}<green>."))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import java.util.*
|
||||
|
||||
class SetBankBalance : CommandExecutor {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
GlobalScope.launch {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.setbankbalance")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null || args.isEmpty()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You did not provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."))
|
||||
return@launch
|
||||
}
|
||||
if (args.size != 2) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val player = try {
|
||||
Bukkit.getPlayer(UUID.fromString(args[0])) ?: Bukkit.getOfflinePlayer(UUID.fromString(args[0]))
|
||||
} catch (_: Exception) {
|
||||
Bukkit.getPlayer(args[0]) ?: Bukkit.getOfflinePlayer(args[0])
|
||||
}
|
||||
if (!player.hasPlayedBefore()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>That player doesn't exist."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val balance: Int
|
||||
try {
|
||||
balance = args[1].toInt()
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Invalid argument."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val error =
|
||||
DiamondBankOG.postgreSQL.setPlayerBalance(player.uniqueId, balance, BalanceType.BANK_BALANCE)
|
||||
if (error) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to set that player's balance."))
|
||||
return@launch
|
||||
}
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>Successfully set the balance of <red>${player.name} <green>to <yellow>$balance <aqua>${if (balance == 1) "Diamond" else "Diamonds"}<green>."))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.Config
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.Helper
|
||||
import net.trueog.diamondbankog.Helper.PostgresFunction
|
||||
import net.trueog.diamondbankog.PostgreSQL.BalanceType
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
class Withdraw : CommandExecutor {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
GlobalScope.launch {
|
||||
if (DiamondBankOG.economyDisabled) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>The economy is disabled because of a severe error. Please notify a staff member."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (sender !is Player) {
|
||||
sender.sendMessage("You can only execute this command as a player.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (DiamondBankOG.blockCommandsWithInventoryActionsFor.contains(sender.uniqueId)) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You are currently blocked from using /withdraw."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val worldName = sender.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You cannot use /withdraw when in a minigame."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.withdraw")) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You do not have permission to use this command."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (args == null || args.isEmpty()) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to withdraw."))
|
||||
return@launch
|
||||
}
|
||||
if (args.size != 1) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to withdraw. Either a number or \"all\"."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
var amount = -1
|
||||
if (args[0] != "all") {
|
||||
try {
|
||||
amount = args[0].toInt()
|
||||
if (amount < 0) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You cannot withdraw a negative amount."))
|
||||
return@launch
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Invalid argument."))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val playerBalance =
|
||||
DiamondBankOG.postgreSQL.getPlayerBalance(sender.uniqueId, BalanceType.BANK_BALANCE)
|
||||
if (playerBalance.bankBalance == null) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to get your balance."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (amount == -1) amount = playerBalance.bankBalance
|
||||
|
||||
if (amount > playerBalance.bankBalance) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Cannot withdraw <yellow>$amount <aqua>${if (amount == 1) "Diamond" else "Diamonds"} <red>because your bank only contains <yellow>${playerBalance.bankBalance} <aqua>${if (playerBalance.bankBalance == 1) "diamond" else "diamonds"}<red>."))
|
||||
return@launch
|
||||
}
|
||||
|
||||
DiamondBankOG.blockInventoryFor.add(sender.uniqueId)
|
||||
val emptySlots = sender.inventory.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpace = sender.inventory.storageContents.filterNotNull().filter { it.type == Material.DIAMOND }
|
||||
.sumOf { 64 - it.amount }
|
||||
if (amount > (emptySlots + leftOverSpace)) {
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>You don't have enough inventory space to withdraw <yellow>$amount <aqua>${if (amount == 1) "Diamond" else "Diamonds"}<red>."))
|
||||
DiamondBankOG.blockInventoryFor.remove(sender.uniqueId)
|
||||
return@launch
|
||||
}
|
||||
|
||||
var error = DiamondBankOG.postgreSQL.subtractFromPlayerBalance(
|
||||
sender.uniqueId,
|
||||
amount,
|
||||
BalanceType.BANK_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
sender.uniqueId,
|
||||
PostgresFunction.SUBTRACT_FROM_PLAYER_BALANCE,
|
||||
amount,
|
||||
BalanceType.BANK_BALANCE,
|
||||
playerBalance,
|
||||
"withdraw"
|
||||
)
|
||||
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to withdraw."))
|
||||
DiamondBankOG.blockInventoryFor.remove(sender.uniqueId)
|
||||
return@launch
|
||||
}
|
||||
|
||||
error = DiamondBankOG.postgreSQL.addToPlayerBalance(
|
||||
sender.uniqueId,
|
||||
amount,
|
||||
BalanceType.INVENTORY_BALANCE
|
||||
)
|
||||
if (error) {
|
||||
Helper.handleError(
|
||||
sender.uniqueId,
|
||||
PostgresFunction.ADD_TO_PLAYER_BALANCE,
|
||||
amount,
|
||||
BalanceType.INVENTORY_BALANCE,
|
||||
playerBalance,
|
||||
"withdraw"
|
||||
)
|
||||
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <red>Something went wrong while trying to withdraw."))
|
||||
DiamondBankOG.blockInventoryFor.remove(sender.uniqueId)
|
||||
return@launch
|
||||
}
|
||||
sender.inventory.addItem(ItemStack(Material.DIAMOND, amount))
|
||||
DiamondBankOG.blockInventoryFor.remove(sender.uniqueId)
|
||||
sender.sendMessage(DiamondBankOG.mm.deserialize("${Config.prefix}<reset>: <green>Successfully withdrew <yellow>$amount <aqua>${if (amount == 1) "Diamond" else "Diamonds"} <green>from your bank account."))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package net.trueog.diamondbankog.config
|
||||
|
||||
interface Config {
|
||||
val prefix: String
|
||||
val postgresHost: String
|
||||
val postgresPort: Int
|
||||
val postgresDatabase: String
|
||||
val postgresUser: String
|
||||
val postgresPassword: String?
|
||||
val postgresMaxActiveConnections: Int
|
||||
val redisHost: String
|
||||
val redisPort: Int
|
||||
val redisDatabase: Int
|
||||
val redisPassword: String?
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package net.trueog.diamondbankog.config
|
||||
|
||||
interface ConfigFactory {
|
||||
fun create(): Config?
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package net.trueog.diamondbankog.config
|
||||
|
||||
import java.io.File
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
import org.bukkit.configuration.file.YamlConfiguration
|
||||
|
||||
internal class YamlConfig
|
||||
private constructor(
|
||||
override val prefix: String,
|
||||
override val postgresHost: String,
|
||||
override val postgresPort: Int,
|
||||
override val postgresDatabase: String,
|
||||
override val postgresUser: String,
|
||||
override val postgresPassword: String?,
|
||||
override val postgresMaxActiveConnections: Int,
|
||||
override val redisHost: String,
|
||||
override val redisPort: Int,
|
||||
override val redisDatabase: Int,
|
||||
override val redisPassword: String?,
|
||||
) : Config {
|
||||
companion object : ConfigFactory {
|
||||
override fun create(): YamlConfig? {
|
||||
plugin.saveDefaultConfig()
|
||||
val file = File(plugin.dataFolder, "config.yml")
|
||||
val yamlConfig = YamlConfiguration.loadConfiguration(file)
|
||||
|
||||
val prefix = yamlConfig.parseKeyAs<String>("prefix")
|
||||
|
||||
val postgresHost = yamlConfig.parseKeyAs<String>("postgresHost")
|
||||
val postgresPort = yamlConfig.parseKeyAs<Int>("postgresPort")
|
||||
val postgresDatabase = yamlConfig.parseKeyAs<String>("postgresDatabase")
|
||||
val postgresUser = yamlConfig.parseKeyAs<String>("postgresUser")
|
||||
val postgresPassword = yamlConfig.parseKeyAs<String?>("postgresPassword")
|
||||
val postgresMaxActiveConnections = yamlConfig.parseKeyAs<Int>("postgresMaxActiveConnections")
|
||||
|
||||
val redisHost = yamlConfig.parseKeyAs<String>("redisHost")
|
||||
val redisPort = yamlConfig.parseKeyAs<Int>("redisPort")
|
||||
val redisDatabase = yamlConfig.parseKeyAs<Int>("redisDatabase")
|
||||
val redisPassword = yamlConfig.parseKeyAs<String?>("redisPassword")
|
||||
|
||||
if (
|
||||
prefix == null ||
|
||||
postgresHost == null ||
|
||||
postgresPort == null ||
|
||||
postgresDatabase == null ||
|
||||
postgresUser == null ||
|
||||
postgresMaxActiveConnections == null ||
|
||||
redisHost == null ||
|
||||
redisPort == null ||
|
||||
redisDatabase == null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return YamlConfig(
|
||||
prefix,
|
||||
postgresHost,
|
||||
postgresPort,
|
||||
postgresDatabase,
|
||||
postgresUser,
|
||||
postgresPassword,
|
||||
postgresMaxActiveConnections,
|
||||
redisHost,
|
||||
redisPort,
|
||||
redisDatabase,
|
||||
redisPassword,
|
||||
)
|
||||
}
|
||||
|
||||
inline fun <reified T> YamlConfiguration.parseKeyAs(key: String): T? {
|
||||
if (!this.contains(key) && null is T) return null
|
||||
return this.get(key) as? T
|
||||
?: run {
|
||||
plugin.logger.severe("Failed to parse config option \"$key\" as ${T::class.simpleName}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package net.trueog.diamondbankog.config.command
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
import net.trueog.diamondbankog.balance.CachingBalanceManager
|
||||
import net.trueog.diamondbankog.config.YamlConfig
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class DiamondBankReload : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (!sender.hasPermission("diamondbank-og.admin")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
config =
|
||||
YamlConfig.create()
|
||||
?: run {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Failed to reload the config. Check the console for more information."
|
||||
)
|
||||
)
|
||||
Bukkit.getPluginManager().disablePlugin(plugin)
|
||||
return true
|
||||
}
|
||||
|
||||
balanceManager =
|
||||
CachingBalanceManager.create()
|
||||
?: run {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Failed to reload the config. Check the console for more information."
|
||||
)
|
||||
)
|
||||
Bukkit.getPluginManager().disablePlugin(plugin)
|
||||
return true
|
||||
}
|
||||
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <green>Successfully reloaded the config."))
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package net.trueog.diamondbankog.economy.command
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class DisableEconomy : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (!sender.hasPermission("diamondbank-og.admin")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
economyDisabled = true
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <green>Successfully <red>disabled <green>the economy.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package net.trueog.diamondbankog.economy.command
|
||||
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class EnableEconomy : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (!sender.hasPermission("diamondbank-og.admin")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
economyDisabled = false
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <green>Successfully enabled the economy."))
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
package net.trueog.diamondbankog.persistence
|
||||
|
||||
import com.github.jasync.sql.db.Connection
|
||||
import com.github.jasync.sql.db.asSuspending
|
||||
import com.github.jasync.sql.db.pool.ConnectionPool
|
||||
import com.github.jasync.sql.db.postgresql.PostgreSQLConnection
|
||||
import com.github.jasync.sql.db.postgresql.PostgreSQLConnectionBuilder
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.future.future
|
||||
import net.trueog.diamondbankog.DiamondBankException.*
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import org.flywaydb.core.Flyway
|
||||
import org.postgresql.ds.PGSimpleDataSource
|
||||
|
||||
internal class PostgreSQL private constructor() {
|
||||
private lateinit var pool: ConnectionPool<PostgreSQLConnection>
|
||||
|
||||
class NoRowsException : Exception()
|
||||
|
||||
companion object {
|
||||
fun create(): PostgreSQL? {
|
||||
val postgreSQL = PostgreSQL()
|
||||
|
||||
try {
|
||||
val pg =
|
||||
PGSimpleDataSource().apply {
|
||||
serverNames = arrayOf(config.postgresHost)
|
||||
portNumbers = intArrayOf(config.postgresPort)
|
||||
databaseName = config.postgresDatabase
|
||||
user = config.postgresUser
|
||||
password = config.postgresPassword
|
||||
}
|
||||
val flyway = Flyway.configure(DiamondBankOG::class.java.classLoader).dataSource(pg).load()
|
||||
flyway.migrate()
|
||||
|
||||
postgreSQL.pool =
|
||||
PostgreSQLConnectionBuilder.createConnectionPool {
|
||||
host = config.postgresHost
|
||||
port = config.postgresPort
|
||||
database = config.postgresDatabase
|
||||
username = config.postgresUser
|
||||
password = config.postgresPassword
|
||||
maxActiveConnections = config.postgresMaxActiveConnections
|
||||
}
|
||||
return postgreSQL
|
||||
} catch (e: Exception) {
|
||||
economyDisabled = true
|
||||
plugin.logger.severe(
|
||||
"ECONOMY DISABLED! Something went wrong while trying to initialise PostgreSQL. Is PostgreSQL running? Are the PostgreSQL config variables correct?"
|
||||
)
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() = pool.disconnect()
|
||||
|
||||
suspend fun setPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
|
||||
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
val playerShards =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn
|
||||
.sendPreparedStatement(
|
||||
"INSERT INTO diamond(uuid, ${type.string}) VALUES(?, ?) ON CONFLICT (uuid) DO UPDATE SET ${type.string} = excluded.${type.string} " +
|
||||
"RETURNING bank_shards, inventory_shards, ender_chest_shards",
|
||||
listOf(uuid, shards),
|
||||
)
|
||||
.thenApply { result ->
|
||||
if (result.rows.isEmpty()) {
|
||||
throw Exception()
|
||||
}
|
||||
val row = result.rows[0]
|
||||
val bankShards = row.getLong("bank_shards")
|
||||
val inventoryShards = row.getLong("inventory_shards")
|
||||
val enderChestShards = row.getLong("ender_chest_shards")
|
||||
if (bankShards == null || inventoryShards == null || enderChestShards == null) {
|
||||
throw Exception()
|
||||
}
|
||||
PlayerShards(bankShards, inventoryShards, enderChestShards)
|
||||
}
|
||||
}
|
||||
.await()
|
||||
DiamondBankOG.eventManager.sendUpdate(uuid, playerShards)
|
||||
return Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addToPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Long> {
|
||||
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
|
||||
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val playerShards =
|
||||
connection
|
||||
.inTransaction { conn -> scope.future { internalAddToPlayerShards(uuid, shards, type, conn) } }
|
||||
.await()
|
||||
|
||||
DiamondBankOG.eventManager.sendUpdate(uuid, playerShards)
|
||||
return when (type) {
|
||||
ShardType.BANK -> Result.success(playerShards.bank)
|
||||
ShardType.INVENTORY -> Result.success(playerShards.inventory)
|
||||
ShardType.ENDER_CHEST -> Result.success(playerShards.enderChest)
|
||||
else -> Result.failure(InvalidArgumentException())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is InsufficientBalanceException) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun internalAddToPlayerShards(
|
||||
uuid: UUID,
|
||||
shards: Long,
|
||||
type: ShardType,
|
||||
conn: Connection,
|
||||
): PlayerShards {
|
||||
val result =
|
||||
conn
|
||||
.sendPreparedStatement(
|
||||
"INSERT INTO diamond(uuid, ${type.string}) VALUES(?, ?) ON CONFLICT (uuid) DO UPDATE SET ${type.string} = diamond.${type.string} + excluded.${type.string} " +
|
||||
"WHERE diamond.${type.string} + excluded.${type.string} >= 0 RETURNING bank_shards, inventory_shards, ender_chest_shards",
|
||||
listOf(uuid, shards),
|
||||
)
|
||||
.await()
|
||||
|
||||
if (result.rows.isEmpty()) {
|
||||
val shards = internalGetShardTypeShards(uuid, type, conn).getOrElse { throw it }
|
||||
throw InsufficientBalanceException(shards)
|
||||
}
|
||||
val row = result.rows[0]
|
||||
val bankShards = row.getLong("bank_shards")
|
||||
val inventoryShards = row.getLong("inventory_shards")
|
||||
val enderChestShards = row.getLong("ender_chest_shards")
|
||||
if (bankShards == null || inventoryShards == null || enderChestShards == null) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
val playerShards = PlayerShards(bankShards, inventoryShards, enderChestShards)
|
||||
return playerShards
|
||||
}
|
||||
|
||||
suspend fun transferBankShards(
|
||||
sender: UUID,
|
||||
receiver: UUID,
|
||||
shardsToSubtractFromSender: Long,
|
||||
shardsToAddToReceiver: Long,
|
||||
): Result<Map<UUID, Long>> {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
val playerShardsMap =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
scope.future {
|
||||
mapOf<UUID, suspend () -> PlayerShards>(
|
||||
sender to
|
||||
{
|
||||
internalAddToPlayerShards(
|
||||
sender,
|
||||
-shardsToSubtractFromSender,
|
||||
ShardType.BANK,
|
||||
conn,
|
||||
)
|
||||
},
|
||||
receiver to
|
||||
{
|
||||
internalAddToPlayerShards(
|
||||
receiver,
|
||||
shardsToAddToReceiver,
|
||||
ShardType.BANK,
|
||||
conn,
|
||||
)
|
||||
},
|
||||
)
|
||||
.toSortedMap() // Sort to avoid deadlock
|
||||
.mapValues { it.value() }
|
||||
}
|
||||
}
|
||||
.await()
|
||||
|
||||
playerShardsMap.forEach { (uuid, playerShards) ->
|
||||
DiamondBankOG.eventManager.sendUpdate(uuid, playerShards)
|
||||
}
|
||||
return Result.success(playerShardsMap.mapValues { (_, playerShards) -> playerShards.bank })
|
||||
} catch (e: Exception) {
|
||||
if (e is InsufficientBalanceException) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTotalShards(uuid: UUID): Result<Long> {
|
||||
var totalShards: Long?
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
val result =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement(
|
||||
"SELECT total_shards FROM diamond WHERE uuid = ? LIMIT 1",
|
||||
listOf(uuid),
|
||||
)
|
||||
}
|
||||
.await()
|
||||
|
||||
totalShards =
|
||||
if (result.rows.isNotEmpty()) {
|
||||
val row = result.rows[0]
|
||||
row.getLong("total_shards") ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
|
||||
return Result.success(totalShards)
|
||||
}
|
||||
|
||||
suspend fun getAllShards(uuid: UUID): Result<PlayerShards> {
|
||||
var bankShards: Long?
|
||||
var inventoryShards: Long?
|
||||
var enderChestShards: Long?
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
val result =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement(
|
||||
"SELECT bank_shards, inventory_shards, ender_chest_shards FROM diamond WHERE uuid = ? LIMIT 1",
|
||||
listOf(uuid),
|
||||
)
|
||||
}
|
||||
.await()
|
||||
|
||||
if (result.rows.isNotEmpty()) {
|
||||
val row = result.rows[0]
|
||||
bankShards = row.getLong("bank_shards") ?: 0
|
||||
inventoryShards = row.getLong("inventory_shards") ?: 0
|
||||
enderChestShards = row.getLong("ender_chest_shards") ?: 0
|
||||
} else {
|
||||
bankShards = 0
|
||||
inventoryShards = 0
|
||||
enderChestShards = 0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
|
||||
return Result.success(PlayerShards(bankShards, inventoryShards, enderChestShards))
|
||||
}
|
||||
|
||||
suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long> {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
return internalGetShardTypeShards(uuid, type, connection)
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun internalGetShardTypeShards(uuid: UUID, type: ShardType, conn: Connection): Result<Long> {
|
||||
var shards: Long?
|
||||
try {
|
||||
val result =
|
||||
conn
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement(
|
||||
"SELECT ${type.string} FROM diamond WHERE uuid = ? LIMIT 1",
|
||||
listOf(uuid),
|
||||
)
|
||||
}
|
||||
.await()
|
||||
|
||||
shards =
|
||||
if (result.rows.isNotEmpty()) {
|
||||
val row = result.rows[0]
|
||||
row.getLong(type.string) ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
|
||||
return Result.success(shards)
|
||||
}
|
||||
|
||||
suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>> {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val result =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement(
|
||||
"SELECT uuid, total_shards " +
|
||||
"FROM diamond " +
|
||||
"ORDER BY total_shards DESC, uuid DESC OFFSET ? LIMIT 9",
|
||||
listOf(offset),
|
||||
)
|
||||
}
|
||||
.await()
|
||||
val baltop = mutableMapOf<UUID?, Long>()
|
||||
result.rows.forEach {
|
||||
val uuid = it.getAs<UUID>("uuid")
|
||||
val totalShards = it.getLong("total_shards") ?: 0
|
||||
baltop[uuid] = totalShards
|
||||
}
|
||||
return Result.success(baltop)
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pair with as the first value a map with the player name and total balance and as the second value the
|
||||
* offset
|
||||
*/
|
||||
suspend fun getBaltopWithUuid(uuid: UUID): Result<Pair<Map<UUID?, Long>, Long>> {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val result =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT uuid, total_shards, ROW_NUMBER() OVER (ORDER BY total_shards DESC, uuid DESC) AS rn
|
||||
FROM diamond
|
||||
),
|
||||
target AS (
|
||||
SELECT rn, ((rn - 1) / 9) * 9 AS page_offset FROM ranked WHERE uuid = ?)
|
||||
paged AS
|
||||
SELECT ranked.*, target.page_offset FROM ranked JOIN target ON true
|
||||
WHERE ranked.rn > target.page_offset AND ranked.rn <= target.page_offset + 9
|
||||
)
|
||||
SELECT uuid, total_shards, page_offset FROM paged ORDER BY rn
|
||||
"""
|
||||
.trimIndent(),
|
||||
listOf(uuid),
|
||||
)
|
||||
}
|
||||
.await()
|
||||
val baltop = mutableMapOf<UUID?, Long>()
|
||||
var offset = 0L
|
||||
result.rows.forEach {
|
||||
val uuid = it.getAs<UUID>("uuid")
|
||||
val totalShards = it.getLong("total_shards") ?: 0
|
||||
offset = it.getLong("page_offset") ?: 0
|
||||
baltop[uuid] = totalShards
|
||||
}
|
||||
return Result.success(Pair(baltop, offset))
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getNumberOfRows(): Result<Long> {
|
||||
var number: Long?
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val result =
|
||||
connection
|
||||
.inTransaction { conn -> conn.sendPreparedStatement("SELECT count(*) AS exact_count FROM diamond") }
|
||||
.await()
|
||||
|
||||
if (result.rows.isNotEmpty()) {
|
||||
val row = result.rows[0]
|
||||
number = row.getLong(0) ?: 0
|
||||
} else {
|
||||
return Result.failure(NoRowsException())
|
||||
}
|
||||
return Result.success(number)
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.severe(e.toString())
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun insertTransactionLog(
|
||||
playerUuid: UUID,
|
||||
transferredShards: Long,
|
||||
playerToUuid: UUID?,
|
||||
transactionReason: String,
|
||||
notes: String?,
|
||||
): Result<Unit> {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement(
|
||||
"INSERT INTO diamond_log(player_uuid, transferred_shards, player_to_uuid, transaction_reason, notes) " +
|
||||
"VALUES(?, ?, ?, ?, ?)",
|
||||
listOf(playerUuid, transferredShards, playerToUuid, transactionReason, notes),
|
||||
)
|
||||
}
|
||||
.await()
|
||||
} catch (e: Exception) {
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun hasEntry(uuid: UUID): Result<Boolean> {
|
||||
try {
|
||||
val connection = pool.asSuspending.connect()
|
||||
val result =
|
||||
connection
|
||||
.inTransaction { conn ->
|
||||
conn.sendPreparedStatement("SELECT 1 FROM diamond WHERE uuid = ?", listOf(uuid))
|
||||
}
|
||||
.await()
|
||||
|
||||
if (result.rows.size == 1) {
|
||||
return Result.success(true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Result.failure(DatabaseException(e.message ?: "Database exception"))
|
||||
}
|
||||
return Result.success(false)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package net.trueog.diamondbankog.persistence
|
||||
|
||||
import io.lettuce.core.RedisClient
|
||||
import io.lettuce.core.RedisConnectionException
|
||||
import io.lettuce.core.RedisURI
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
|
||||
|
||||
internal class Redis {
|
||||
private val redisClient: RedisClient =
|
||||
RedisClient.create(
|
||||
RedisURI.Builder.redis(config.redisHost, config.redisPort)
|
||||
.apply { config.redisPassword?.let { withPassword(it) } }
|
||||
.withDatabase(config.redisDatabase)
|
||||
.build()
|
||||
)
|
||||
|
||||
/** @return True if failed */
|
||||
fun testConnection(): Boolean {
|
||||
try {
|
||||
val connection = redisClient.connect()
|
||||
connection.close()
|
||||
return false
|
||||
} catch (_: RedisConnectionException) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fun getValue(key: String): String? {
|
||||
val connection = redisClient.connect()
|
||||
val commands = connection.sync()
|
||||
val value = commands.get(key)
|
||||
connection.close()
|
||||
return value
|
||||
}
|
||||
|
||||
fun setValue(key: String, value: String) {
|
||||
val connection = redisClient.connect()
|
||||
val commands = connection.sync()
|
||||
commands.set(key, value)
|
||||
connection.close()
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
redisClient.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction
|
||||
|
||||
import java.util.*
|
||||
import kotlin.math.floor
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.DiamondBankException.InsufficientFundsException
|
||||
import net.trueog.diamondbankog.DiamondBankOG
|
||||
import net.trueog.diamondbankog.DiamondBankRuntimeException.MoreThanOneDecimalDigitRuntimeException
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.OfflinePlayer
|
||||
|
||||
object CommonOperations {
|
||||
/** @return Amount of shards that should be removed from player's bank balance */
|
||||
suspend fun consume(
|
||||
uuid: UUID,
|
||||
shards: Long,
|
||||
inventorySnapshot: InventorySnapshot,
|
||||
config: Config = DiamondBankOG.config,
|
||||
balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
mm: MiniMessage = DiamondBankOG.mm,
|
||||
): Result<Long> {
|
||||
val bankShards =
|
||||
balanceManager.getBankShards(uuid).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val shardsToSubtract =
|
||||
if (bankShards < shards) {
|
||||
val toRemoveShards = shards - bankShards
|
||||
val removedInShards =
|
||||
InventorySnapshotUtils.removeShards(inventorySnapshot, toRemoveShards, config, balanceManager, mm)
|
||||
.getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
.toLong()
|
||||
if (removedInShards != toRemoveShards) {
|
||||
val short = toRemoveShards - removedInShards
|
||||
return Result.failure(InsufficientFundsException(short))
|
||||
}
|
||||
bankShards
|
||||
} else {
|
||||
shards
|
||||
}
|
||||
return Result.success(shardsToSubtract)
|
||||
}
|
||||
|
||||
fun diamondsToShards(diamonds: Float): Result<Long> {
|
||||
val split = diamonds.toString().split('.')
|
||||
if (split[1].length > 1) {
|
||||
return Result.failure(MoreThanOneDecimalDigitRuntimeException())
|
||||
}
|
||||
return Result.success((split[0].toLong() * 9) + split[1].toLong())
|
||||
}
|
||||
|
||||
fun shardsToDiamonds(shards: Long) = String.format("%.1f", floor((shards / 9.0) * 10) / 10.0)
|
||||
|
||||
fun shardsToDiamondsFull(shards: Long): String {
|
||||
val diamonds = shardsToDiamonds(shards)
|
||||
return "<yellow>$diamonds <aqua>Diamond${if (diamonds != "1.0") "s" else ""}"
|
||||
}
|
||||
|
||||
fun getPlayerUsingUuidOrName(value: String): OfflinePlayer {
|
||||
return try {
|
||||
val uuid = UUID.fromString(value)
|
||||
Bukkit.getPlayer(uuid) ?: Bukkit.getOfflinePlayer(uuid)
|
||||
} catch (_: Exception) {
|
||||
Bukkit.getPlayer(value) ?: Bukkit.getOfflinePlayer(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction
|
||||
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.WeakHashMap
|
||||
|
||||
object InventoryLockExtensions {
|
||||
val lockMap: MutableSet<UUID> = Collections.newSetFromMap(Collections.synchronizedMap(WeakHashMap()))
|
||||
|
||||
fun <T> UUID.withInventoryLock(block: () -> T): T {
|
||||
this.lockInventory()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
this.unlockInventory()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> UUID.withInventoryLockSuspend(block: suspend () -> T): T {
|
||||
this.lockInventory()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
this.unlockInventory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UUID.lockInventory() {
|
||||
val added = lockMap.add(this)
|
||||
if (!added) throw IllegalStateException("Inventory was already locked")
|
||||
}
|
||||
|
||||
private fun UUID.unlockInventory() {
|
||||
val removed = lockMap.remove(this)
|
||||
if (!removed) throw IllegalStateException("Inventory was not locked")
|
||||
}
|
||||
|
||||
fun UUID.isInventoryLocked(): Boolean {
|
||||
return this in lockMap
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction
|
||||
|
||||
import java.util.*
|
||||
import kotlin.UnsupportedOperationException
|
||||
import kotlin.collections.HashMap
|
||||
import kotlinx.coroutines.launch
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.isInventoryLocked
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countTotal
|
||||
import net.trueog.utilitiesog.UtilitiesOG
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.HumanEntity
|
||||
import org.bukkit.event.inventory.InventoryType
|
||||
import org.bukkit.inventory.EquipmentSlot
|
||||
import org.bukkit.inventory.InventoryHolder
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.PlayerInventory
|
||||
|
||||
class InventorySnapshot
|
||||
private constructor(
|
||||
private var contents: Array<ItemStack?>,
|
||||
private val heldItemSlot: Int,
|
||||
private var helmet: ItemStack?,
|
||||
private var chestplate: ItemStack?,
|
||||
private var leggings: ItemStack?,
|
||||
private var boots: ItemStack?,
|
||||
val holder: UUID,
|
||||
val balanceManager: BalanceManager,
|
||||
) : PlayerInventory {
|
||||
private val maxAmount = 64
|
||||
|
||||
companion object {
|
||||
fun from(uuid: UUID, balanceManager: BalanceManager): InventorySnapshot {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
throw IllegalStateException("This method should only be called on the main thread")
|
||||
}
|
||||
if (!uuid.isInventoryLocked()) {
|
||||
throw IllegalStateException("Can only take a snapshot from a locked inventory")
|
||||
}
|
||||
val player = Bukkit.getPlayer(uuid)
|
||||
val contents =
|
||||
player?.inventory?.contents?.map { it?.clone() }?.toTypedArray()
|
||||
?: UtilitiesOG.getInventoryData(uuid).map { it?.clone() }.toTypedArray()
|
||||
|
||||
return InventorySnapshot(
|
||||
contents.filterIndexed { i, _ -> i !in 36..39 }.toTypedArray(),
|
||||
player?.inventory?.heldItemSlot ?: UtilitiesOG.getHeldItemSlot(uuid),
|
||||
contents[39],
|
||||
contents[38],
|
||||
contents[37],
|
||||
contents[36],
|
||||
uuid,
|
||||
balanceManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreTo(uuid: UUID) {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
throw IllegalStateException("This method should only be called on the main thread")
|
||||
}
|
||||
if (!uuid.isInventoryLocked()) {
|
||||
throw IllegalStateException("Can only restore to a locked inventory")
|
||||
}
|
||||
val player = Bukkit.getPlayer(uuid)
|
||||
val newContents = arrayOfNulls<ItemStack>(41)
|
||||
contents.take(36).toTypedArray().copyInto(newContents)
|
||||
newContents[39] = helmet
|
||||
newContents[38] = chestplate
|
||||
newContents[37] = leggings
|
||||
newContents[36] = boots
|
||||
newContents[40] = contents[36]
|
||||
if (player == null) {
|
||||
UtilitiesOG.setInventoryData(uuid, newContents.map { it?.clone() }.toTypedArray())
|
||||
} else {
|
||||
player.inventory.contents = newContents.map { it?.clone() }.toTypedArray()
|
||||
}
|
||||
val inventoryShards = this.countTotal()
|
||||
scope.launch {
|
||||
balanceManager.setPlayerShards(uuid, inventoryShards, ShardType.INVENTORY).getOrElse { handleError(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstPartial(item: ItemStack?, inventory: Array<ItemStack?>): Int {
|
||||
if (item == null) return -1
|
||||
return inventory.indexOfFirst { it != null && it.amount < 64 && it.isSimilar(item) }
|
||||
}
|
||||
|
||||
override fun getSize() = contents.size
|
||||
|
||||
override fun getMaxStackSize() = maxAmount
|
||||
|
||||
override fun setMaxStackSize(size: Int) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getItem(index: Int) = contents[index]
|
||||
|
||||
override fun getArmorContents(): Array<out ItemStack?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getExtraContents(): Array<out ItemStack?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getHelmet(): ItemStack? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getChestplate(): ItemStack? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getLeggings(): ItemStack? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getBoots(): ItemStack? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setItem(index: Int, item: ItemStack?) = contents.set(index, item)
|
||||
|
||||
override fun setItem(slot: EquipmentSlot, item: ItemStack?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getItem(slot: EquipmentSlot): ItemStack {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setArmorContents(items: Array<out ItemStack?>?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setExtraContents(items: Array<out ItemStack?>?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setHelmet(helmet: ItemStack?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setChestplate(chestplate: ItemStack?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setLeggings(leggings: ItemStack?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setBoots(boots: ItemStack?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getItemInMainHand() = contents[heldItemSlot] ?: ItemStack(Material.AIR)
|
||||
|
||||
override fun setItemInMainHand(item: ItemStack?) = contents.set(heldItemSlot, item)
|
||||
|
||||
override fun getItemInOffHand() = contents[36] ?: ItemStack(Material.AIR)
|
||||
|
||||
override fun setItemInOffHand(item: ItemStack?) = contents.set(36, item)
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getItemInHand(): ItemStack {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setItemInHand(stack: ItemStack?) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getHeldItemSlot(): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setHeldItemSlot(slot: Int) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun addItem(vararg items: ItemStack): HashMap<Int, ItemStack> {
|
||||
val leftover = HashMap<Int, ItemStack>()
|
||||
|
||||
items.forEachIndexed { index, item ->
|
||||
if (item.type.isAir) return@forEachIndexed
|
||||
|
||||
while (item.amount > 0) {
|
||||
val firstPartial = firstPartial(item, contents)
|
||||
|
||||
if (firstPartial != -1) {
|
||||
val stack = contents[firstPartial]!!
|
||||
val space = maxAmount - stack.amount
|
||||
val amount = minOf(item.amount, space)
|
||||
|
||||
stack.amount += amount
|
||||
item.amount -= amount
|
||||
continue
|
||||
}
|
||||
|
||||
val empty = firstEmpty()
|
||||
|
||||
if (empty == -1) {
|
||||
leftover[index] = item.clone()
|
||||
break
|
||||
}
|
||||
|
||||
val amount = minOf(item.amount, maxAmount)
|
||||
|
||||
contents[empty] = item.clone().apply { this.amount = amount }
|
||||
|
||||
item.amount -= amount
|
||||
}
|
||||
}
|
||||
return leftover
|
||||
}
|
||||
|
||||
override fun removeItem(vararg items: ItemStack): HashMap<Int, ItemStack> {
|
||||
val leftover = HashMap<Int, ItemStack>()
|
||||
|
||||
items.forEachIndexed { index, item ->
|
||||
if (item.type.isAir) return@forEachIndexed
|
||||
var toDelete = item.amount
|
||||
|
||||
while (toDelete > 0) {
|
||||
val firstSlot = first(item)
|
||||
|
||||
if (firstSlot == -1) {
|
||||
item.amount = toDelete
|
||||
leftover[index] = item
|
||||
toDelete = 0
|
||||
} else {
|
||||
val itemStack = contents[firstSlot]!!
|
||||
val removed = minOf(itemStack.amount, toDelete)
|
||||
|
||||
itemStack.amount -= removed
|
||||
toDelete -= removed
|
||||
|
||||
if (itemStack.amount == 0) {
|
||||
setItem(firstSlot, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leftover
|
||||
}
|
||||
|
||||
override fun removeItemAnySlot(vararg items: ItemStack): HashMap<Int?, ItemStack?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getContents(): Array<out ItemStack?> {
|
||||
return (contents.dropLast(1) + boots + leggings + chestplate + helmet + contents.takeLast(1)).toTypedArray()
|
||||
}
|
||||
|
||||
override fun setContents(items: Array<out ItemStack?>) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getStorageContents(): Array<out ItemStack?> = contents
|
||||
|
||||
override fun setStorageContents(items: Array<out ItemStack?>) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun contains(material: Material): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun contains(item: ItemStack?): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun contains(material: Material, amount: Int): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun contains(item: ItemStack?, amount: Int): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun containsAtLeast(item: ItemStack?, amount: Int): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun all(material: Material): HashMap<Int, out ItemStack?> =
|
||||
contents
|
||||
.dropLast(1) // Bukkit's implementation doesn't include the offhand slot
|
||||
.withIndex()
|
||||
.filter { (_, value) -> value?.type == material }
|
||||
.associateTo(HashMap()) { (index, value) -> index to value }
|
||||
|
||||
override fun all(item: ItemStack?): HashMap<Int?, out ItemStack?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun first(material: Material): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun first(item: ItemStack): Int {
|
||||
return contents.indexOfFirst { it != null && item.isSimilar(it) }
|
||||
}
|
||||
|
||||
override fun firstEmpty(): Int {
|
||||
return contents.indexOfFirst { it == null }
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun remove(material: Material) {
|
||||
contents.forEachIndexed { index, item ->
|
||||
if (item?.type == material) {
|
||||
contents[index] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(itemToRemove: ItemStack) {
|
||||
contents.forEachIndexed { index, item ->
|
||||
if (item?.equals(itemToRemove) == true) {
|
||||
contents[index] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear(index: Int) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun close(): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getViewers(): List<HumanEntity?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getType(): InventoryType {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getHolder(): HumanEntity? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getHolder(useSnapshot: Boolean): InventoryHolder? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun iterator(): MutableListIterator<ItemStack?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun iterator(index: Int): ListIterator<ItemStack?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getLocation(): Location? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction
|
||||
|
||||
import kotlin.math.ceil
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.removeItemWithSameKey
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
object InventorySnapshotUtils {
|
||||
/** @return the amount of shards that could be removed */
|
||||
suspend fun removeShards(
|
||||
inventory: InventorySnapshot,
|
||||
shards: Long,
|
||||
config: Config,
|
||||
balanceManager: BalanceManager,
|
||||
mm: MiniMessage,
|
||||
): Result<Int> {
|
||||
require(shards > 0)
|
||||
|
||||
val shards = shards.toInt()
|
||||
|
||||
val notRemovedShards = inventory.removeItemWithSameKey(Shard.createItemStack(shards)).values.sumOf { it.amount }
|
||||
if (notRemovedShards == 0) return Result.success(shards)
|
||||
|
||||
val diamondsToBeRemoved = ceil(notRemovedShards / 9.0).toInt()
|
||||
val diamondsRemainder = notRemovedShards % 9
|
||||
val shardsChange = if (diamondsRemainder == 0) 0 else 9 - diamondsRemainder
|
||||
val notRemovedDiamonds =
|
||||
inventory.removeItem(ItemStack(Material.DIAMOND, diamondsToBeRemoved)).values.sumOf { it.amount }
|
||||
if (notRemovedDiamonds == 0) {
|
||||
val addMap = inventory.addItem(Shard.createItemStack(shardsChange))
|
||||
if (addMap.values.sumOf { it.amount } != 0) {
|
||||
val excessShards = addMap.values.sumOf { it.amount }.toLong()
|
||||
balanceManager.addToBankShards(inventory.holder, excessShards).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
val player = Bukkit.getPlayer(inventory.holder)
|
||||
player?.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <yellow>The excess change of ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
excessShards
|
||||
)
|
||||
} <yellow>has been deposited into your bank account"
|
||||
)
|
||||
)
|
||||
}
|
||||
return Result.success(shards)
|
||||
}
|
||||
|
||||
val diamondBlocksToBeRemoved = ceil(notRemovedDiamonds / 9.0).toInt()
|
||||
val diamondBlocksRemainder = notRemovedDiamonds % 9
|
||||
val diamondsChange = if (diamondBlocksRemainder == 0) 0 else 9 - diamondBlocksRemainder
|
||||
val notRemovedDiamondBlocks =
|
||||
inventory.removeItem(ItemStack(Material.DIAMOND_BLOCK, diamondBlocksToBeRemoved)).values.sumOf { it.amount }
|
||||
if (notRemovedDiamondBlocks == 0) {
|
||||
val shardsAddMap = inventory.addItem(Shard.createItemStack(shardsChange))
|
||||
val excessShards = shardsAddMap.values.sumOf { it.amount }.toLong()
|
||||
if (shardsAddMap.values.sumOf { it.amount } != 0) {
|
||||
balanceManager.addToBankShards(inventory.holder, excessShards).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
}
|
||||
val diamondsAddMap = inventory.addItem(ItemStack(Material.DIAMOND, diamondsChange))
|
||||
val excessDiamondsInShards = diamondsAddMap.values.sumOf { it.amount }.toLong() * 9
|
||||
if (diamondsAddMap.values.sumOf { it.amount } != 0) {
|
||||
balanceManager.addToBankShards(inventory.holder, excessDiamondsInShards).getOrElse {
|
||||
return Result.failure(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (shardsAddMap.values.sumOf { it.amount } != 0 || diamondsAddMap.values.sumOf { it.amount } != 0) {
|
||||
val player = Bukkit.getPlayer(inventory.holder)
|
||||
player?.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <yellow>The excess change of ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
excessShards + excessDiamondsInShards
|
||||
)
|
||||
} <yellow>has been deposited into your bank account"
|
||||
)
|
||||
)
|
||||
}
|
||||
return Result.success(shards)
|
||||
}
|
||||
return Result.success(shards - (notRemovedDiamondBlocks * 81 - diamondsChange * 9 - shardsChange))
|
||||
}
|
||||
|
||||
/** @return the amount of currency items that could be removed in shards */
|
||||
fun removeAll(inventory: InventorySnapshot): Int {
|
||||
val removedShards = removeAllShards(inventory)
|
||||
val removedDiamonds = removeAllDiamonds(inventory)
|
||||
val removedDiamondBlocks = removeAllDiamondBlocks(inventory)
|
||||
return removedShards + (removedDiamonds * 9) + (removedDiamondBlocks * 81)
|
||||
}
|
||||
|
||||
/** @return the amount of shards that could be removed */
|
||||
fun removeAllShards(inventory: InventorySnapshot): Int {
|
||||
val shards = inventory.countShards().toInt()
|
||||
inventory.removeItemWithSameKey(Shard.createItemStack(shards))
|
||||
return shards
|
||||
}
|
||||
|
||||
/** @return the amount of diamonds that could be removed */
|
||||
fun removeAllDiamonds(inventory: InventorySnapshot): Int {
|
||||
val diamonds = inventory.countDiamonds().toInt()
|
||||
inventory.remove(Material.DIAMOND)
|
||||
return diamonds
|
||||
}
|
||||
|
||||
/** @return the amount of diamond blocks that could be removed */
|
||||
fun removeAllDiamondBlocks(inventory: InventorySnapshot): Int {
|
||||
val diamondBlocks = inventory.countDiamondBlocks().toInt()
|
||||
inventory.remove(Material.DIAMOND_BLOCK)
|
||||
return diamondBlocks
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Semaphore
|
||||
|
||||
internal class TransactionLock {
|
||||
private val locks = ConcurrentHashMap<UUID, Semaphore>()
|
||||
|
||||
sealed class LockResult<out T> {
|
||||
data class Acquired<T>(val result: T) : LockResult<T>()
|
||||
|
||||
object Failed : LockResult<Nothing>()
|
||||
}
|
||||
|
||||
fun <T> withLock(uuid: UUID, block: () -> T): T {
|
||||
val lock = locks.computeIfAbsent(uuid) { Semaphore(1, true) }
|
||||
lock.acquire()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> withLockSuspend(uuid: UUID, block: suspend () -> T): T {
|
||||
val lock = locks.computeIfAbsent(uuid) { Semaphore(1, true) }
|
||||
lock.acquire()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> tryWithLock(uuid: UUID, block: () -> T): LockResult<T> {
|
||||
val lock = locks.computeIfAbsent(uuid) { Semaphore(1, true) }
|
||||
return if (lock.tryAcquire()) {
|
||||
try {
|
||||
return LockResult.Acquired(block())
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
} else {
|
||||
LockResult.Failed
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> tryWithLockSuspend(uuid: UUID, block: suspend () -> T): LockResult<T> {
|
||||
val lock = locks.computeIfAbsent(uuid) { Semaphore(1, true) }
|
||||
return if (lock.tryAcquire()) {
|
||||
try {
|
||||
LockResult.Acquired(block())
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
} else {
|
||||
LockResult.Failed
|
||||
}
|
||||
}
|
||||
|
||||
fun isLocked(uuid: UUID): Boolean {
|
||||
return locks[uuid]?.availablePermits() == 0
|
||||
}
|
||||
|
||||
fun removeAllLocks() {
|
||||
locks.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction.command
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.*
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.util.CommonCommandInterlude
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countShards
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.removeItemWithSameKey
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.block.ShulkerBox
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.PlayerInventory
|
||||
import org.bukkit.inventory.meta.BlockStateMeta
|
||||
|
||||
internal class Compress(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (CommonCommandInterlude.run(sender, "compress", config, mm)) {
|
||||
return true
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
var isShulkerBox = false
|
||||
var finalShards: Int
|
||||
var finalDiamonds: Int
|
||||
var finalDiamondBlocks: Int
|
||||
when (
|
||||
transactionLock.tryWithLockSuspend(sender.uniqueId) {
|
||||
val summaryString =
|
||||
sender.uniqueId
|
||||
.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread {
|
||||
InventorySnapshot.from(sender.uniqueId, balanceManager)
|
||||
}
|
||||
|
||||
val (inventory, blockStateMeta, blockState) =
|
||||
if (!args.isNullOrEmpty()) {
|
||||
if (args.size > 1) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Do not provide more arguments than \"yes\" if you want to compress the items in the shulker box you're holding."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
if (args[0] != "yes") {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>Invalid argument.")
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
val itemInMainHand = inventorySnapshot.itemInMainHand
|
||||
if (itemInMainHand.type != Material.SHULKER_BOX) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You are not holding a shulker box."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
val blockStateMeta = itemInMainHand.itemMeta as BlockStateMeta
|
||||
val blockState = blockStateMeta.blockState as ShulkerBox
|
||||
isShulkerBox = true
|
||||
Triple(blockState.inventory, blockStateMeta, blockState)
|
||||
} else {
|
||||
Triple(inventorySnapshot, null, null)
|
||||
}
|
||||
|
||||
if (inventorySnapshot.itemInMainHand.type == Material.SHULKER_BOX && !isShulkerBox) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <#FFA500>Are you sure you want to compress the Diamond currency items in the shulker box you're holding? If so, run \"/compress yes\""
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
// Make sure we use the PlayerInventory implementations if it is one
|
||||
val playerInventory = inventory as? PlayerInventory
|
||||
val shardsInInventory =
|
||||
(playerInventory?.countShards() ?: inventory.countShards()).toInt()
|
||||
val diamondsInInventory =
|
||||
(playerInventory?.countDiamonds() ?: inventory.countDiamonds()).toInt()
|
||||
val diamondBlocksInInventory =
|
||||
(playerInventory?.countDiamondBlocks() ?: inventory.countDiamondBlocks()).toInt()
|
||||
|
||||
finalShards = shardsInInventory
|
||||
finalDiamonds = diamondsInInventory
|
||||
finalDiamondBlocks = diamondBlocksInInventory
|
||||
|
||||
finalDiamonds += finalShards / 9
|
||||
if (finalDiamonds - diamondsInInventory != 0) {
|
||||
val remainder = finalShards % 9
|
||||
finalShards = remainder
|
||||
}
|
||||
|
||||
finalDiamondBlocks += finalDiamonds / 9
|
||||
if (finalDiamondBlocks - diamondBlocksInInventory != 0) {
|
||||
val remainder = finalDiamonds % 9
|
||||
finalDiamonds = remainder
|
||||
}
|
||||
|
||||
val changeInShards = finalShards - shardsInInventory
|
||||
val changeInDiamonds = finalDiamonds - diamondsInInventory
|
||||
val changeInDiamondBlocks = finalDiamondBlocks - diamondBlocksInInventory
|
||||
|
||||
if (changeInDiamonds > 0) {
|
||||
val emptySlots = inventory.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpaceDiamonds =
|
||||
inventory.storageContents
|
||||
.filterNotNull()
|
||||
.filter { it.type == Material.DIAMOND }
|
||||
.sumOf { 64 - it.amount }
|
||||
|
||||
if (changeInDiamonds > emptySlots + leftOverSpaceDiamonds) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You do not have enough space in your ${if (isShulkerBox) "shulker box" else "inventory"} to compress all the Diamond currency items (<green>+$changeInDiamonds <aqua>Diamonds<red>)."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
}
|
||||
|
||||
if (changeInDiamondBlocks > 0) {
|
||||
val emptySlots = inventory.storageContents.filter { it == null }.size * 64
|
||||
val leftOverSpaceDiamondBlocks =
|
||||
inventory.storageContents
|
||||
.filterNotNull()
|
||||
.filter { it.type == Material.DIAMOND_BLOCK }
|
||||
.sumOf { 64 - it.amount }
|
||||
|
||||
if (changeInDiamondBlocks > emptySlots + leftOverSpaceDiamondBlocks) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You do not have enough space in your ${if (isShulkerBox) "shulker box" else "inventory"} to compress all the Diamond currency items (<green>+$changeInDiamondBlocks <aqua>Diamond Blocks<red>)."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
}
|
||||
|
||||
val summaryStringBuilder = StringBuilder("Compression Summary:")
|
||||
|
||||
if (changeInShards < 0) {
|
||||
val removeMap =
|
||||
inventory.removeItemWithSameKey(Shard.createItemStack(abs(changeInShards)))
|
||||
if (removeMap.isNotEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your ${if (isShulkerBox) "shulker box" else "inventory"}."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
summaryStringBuilder.append(
|
||||
"\n<red>$changeInShards Diamond Shard${if (changeInShards != -1) "s" else ""}"
|
||||
)
|
||||
}
|
||||
|
||||
if (changeInDiamonds > 0) {
|
||||
val addMap = inventory.addItem(ItemStack(Material.DIAMOND, changeInDiamonds))
|
||||
if (addMap.isNotEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your ${if (isShulkerBox) "shulker box" else "inventory"}."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
summaryStringBuilder.append(
|
||||
"\n<green>+$changeInDiamonds Diamond${if (changeInDiamonds != 1) "s" else ""}"
|
||||
)
|
||||
} else if (changeInDiamonds < 0) {
|
||||
val removeMap =
|
||||
inventory.removeItem(ItemStack(Material.DIAMOND, abs(changeInDiamonds)))
|
||||
if (removeMap.isNotEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your ${if (isShulkerBox) "shulker box" else "inventory"}."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
summaryStringBuilder.append(
|
||||
"\n<red>$changeInDiamonds Diamond${if (changeInDiamonds != -1) "s" else ""}"
|
||||
)
|
||||
}
|
||||
|
||||
if (changeInDiamondBlocks > 0) {
|
||||
val addMap =
|
||||
inventory.addItem(ItemStack(Material.DIAMOND_BLOCK, changeInDiamondBlocks))
|
||||
if (addMap.isNotEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your ${if (isShulkerBox) "shulker box" else "inventory"}."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
summaryStringBuilder.append(
|
||||
"\n<green>+$changeInDiamondBlocks Diamond Block${if (changeInDiamondBlocks != 1) "s" else ""}"
|
||||
)
|
||||
}
|
||||
|
||||
if (blockStateMeta != null && blockState != null) {
|
||||
blockStateMeta.blockState = blockState
|
||||
inventorySnapshot.itemInMainHand.itemMeta = blockStateMeta
|
||||
}
|
||||
|
||||
if (changeInShards == 0 && changeInDiamonds == 0 && changeInDiamondBlocks == 0) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <#FFA500>Nothing found to compress.")
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(sender.uniqueId) }
|
||||
Result.success(summaryStringBuilder.toString())
|
||||
}
|
||||
.getOrElse {
|
||||
return@tryWithLockSuspend
|
||||
}
|
||||
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>Successfully compressed all the Diamond currency items in your ${if (isShulkerBox) "shulker box" else "inventory"}!"
|
||||
)
|
||||
)
|
||||
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: $summaryString"))
|
||||
}
|
||||
) {
|
||||
is TransactionLock.LockResult.Failed -> {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You are currently blocked from using /compress.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction.command
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.*
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshotUtils
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.util.CommonCommandInterlude
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class Deposit(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (CommonCommandInterlude.run(sender, "deposit", config, mm)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (args.isNullOrEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to deposit."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
if (args.size != 1) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to deposit. Either a number or \"all\"."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
var shards: Long = -1L
|
||||
if (args[0] != "all") {
|
||||
val amount: Float
|
||||
try {
|
||||
amount = args[0].toFloat()
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You cannot deposit a negative or zero amount.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>Invalid argument."))
|
||||
return true
|
||||
}
|
||||
shards =
|
||||
CommonOperations.diamondsToShards(amount).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
when (
|
||||
transactionLock.tryWithLockSuspend(sender.uniqueId) {
|
||||
val removedInShards =
|
||||
sender.uniqueId
|
||||
.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread {
|
||||
InventorySnapshot.from(sender.uniqueId, balanceManager)
|
||||
}
|
||||
|
||||
val removedInShards: Long =
|
||||
if (shards == -1L) {
|
||||
InventorySnapshotUtils.removeAll(inventorySnapshot).toLong()
|
||||
} else {
|
||||
InventorySnapshotUtils.removeShards(
|
||||
inventorySnapshot,
|
||||
shards,
|
||||
config,
|
||||
balanceManager,
|
||||
mm,
|
||||
)
|
||||
.getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong."
|
||||
)
|
||||
)
|
||||
handleError(it)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
.toLong()
|
||||
}
|
||||
if (shards != -1L && removedInShards != shards) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You do not have ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
shards
|
||||
)
|
||||
} <red>to deposit."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
balanceManager.addToBankShards(sender.uniqueId, removedInShards).getOrElse {
|
||||
handleError(it)
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to add to your balance."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(sender.uniqueId) }
|
||||
Result.success(removedInShards)
|
||||
}
|
||||
.getOrElse {
|
||||
return@tryWithLockSuspend
|
||||
}
|
||||
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>Successfully deposited ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
removedInShards
|
||||
)
|
||||
} <green>into your bank account."
|
||||
)
|
||||
)
|
||||
|
||||
balanceManager.insertTransactionLog(sender.uniqueId, shards, null, "Deposit", null).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
}
|
||||
) {
|
||||
is TransactionLock.LockResult.Failed -> {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You are currently blocked from using /deposit.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction.command
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.luckperms.api.LuckPerms
|
||||
import net.trueog.diamondbankog.*
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.util.CommonCommandInterlude
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import net.trueog.diamondbankog.util.PlayerPrefix.getPrefix
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
|
||||
internal class Pay(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
val luckPerms: LuckPerms = DiamondBankOG.luckPerms,
|
||||
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (CommonCommandInterlude.run(sender, "pay", config, mm)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (args.isNullOrEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You did not provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
if (args.size != 2) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
val receiver = CommonOperations.getPlayerUsingUuidOrName(args[0])
|
||||
if (sender.uniqueId == receiver.uniqueId) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>You cannot pay yourself."))
|
||||
return true
|
||||
}
|
||||
|
||||
if (!receiver.hasPlayedBefore()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>That player doesn't exist or hasn't joined this server before."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
val amount: Float
|
||||
try {
|
||||
amount = args[1].toFloat()
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You cannot pay a negative or zero amount.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>Invalid argument."))
|
||||
return true
|
||||
}
|
||||
val shards =
|
||||
CommonOperations.diamondsToShards(amount).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
when (
|
||||
transactionLock.tryWithLockSuspend(sender.uniqueId) {
|
||||
sender.uniqueId
|
||||
.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread {
|
||||
InventorySnapshot.from(sender.uniqueId, balanceManager)
|
||||
}
|
||||
|
||||
val shardsToSubtractFromSender =
|
||||
CommonOperations.consume(
|
||||
sender.uniqueId,
|
||||
shards,
|
||||
inventorySnapshot,
|
||||
config,
|
||||
balanceManager,
|
||||
mm,
|
||||
)
|
||||
.getOrElse {
|
||||
when (it) {
|
||||
is DiamondBankException.InsufficientFundsException -> {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You are ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
it.short
|
||||
)
|
||||
} <red>short for that payment."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
else -> {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>A severe error has occurred. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
balanceManager
|
||||
.transferBankShards(
|
||||
sender.uniqueId,
|
||||
receiver.uniqueId,
|
||||
shardsToSubtractFromSender,
|
||||
shards,
|
||||
)
|
||||
.getOrElse {
|
||||
handleError(it)
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>A severe error has occurred. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(sender.uniqueId) }
|
||||
Result.success(Unit)
|
||||
}
|
||||
.getOrElse {
|
||||
return@tryWithLockSuspend
|
||||
}
|
||||
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>Successfully paid ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
shards
|
||||
)
|
||||
} <green>to ${
|
||||
getPrefix(
|
||||
receiver.uniqueId,
|
||||
luckPerms,
|
||||
)
|
||||
}${receiver.name}<reset><green>."
|
||||
)
|
||||
)
|
||||
|
||||
if (receiver.isOnline) {
|
||||
val receiverPlayer = receiver.player ?: return@tryWithLockSuspend
|
||||
receiverPlayer.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>${
|
||||
getPrefix(
|
||||
sender.uniqueId,
|
||||
luckPerms,
|
||||
)
|
||||
}${sender.name}<reset> <green>has paid you ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
shards
|
||||
)
|
||||
}<green>."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
balanceManager
|
||||
.insertTransactionLog(sender.uniqueId, shards, receiver.uniqueId, "Pay", null)
|
||||
.getOrElse { handleError(it) }
|
||||
}
|
||||
) {
|
||||
is TransactionLock.LockResult.Failed -> {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You are currently blocked from using /pay.")
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package net.trueog.diamondbankog.transaction.command
|
||||
|
||||
import kotlin.math.floor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.*
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.CommonOperations
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.withInventoryLockSuspend
|
||||
import net.trueog.diamondbankog.transaction.InventorySnapshot
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.util.CommonCommandInterlude
|
||||
import net.trueog.diamondbankog.util.ErrorHandler.handleError
|
||||
import net.trueog.diamondbankog.util.MainThreadBlock.runOnMainThread
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
internal class Withdraw(
|
||||
val config: Config = DiamondBankOG.config,
|
||||
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
|
||||
val mm: MiniMessage = DiamondBankOG.mm,
|
||||
val scope: CoroutineScope = DiamondBankOG.scope,
|
||||
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
|
||||
) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
|
||||
if (CommonCommandInterlude.run(sender, "withdraw", config, mm)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (args.isNullOrEmpty()) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to withdraw."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
if (args.size != 1) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to withdraw. Either a number or \"all\"."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
var shards = -1L
|
||||
if (args[0] != "all") {
|
||||
val amount: Float
|
||||
try {
|
||||
amount = args[0].toFloat()
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You cannot withdraw a negative or zero amount.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: <red>Invalid argument."))
|
||||
return true
|
||||
}
|
||||
shards =
|
||||
CommonOperations.diamondsToShards(amount).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information."
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
when (
|
||||
transactionLock.tryWithLockSuspend(sender.uniqueId) {
|
||||
val shardsToWithdraw =
|
||||
sender.uniqueId
|
||||
.withInventoryLockSuspend {
|
||||
val inventorySnapshot = runOnMainThread {
|
||||
InventorySnapshot.from(sender.uniqueId, balanceManager)
|
||||
}
|
||||
|
||||
val bankShards =
|
||||
balanceManager.getBankShards(sender.uniqueId).getOrElse {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Something went wrong while trying to get your balance."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
val shardsToWithdraw =
|
||||
if (shards == -1L) {
|
||||
bankShards
|
||||
} else {
|
||||
shards
|
||||
}
|
||||
|
||||
if (shards != -1L && shards > bankShards) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>Cannot withdraw ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
shardsToWithdraw
|
||||
)
|
||||
} <red>because your bank only contains ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
bankShards
|
||||
)
|
||||
}<red>."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
val diamondsToAdd = floor(shardsToWithdraw / 9.0).toInt()
|
||||
val shardsChange = shardsToWithdraw.toInt() % 9
|
||||
|
||||
if (
|
||||
inventorySnapshot
|
||||
.addItem(ItemStack(Material.DIAMOND, diamondsToAdd))
|
||||
.isNotEmpty() ||
|
||||
inventorySnapshot.addItem(Shard.createItemStack(shardsChange)).isNotEmpty()
|
||||
) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>You don't have enough inventory space to withdraw ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
shardsToWithdraw
|
||||
)
|
||||
}<red>."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
balanceManager.subtractFromBankShards(sender.uniqueId, shardsToWithdraw).getOrElse {
|
||||
handleError(it)
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <red>A severe error has occurred. Please notify a staff member."
|
||||
)
|
||||
)
|
||||
return@withInventoryLockSuspend Result.failure(Exception())
|
||||
}
|
||||
|
||||
runOnMainThread { inventorySnapshot.restoreTo(sender.uniqueId) }
|
||||
Result.success(shardsToWithdraw)
|
||||
}
|
||||
.getOrElse {
|
||||
return@tryWithLockSuspend
|
||||
}
|
||||
|
||||
sender.sendMessage(
|
||||
mm.deserialize(
|
||||
"${config.prefix}<reset>: <green>Successfully withdrew ${
|
||||
CommonOperations.shardsToDiamondsFull(
|
||||
shardsToWithdraw
|
||||
)
|
||||
} <green>from your bank account."
|
||||
)
|
||||
)
|
||||
|
||||
balanceManager.insertTransactionLog(sender.uniqueId, shards, null, "Withdraw", null).getOrElse {
|
||||
handleError(it)
|
||||
}
|
||||
}
|
||||
) {
|
||||
is TransactionLock.LockResult.Failed -> {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You are currently blocked from using /withdraw.")
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package net.trueog.diamondbankog.util
|
||||
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
object CommonCommandInterlude {
|
||||
/** @return should return */
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun run(sender: CommandSender, command: String, config: Config, mm: MiniMessage): Boolean {
|
||||
contract { returns(false) implies (sender is Player) }
|
||||
if (economyDisabled) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>The economy is disabled. Please notify a staff member.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (sender !is Player) {
|
||||
sender.sendMessage("You can only execute this command as a player.")
|
||||
return true
|
||||
}
|
||||
|
||||
val worldName = sender.world.name
|
||||
if (worldName != "world" && worldName != "world_nether" && worldName != "world_the_end") {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You cannot use /$command when in a minigame.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("diamondbank-og.$command")) {
|
||||
sender.sendMessage(
|
||||
mm.deserialize("${config.prefix}<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package net.trueog.diamondbankog.util
|
||||
|
||||
import java.util.logging.Level
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
|
||||
internal object ErrorHandler {
|
||||
/** Handles the error by throwing, disables the economy unless specified that it shouldn't */
|
||||
fun handleError(exception: Throwable, dontDisableEconomy: Boolean = false) {
|
||||
if (!dontDisableEconomy) economyDisabled = true
|
||||
plugin.logger.log(Level.SEVERE, "Economy error", exception)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package net.trueog.diamondbankog.util
|
||||
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.block.ShulkerBox
|
||||
import org.bukkit.inventory.Inventory
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.BlockStateMeta
|
||||
|
||||
internal object InventoryExtensions {
|
||||
fun Inventory.countTotal(): Long {
|
||||
return this.countShards() +
|
||||
this.countDiamonds() * 9 +
|
||||
this.countDiamondBlocks() * 81 +
|
||||
this.all(Material.SHULKER_BOX).values.sumOf {
|
||||
((it.itemMeta as BlockStateMeta).blockState as ShulkerBox).inventory.countTotal()
|
||||
}
|
||||
}
|
||||
|
||||
fun Inventory.countShards(): Long {
|
||||
val inventoryShards =
|
||||
this.all(Material.PRISMARINE_SHARD).values.filter { Shard.isShardItem(it) }.sumOf { it.amount }
|
||||
return inventoryShards.toLong()
|
||||
}
|
||||
|
||||
fun Inventory.countDiamonds(): Long {
|
||||
val inventoryDiamonds = this.all(Material.DIAMOND).values.sumOf { it.amount }
|
||||
return inventoryDiamonds.toLong()
|
||||
}
|
||||
|
||||
fun Inventory.countDiamondBlocks(): Long {
|
||||
val inventoryDiamondBlocks = this.all(Material.DIAMOND_BLOCK).values.sumOf { it.amount }
|
||||
return inventoryDiamondBlocks.toLong()
|
||||
}
|
||||
|
||||
fun Inventory.removeItemWithSameKey(vararg items: ItemStack): HashMap<Int, ItemStack> {
|
||||
val leftover = HashMap<Int, ItemStack>()
|
||||
|
||||
items.forEachIndexed { index, item ->
|
||||
if (item.type.isAir) return@forEachIndexed
|
||||
var toDelete = item.amount
|
||||
|
||||
while (toDelete > 0) {
|
||||
val firstSlot = firstWithSameKey(item)
|
||||
|
||||
if (firstSlot == -1) {
|
||||
item.amount = toDelete
|
||||
leftover[index] = item
|
||||
toDelete = 0
|
||||
} else {
|
||||
val itemStack = contents[firstSlot]!!
|
||||
val removed = minOf(itemStack.amount, toDelete)
|
||||
|
||||
itemStack.amount -= removed
|
||||
toDelete -= removed
|
||||
|
||||
if (itemStack.amount == 0) {
|
||||
setItem(firstSlot, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leftover
|
||||
}
|
||||
|
||||
fun Inventory.firstWithSameKey(item: ItemStack): Int {
|
||||
return contents.indexOfFirst {
|
||||
it != null && item.itemMeta.persistentDataContainer.keys == it.itemMeta.persistentDataContainer.keys
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package net.trueog.diamondbankog.util
|
||||
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
|
||||
import org.bukkit.Bukkit
|
||||
|
||||
internal object MainThreadBlock {
|
||||
suspend fun <T> runOnMainThread(block: () -> T): T {
|
||||
return if (Bukkit.isPrimaryThread()) {
|
||||
block()
|
||||
} else {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
Bukkit.getScheduler()
|
||||
.runTask(
|
||||
plugin,
|
||||
Runnable {
|
||||
try {
|
||||
cont.resume(block())
|
||||
} catch (e: Throwable) {
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package net.trueog.diamondbankog.util
|
||||
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.PlayerInventory
|
||||
|
||||
internal object PlayerInventoryExtensions {
|
||||
private fun PlayerInventory.allValues(material: Material): List<ItemStack> =
|
||||
this.all(material).values + listOfNotNull(this.itemInOffHand.takeIf { it.type == material })
|
||||
|
||||
fun PlayerInventory.countTotal(): Long {
|
||||
return this.countShards() + this.countDiamonds() * 9 + this.countDiamondBlocks() * 81
|
||||
// this.allValues(Material.SHULKER_BOX).sumOf {
|
||||
// ((it.itemMeta as BlockStateMeta).blockState as ShulkerBox).inventory.countTotal()
|
||||
// }
|
||||
}
|
||||
|
||||
fun PlayerInventory.countShards(): Long {
|
||||
val inventoryShards =
|
||||
this.allValues(Material.PRISMARINE_SHARD).filter { Shard.isShardItem(it) }.sumOf { it.amount }
|
||||
return inventoryShards.toLong()
|
||||
}
|
||||
|
||||
fun PlayerInventory.countDiamonds(): Long {
|
||||
val inventoryDiamonds = this.allValues(Material.DIAMOND).sumOf { it.amount }
|
||||
return inventoryDiamonds.toLong()
|
||||
}
|
||||
|
||||
fun PlayerInventory.countDiamondBlocks(): Long {
|
||||
val inventoryDiamondBlocks = this.allValues(Material.DIAMOND_BLOCK).sumOf { it.amount }
|
||||
return inventoryDiamondBlocks.toLong()
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package net.trueog.diamondbankog.util
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.future.await
|
||||
import net.luckperms.api.LuckPerms
|
||||
|
||||
internal object PlayerPrefix {
|
||||
private val legacyToMmMap =
|
||||
mapOf(
|
||||
"0" to "<black>",
|
||||
"1" to "<dark_blue>",
|
||||
"2" to "<dark_green>",
|
||||
"3" to "<dark_aqua>",
|
||||
"4" to "<dark_red>",
|
||||
"5" to "<dark_purple>",
|
||||
"6" to "<gold>",
|
||||
"7" to "<gray>",
|
||||
"8" to "<dark_gray>",
|
||||
"9" to "<blue>",
|
||||
"a" to "<green>",
|
||||
"b" to "<aqua>",
|
||||
"c" to "<red>",
|
||||
"d" to "<light_purple>",
|
||||
"e" to "<yellow>",
|
||||
"f" to "<white>",
|
||||
"k" to "<obfuscated>",
|
||||
"l" to "<bold>",
|
||||
"m" to "<strikethrough>",
|
||||
"n" to "<underlined>",
|
||||
"o" to "<italic>",
|
||||
"r" to "<reset>",
|
||||
"*" to "<rainbow>",
|
||||
)
|
||||
|
||||
val legacyRegex = Regex("[§&]([0-9a-fk-or*])", RegexOption.IGNORE_CASE)
|
||||
|
||||
fun legacyToMm(text: String): String {
|
||||
return legacyRegex.replace(text) { legacyToMmMap[it.groupValues[1].lowercase()] ?: it.value }
|
||||
}
|
||||
|
||||
suspend fun getPrefix(uuid: UUID, luckPerms: LuckPerms): String {
|
||||
val user = luckPerms.userManager.getUser(uuid) ?: luckPerms.userManager.loadUser(uuid).await()
|
||||
val prefixNode = user.nodes.singleOrNull { node -> node.key.startsWith("prefix.1.") }
|
||||
if (prefixNode == null) return ""
|
||||
return legacyToMm(prefixNode.key.split(".").last()) + " "
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
# ---General---
|
||||
# MiniMessage-formatted prefix (https://docs.advntr.dev/minimessage/format.html)
|
||||
prefix: <gray>[<aqua>DiamondBank<white>-<red>OG<gray>]
|
||||
prefix: <dark_gray>[<aqua>DiamondBank<white>-<dark_red>OG<dark_gray>]
|
||||
|
||||
# ---PostgreSQL---
|
||||
postgresHost: localhost
|
||||
postgresPort: 5432
|
||||
postgresDatabase: diamond
|
||||
postgresUser: ^{DIAMONDBANKOG_POSTGRES_USER}
|
||||
postgresPassword: ^{DIAMONDBANKOG_POSTGRES_PASSWORD}
|
||||
postgresMaxActiveConnections: 10
|
||||
# ---Sentry (or compatible services like GlitchTip)---
|
||||
sentryEnabled: false
|
||||
sentryDsn: Sentry(-compatible) DSN URL
|
||||
|
||||
# ---Redis(-compatible) key value store---
|
||||
redisHost: 127.0.0.1
|
||||
redisPort: 6379
|
||||
redisDatabase: 0
|
||||
redisPassword: ^{KEYDB_PASSWORD}
|
||||
# ---Postgres---
|
||||
postgresUrl: jdbc:postgresql://localhost:5432/diamond
|
||||
postgresUser: postgres
|
||||
postgresPassword: postgresPassword
|
||||
postgresTable: diamond
|
||||
@@ -1,30 +0,0 @@
|
||||
CREATE TABLE diamond(
|
||||
uuid UUID PRIMARY KEY, bank_shards BIGINT DEFAULT 0, inventory_shards BIGINT DEFAULT 0, ender_chest_shards BIGINT DEFAULT 0,
|
||||
total_shards BIGINT GENERATED ALWAYS AS ( COALESCE(bank_shards, 0) + COALESCE(inventory_shards, 0) + COALESCE(ender_chest_shards, 0) ) STORED
|
||||
);
|
||||
|
||||
CREATE INDEX idx_total_shards ON diamond(total_shards DESC);
|
||||
|
||||
CREATE TABLE diamond_log(
|
||||
id SERIAL PRIMARY KEY, player_uuid UUID NOT NULL, transferred_shards BIGINT NOT NULL, player_to_uuid UUID,
|
||||
transaction_reason TEXT, notes TEXT, timestamp TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE FUNCTION fifo_limit_trigger() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
max_rows CONSTANT BIGINT := 500;
|
||||
BEGIN
|
||||
DELETE FROM diamond_log
|
||||
WHERE id <= (
|
||||
SELECT id FROM diamond_log
|
||||
ORDER BY id DESC
|
||||
OFFSET max_rows LIMIT 1
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER fifo_limit_trigger
|
||||
AFTER INSERT ON diamond_log
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION fifo_limit_trigger();
|
||||
@@ -5,25 +5,25 @@ api-version: '${apiVersion}'
|
||||
commands:
|
||||
deposit:
|
||||
description: Deposit diamonds into your bank
|
||||
usage: /deposit <diamonds> or /deposit all
|
||||
usage: /deposit <amount> or /deposit all
|
||||
withdraw:
|
||||
description: Withdraw diamonds from your bank
|
||||
usage: /withdraw <diamonds> or /withdraw all
|
||||
usage: /withdraw <amount> or /withdraw all
|
||||
setbankbalance:
|
||||
description: Set the bank balance of a player
|
||||
usage: /setbankbalance <player name or player uuid> <shards>
|
||||
usage: /setbankbalance <player name or player uuid> <amount>
|
||||
setbankbal:
|
||||
description: Set the bank balance of a player
|
||||
usage: /setbankbal <player name or player uuid> <shards>
|
||||
usage: /setbankbal <player name or player uuid> <amount>
|
||||
pay:
|
||||
description: Pay a player
|
||||
usage: /pay <player name or player uuid> <diamonds>
|
||||
usage: /pay <player name or player uuid> <amount>
|
||||
balancetop:
|
||||
description: Show the balancetop
|
||||
usage: /balancetop <page number (optional)> or /balancetop <player name or player uuid>
|
||||
usage: /balancetop <page number (optional)>
|
||||
baltop:
|
||||
description: Show the balancetop
|
||||
usage: /baltop <page number (optional)> or /baltop <player name or player uuid>
|
||||
usage: /baltop <page number (optional)>
|
||||
balance:
|
||||
description: Get your or someone else's balance
|
||||
usage: /balance or /balance <player name or player uuid>
|
||||
@@ -33,27 +33,6 @@ commands:
|
||||
diamondbankreload:
|
||||
description: Reload the config
|
||||
usage: /diamondbankreload
|
||||
diamondbankhelp:
|
||||
description: DiamondBank help
|
||||
usage: /diamondbankhelp
|
||||
enableeconomy:
|
||||
description: Enable the economy
|
||||
usage: /enableeconomy
|
||||
disableeconomy:
|
||||
description: Disable the economy
|
||||
usage: /disableeconomy
|
||||
compress:
|
||||
description: Compresses every Diamond-related item into the highest value form
|
||||
usage: /compress
|
||||
autocompress:
|
||||
description: Toggles auto-compress, automatically compresses all Diamond currency items in your inventory into the highest value form to save space, mutually exclusive with auto-deposit
|
||||
usage: /autocompress
|
||||
autodeposit:
|
||||
description: Toggles auto-deposit, automatically deposits picked up Diamond currency items, mutually exclusive with auto-compress
|
||||
usage: /autodeposit
|
||||
dbogtoggledebug:
|
||||
description: Toggles debug mode
|
||||
usage: /dbtoggledebug
|
||||
|
||||
# So we can overwrite Essentials' commands
|
||||
softdepend: [Essentials-OG, Utilities-OG]
|
||||
softdepend: [ Essentials ]
|
||||
@@ -1,279 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import io.mockk.*
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.trueog.diamondbankog.Utils.allImpl
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.NamespacedKey
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.block.ShulkerBox
|
||||
import org.bukkit.inventory.Inventory
|
||||
import org.bukkit.inventory.InventoryHolder
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.BlockStateMeta
|
||||
import org.bukkit.inventory.meta.Damageable
|
||||
import org.bukkit.inventory.meta.ItemMeta
|
||||
import org.bukkit.persistence.PersistentDataType
|
||||
|
||||
object BukkitMock {
|
||||
private fun first(item: ItemStack?, inventory: Array<ItemStack?>): Int {
|
||||
if (item == null) return -1
|
||||
return inventory.indexOfFirst { it != null && item.isSimilar(it) }
|
||||
}
|
||||
|
||||
private fun firstPartial(item: ItemStack?, inventory: Array<ItemStack?>): Int {
|
||||
if (item == null) return -1
|
||||
return inventory.indexOfFirst { it != null && it.amount < 64 && it.isSimilar(item) }
|
||||
}
|
||||
|
||||
private fun firstEmpty(inventory: Array<ItemStack?>): Int {
|
||||
return inventory.indexOfFirst { it == null }
|
||||
}
|
||||
|
||||
const val MAX_AMOUNT = 64
|
||||
|
||||
fun mockBukkit(): Server {
|
||||
val server = mockk<Server>()
|
||||
mockkStatic(Bukkit::class)
|
||||
val serverField = Bukkit::class.java.getDeclaredField("server")
|
||||
serverField.isAccessible = true
|
||||
serverField.set(null, server)
|
||||
every { Bukkit.getServer() } returns server
|
||||
every { server.isPrimaryThread } returns true
|
||||
every { Bukkit.isPrimaryThread() } returns true
|
||||
|
||||
every { Bukkit.getItemFactory() } answers { server.itemFactory }
|
||||
|
||||
every { Bukkit.createInventory(anyNullable<InventoryHolder>(), any<Int>()) } answers
|
||||
{
|
||||
server.createInventory(firstArg<InventoryHolder?>(), secondArg<Int>())
|
||||
}
|
||||
|
||||
val sizeSlot = slot<Int>()
|
||||
every { server.createInventory(any(), capture(sizeSlot)) } returns createInventory()
|
||||
|
||||
every { server.itemFactory.getItemMeta(any()) } answers
|
||||
{
|
||||
val material = firstArg<Material>()
|
||||
val itemMeta =
|
||||
if (material == Material.SHULKER_BOX) {
|
||||
mockk<BlockStateMeta>()
|
||||
} else {
|
||||
mockk<Damageable>()
|
||||
}
|
||||
|
||||
val persistentDataContainerKeys = mutableSetOf<NamespacedKey>()
|
||||
var displayName: Component? = null
|
||||
every { itemMeta.clone() } returns itemMeta
|
||||
every { itemMeta.displayName(any()) } answers { displayName = firstArg<Component>() }
|
||||
every { itemMeta.displayName() } answers { displayName }
|
||||
every { itemMeta.addEnchant(any(), any(), any()) } returns true
|
||||
every { itemMeta.addItemFlags(any()) } just Runs
|
||||
every { itemMeta.persistentDataContainer.set(any(), PersistentDataType.STRING, any()) } answers
|
||||
{
|
||||
val key = firstArg<NamespacedKey>()
|
||||
persistentDataContainerKeys.add(key)
|
||||
}
|
||||
every { itemMeta.persistentDataContainer.has(any()) } answers
|
||||
{
|
||||
val key = firstArg<NamespacedKey>()
|
||||
persistentDataContainerKeys.contains(key)
|
||||
}
|
||||
every { itemMeta.persistentDataContainer.keys } answers { persistentDataContainerKeys }
|
||||
every { itemMeta.persistentDataContainer.isEmpty } answers { persistentDataContainerKeys.isEmpty() }
|
||||
|
||||
if (itemMeta is BlockStateMeta) {
|
||||
val blockState = mockk<ShulkerBox>()
|
||||
var inventory = createInventory()
|
||||
every { itemMeta.blockState } returns blockState
|
||||
every { itemMeta.blockState = any() } answers { inventory = firstArg<ShulkerBox>().inventory }
|
||||
every { blockState.inventory } returns inventory
|
||||
}
|
||||
|
||||
if (itemMeta is Damageable) {
|
||||
every { itemMeta.damage } returns 0
|
||||
}
|
||||
|
||||
itemMeta
|
||||
}
|
||||
|
||||
every { server.itemFactory.isApplicable(any(), Material.PRISMARINE_SHARD) } returns true
|
||||
every { server.itemFactory.isApplicable(any(), Material.SHULKER_BOX) } returns true
|
||||
|
||||
every { server.itemFactory.asMetaFor(any(), any<Material>()) } answers
|
||||
{
|
||||
val itemMeta = firstArg<ItemMeta>()
|
||||
val persistentDataContainerKeys = mutableSetOf<NamespacedKey>()
|
||||
var displayName = itemMeta.displayName()
|
||||
itemMeta.persistentDataContainer.keys.forEach { persistentDataContainerKeys.add(it) }
|
||||
|
||||
val material = secondArg<Material>()
|
||||
val newItemMeta =
|
||||
if (material == Material.SHULKER_BOX) {
|
||||
mockk<BlockStateMeta>()
|
||||
} else {
|
||||
mockk<Damageable>()
|
||||
}
|
||||
|
||||
every { newItemMeta.clone() } returns newItemMeta
|
||||
every { newItemMeta.displayName() } answers { displayName }
|
||||
every { newItemMeta.displayName(any()) } answers { displayName = firstArg<Component>() }
|
||||
every { newItemMeta.persistentDataContainer.has(any()) } answers
|
||||
{
|
||||
val key = firstArg<NamespacedKey>()
|
||||
persistentDataContainerKeys.contains(key)
|
||||
}
|
||||
every { newItemMeta.persistentDataContainer.isEmpty } answers { persistentDataContainerKeys.isEmpty() }
|
||||
every { newItemMeta.persistentDataContainer.keys } answers { persistentDataContainerKeys }
|
||||
|
||||
if (newItemMeta is BlockStateMeta) {
|
||||
val blockState = mockk<ShulkerBox>()
|
||||
var inventory = ((itemMeta as BlockStateMeta).blockState as ShulkerBox).inventory
|
||||
|
||||
every { newItemMeta.blockState } returns blockState
|
||||
every { newItemMeta.blockState = any() } answers { inventory = firstArg<ShulkerBox>().inventory }
|
||||
every { blockState.inventory } returns inventory
|
||||
}
|
||||
|
||||
if (newItemMeta is Damageable) {
|
||||
every { newItemMeta.damage } returns 0
|
||||
}
|
||||
|
||||
newItemMeta
|
||||
}
|
||||
|
||||
every { server.itemFactory.equals(any(), any()) } answers
|
||||
{
|
||||
val meta1 = firstArg<ItemMeta?>()
|
||||
val meta2 = secondArg<ItemMeta?>()
|
||||
if (meta1 == meta2) return@answers true
|
||||
|
||||
if (meta1 == null) {
|
||||
return@answers (meta2!!.displayName() == null &&
|
||||
!meta2.hasEnchants() &&
|
||||
meta2.persistentDataContainer.isEmpty)
|
||||
}
|
||||
if (meta2 == null) {
|
||||
return@answers (meta1.displayName() == null &&
|
||||
!meta1.hasEnchants() &&
|
||||
meta1.persistentDataContainer.isEmpty)
|
||||
}
|
||||
|
||||
meta1.displayName() == meta2.displayName() &&
|
||||
meta1.persistentDataContainer.keys == meta2.persistentDataContainer.keys
|
||||
}
|
||||
|
||||
every { @Suppress("DEPRECATION") server.itemFactory.updateMaterial(any(), Material.PRISMARINE_SHARD) } returns
|
||||
Material.PRISMARINE_SHARD
|
||||
|
||||
every { @Suppress("DEPRECATION") server.itemFactory.updateMaterial(any(), Material.SHULKER_BOX) } returns
|
||||
Material.SHULKER_BOX
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
fun createInventory(): Inventory {
|
||||
val inventory = mockk<Inventory>()
|
||||
var contents = arrayOfNulls<ItemStack>(36)
|
||||
every { inventory.contents = any<Array<ItemStack?>>() } answers { contents = firstArg<Array<ItemStack?>>() }
|
||||
every { inventory.contents } answers { contents }
|
||||
every { inventory.setItem(any<Int>(), anyNullable<ItemStack?>()) } answers
|
||||
{
|
||||
contents[firstArg()] = secondArg()
|
||||
}
|
||||
every { inventory.storageContents } answers { contents }
|
||||
every { inventory.all(any<Material>()) } answers
|
||||
{
|
||||
val material = firstArg<Material>()
|
||||
allImpl(contents, material)
|
||||
}
|
||||
every { inventory.addItem(any()) } answers { addToInventory(firstArg<Array<out ItemStack?>>(), contents) }
|
||||
every { inventory.removeItem(any()) } answers
|
||||
{
|
||||
val items = firstArg<Array<out ItemStack?>>()
|
||||
val leftover = HashMap<Int, ItemStack>()
|
||||
|
||||
items.forEachIndexed { index, item ->
|
||||
requireNotNull(item) { "ItemStack cannot be null" }
|
||||
|
||||
if (item.type.isAir) return@forEachIndexed
|
||||
var toDelete = item.amount
|
||||
|
||||
while (toDelete > 0) {
|
||||
val first = first(item, contents)
|
||||
|
||||
if (first == -1) {
|
||||
item.amount = toDelete
|
||||
leftover[index] = item
|
||||
toDelete = 0
|
||||
} else {
|
||||
val itemStack = contents[first]!!
|
||||
val removed = minOf(itemStack.amount, toDelete)
|
||||
|
||||
itemStack.amount -= removed
|
||||
toDelete -= removed
|
||||
|
||||
if (itemStack.amount == 0) {
|
||||
contents[first] = null
|
||||
} else {
|
||||
contents[first] = itemStack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leftover
|
||||
}
|
||||
every { inventory.remove(anyNullable<Material>()) } answers
|
||||
{
|
||||
val material = firstArg<Material?>() ?: throw IllegalArgumentException("Material cannot be null")
|
||||
|
||||
contents.forEachIndexed { index, item ->
|
||||
if (item?.type == material) {
|
||||
contents[index] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
every { inventory.getItem(any<Int>()) } answers { contents[firstArg()] }
|
||||
return inventory
|
||||
}
|
||||
|
||||
fun addToInventory(items: Array<out ItemStack?>, contents: Array<ItemStack?>): HashMap<Int, ItemStack> {
|
||||
val leftover = HashMap<Int, ItemStack>()
|
||||
|
||||
items.forEachIndexed { index, item ->
|
||||
requireNotNull(item) { "ItemStack cannot be null" }
|
||||
|
||||
if (item.type.isAir) return@forEachIndexed
|
||||
|
||||
while (item.amount > 0) {
|
||||
val firstPartial = firstPartial(item, contents)
|
||||
|
||||
if (firstPartial != -1) {
|
||||
val stack = contents[firstPartial]!!
|
||||
val space = MAX_AMOUNT - stack.amount
|
||||
val amount = minOf(item.amount, space)
|
||||
|
||||
stack.amount += amount
|
||||
item.amount -= amount
|
||||
continue
|
||||
}
|
||||
|
||||
val empty = firstEmpty(contents)
|
||||
|
||||
if (empty == -1) {
|
||||
leftover[index] = item.clone()
|
||||
break
|
||||
}
|
||||
|
||||
val amount = minOf(item.amount, MAX_AMOUNT)
|
||||
|
||||
contents[empty] = item.clone().apply { this.amount = amount }
|
||||
|
||||
item.amount -= amount
|
||||
}
|
||||
}
|
||||
return leftover
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import java.util.*
|
||||
import net.trueog.diamondbankog.balance.Cache
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
|
||||
class CacheTest {
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Cache Set")
|
||||
@CsvSource("Bank Shards, BANK", "Inventory Shards, INVENTORY", "Ender Chest Shards, ENDER_CHEST")
|
||||
fun cacheSet(@Suppress("UNUSED_PARAMETER") name: String, enumName: String) {
|
||||
val shardType = ShardType.valueOf(enumName)
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
val result = cache.setBalance(uuid, 3, shardType)
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "setBalance result should not be a failure") },
|
||||
{ assertEquals(3, cache.getBalance(uuid, shardType).getOrNull()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cache Set TOTAL should fail")
|
||||
fun cacheSetTotalFail() {
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
val result = cache.setBalance(uuid, 3, ShardType.TOTAL)
|
||||
assertTrue(result.isFailure, "setBalance result should be a failure with ShardType.TOTAL")
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Cache Get (after set)")
|
||||
@CsvSource("Bank Shards, BANK", "Inventory Shards, INVENTORY", "Ender Chest Shards, ENDER_CHEST")
|
||||
fun cacheGetAfterSet(@Suppress("UNUSED_PARAMETER") name: String, enumName: String) {
|
||||
val shardType = ShardType.valueOf(enumName)
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
cache.setBalance(uuid, 3, if (shardType != ShardType.TOTAL) shardType else ShardType.BANK)
|
||||
val result = cache.getBalance(uuid, shardType)
|
||||
assertEquals(3, result.getOrNull())
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Cache Get")
|
||||
@CsvSource("Bank Shards, BANK", "Inventory Shards, INVENTORY", "Ender Chest Shards, ENDER_CHEST")
|
||||
fun cacheGet(@Suppress("UNUSED_PARAMETER") name: String, enumName: String) {
|
||||
val shardType = ShardType.valueOf(enumName)
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
val result = cache.getBalance(uuid, shardType)
|
||||
assertEquals(-1, result.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cache Get TOTAL should fail")
|
||||
fun cacheGetTotalFail() {
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
val result = cache.getBalance(uuid, ShardType.TOTAL)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Cache Remove")
|
||||
@CsvSource("Bank Shards, BANK", "Inventory Shards, INVENTORY", "Ender Chest Shards, ENDER_CHEST")
|
||||
fun cacheRemove(@Suppress("UNUSED_PARAMETER") name: String, enumName: String) {
|
||||
val shardType = ShardType.valueOf(enumName)
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
cache.setBalance(uuid, 3, shardType)
|
||||
val result = cache.removeBalance(uuid, shardType)
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "removeBalance result should not be a failure") },
|
||||
{ assertEquals(-1, cache.getBalance(uuid, shardType).getOrNull()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cache Remove TOTAL should fail")
|
||||
fun cacheRemoveTotalFail() {
|
||||
val cache = Cache()
|
||||
val uuid = UUID.randomUUID()
|
||||
val result = cache.removeBalance(uuid, ShardType.TOTAL)
|
||||
assertTrue(result.isFailure, "removeBalance result should be a failure with ShardType.TOTAL")
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import io.mockk.*
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.trueog.diamondbankog.Constants.otherPlayerUuid
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
|
||||
import net.trueog.diamondbankog.balance.Cache
|
||||
import net.trueog.diamondbankog.balance.CachingBalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.PlayerShards
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.persistence.PostgreSQL
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
|
||||
class CachingBalanceManagerTest {
|
||||
@MockK private lateinit var postgreSQL: PostgreSQL
|
||||
@SpyK private var cache = Cache()
|
||||
|
||||
private lateinit var manager: CachingBalanceManager
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
// Constructor is private, only reachable through the factory (which opens a real DB
|
||||
// connection). Reflection bypasses that so we can inject a mocked PostgreSQL instead.
|
||||
val constructor = CachingBalanceManager::class.java.getDeclaredConstructor()
|
||||
constructor.isAccessible = true
|
||||
manager = constructor.newInstance()
|
||||
val postgreSQL = manager::class.java.getDeclaredField("postgreSQL")
|
||||
postgreSQL.isAccessible = true
|
||||
postgreSQL.set(manager, this.postgreSQL)
|
||||
val cache = manager::class.java.getDeclaredField("cache")
|
||||
cache.isAccessible = true
|
||||
cache.set(manager, this.cache)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Set Player Shards")
|
||||
@CsvSource("Bank Shards, BANK", "Inventory Shards, INVENTORY", "Ender Chest Shards, ENDER_CHEST")
|
||||
fun setPlayerShards(@Suppress("UNUSED_PARAMETER") name: String, enumName: String) = runTest {
|
||||
val shardType = ShardType.valueOf(enumName)
|
||||
coEvery { postgreSQL.setPlayerShards(playerUuid, 10, shardType) } returns Result.success(Unit)
|
||||
|
||||
val result = manager.setPlayerShards(playerUuid, 10, shardType)
|
||||
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "setPlayerShards result should not be a failure") },
|
||||
{ assertEquals(10, cache.getBalance(playerUuid, shardType).getOrNull()) },
|
||||
{ coVerify { postgreSQL.setPlayerShards(playerUuid, 10, shardType) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set Player Shards TOTAL should fail")
|
||||
fun setPlayerShardsTotalFail() = runTest {
|
||||
val result = manager.setPlayerShards(playerUuid, 10, ShardType.TOTAL)
|
||||
assertTrue(result.isFailure, "setPlayerShards result should be a failure with ShardType.TOTAL")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set Player Shards fails when economy disabled")
|
||||
fun setPlayerShardsEconomyDisabled() = runTest {
|
||||
economyDisabled = true
|
||||
try {
|
||||
val result = manager.setPlayerShards(playerUuid, 10, ShardType.BANK)
|
||||
assertTrue(result.isFailure, "setPlayerShards result should be a failure when economy disabled")
|
||||
} finally {
|
||||
economyDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set Player Shards DB failure propagates, cache untouched")
|
||||
fun setPlayerShardsDbFail() = runTest {
|
||||
coEvery { postgreSQL.setPlayerShards(playerUuid, 10, ShardType.BANK) } returns
|
||||
Result.failure(RuntimeException("db error"))
|
||||
|
||||
val result = manager.setPlayerShards(playerUuid, 10, ShardType.BANK)
|
||||
|
||||
assertAll(
|
||||
{ assertTrue(result.isFailure, "setPlayerShards result should be a failure") },
|
||||
{ assertEquals(-1, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Add To Bank Shards")
|
||||
@CsvSource("No existing balance, 5, 0", "Existing balance, 10, 5")
|
||||
fun addToBankShards(@Suppress("UNUSED_PARAMETER") name: String, toRemove: Long, existingBalance: Long) = runTest {
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, ShardType.BANK) } returns Result.success(existingBalance)
|
||||
coEvery { postgreSQL.addToPlayerShards(playerUuid, toRemove, ShardType.BANK) } returns
|
||||
Result.success(toRemove - existingBalance)
|
||||
|
||||
val result = manager.addToBankShards(playerUuid, toRemove)
|
||||
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "addToBankShards result should not be a failure") },
|
||||
{ assertEquals(toRemove - existingBalance, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ coVerify { postgreSQL.addToPlayerShards(playerUuid, toRemove, ShardType.BANK) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Subtract From Bank Shards")
|
||||
fun subtractFromBankShards() = runTest {
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, ShardType.BANK) } returns Result.success(10)
|
||||
coEvery { postgreSQL.addToPlayerShards(playerUuid, -4, ShardType.BANK) } returns Result.success(6)
|
||||
|
||||
val result = manager.subtractFromBankShards(playerUuid, 4)
|
||||
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "subtractFromBankShards result should not be a failure") },
|
||||
{ assertEquals(6, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ coVerify { postgreSQL.addToPlayerShards(playerUuid, -4, ShardType.BANK) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Subtract From Bank Shards (insufficient balance)")
|
||||
fun subtractFromBankShardsInsufficientBalance() = runTest {
|
||||
cache.setBalance(playerUuid, 2, ShardType.BANK)
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, ShardType.BANK) } returns Result.success(2)
|
||||
coEvery { postgreSQL.addToPlayerShards(playerUuid, -4, ShardType.BANK) } returns Result.failure(Exception())
|
||||
|
||||
val result = manager.subtractFromBankShards(playerUuid, 4)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
assertTrue(
|
||||
result.exceptionOrNull() is DiamondBankException.InsufficientBalanceException,
|
||||
"subtractFromBankShards result should be an InsufficientBalanceException",
|
||||
)
|
||||
},
|
||||
{ assertEquals(2, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ coVerify(exactly = 0) { postgreSQL.addToPlayerShards(playerUuid, -4, ShardType.BANK) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Add To Bank Shards DB failure propagates")
|
||||
fun addToBankShardsDbFail() = runTest {
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, ShardType.BANK) } returns Result.success(5)
|
||||
coEvery { postgreSQL.addToPlayerShards(playerUuid, 5, ShardType.BANK) } returns
|
||||
Result.failure(RuntimeException("db error"))
|
||||
|
||||
val result = manager.addToBankShards(playerUuid, 5)
|
||||
|
||||
assertTrue(result.isFailure, "addToBankShards result should be a failure")
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Transfer Bank Shards")
|
||||
@CsvSource("No existing balance, 5, 0", "Existing balance, 5, 5")
|
||||
fun transferBankShards(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
toTransfer: Long,
|
||||
receiverExistingBalance: Long,
|
||||
) = runTest {
|
||||
val senderExistingBalance = 10L
|
||||
val newSenderBalance = senderExistingBalance - toTransfer
|
||||
val newReceiverBalance = receiverExistingBalance + toTransfer
|
||||
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, ShardType.BANK) } returns
|
||||
Result.success(senderExistingBalance)
|
||||
coEvery { postgreSQL.getShardTypeShards(otherPlayerUuid, ShardType.BANK) } returns
|
||||
Result.success(receiverExistingBalance)
|
||||
coEvery { postgreSQL.transferBankShards(playerUuid, otherPlayerUuid, toTransfer, toTransfer) } returns
|
||||
Result.success(mapOf(playerUuid to newSenderBalance, otherPlayerUuid to newReceiverBalance))
|
||||
|
||||
val result = manager.transferBankShards(playerUuid, otherPlayerUuid, toTransfer, toTransfer)
|
||||
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "transferBankShards result should not be a failure") },
|
||||
{ assertEquals(newSenderBalance, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ assertEquals(newReceiverBalance, cache.getBalance(otherPlayerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ coVerify { postgreSQL.transferBankShards(playerUuid, otherPlayerUuid, toTransfer, toTransfer) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Transfer Bank Shards (insufficient balance)")
|
||||
fun transferBankShardsInsufficientBalance() = runTest {
|
||||
cache.setBalance(playerUuid, 2, ShardType.BANK)
|
||||
cache.setBalance(otherPlayerUuid, 0, ShardType.BANK)
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, ShardType.BANK) } returns Result.success(2)
|
||||
coEvery { postgreSQL.transferBankShards(playerUuid, otherPlayerUuid, 4, 4) } returns Result.failure(Exception())
|
||||
|
||||
val result = manager.transferBankShards(playerUuid, otherPlayerUuid, 4, 4)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
assertTrue(
|
||||
result.exceptionOrNull() is DiamondBankException.InsufficientBalanceException,
|
||||
"transferBankShards result should be an InsufficientBalanceException",
|
||||
)
|
||||
},
|
||||
{ assertEquals(2, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ assertEquals(0, cache.getBalance(otherPlayerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ coVerify(exactly = 0) { postgreSQL.transferBankShards(playerUuid, otherPlayerUuid, 4, 4) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Transfer Bank Shards fails for negative shardsToSubtractFromSender")
|
||||
fun transferBankShardsNegativeSubtract() = runTest {
|
||||
assertThrows<IllegalArgumentException> { manager.transferBankShards(playerUuid, otherPlayerUuid, -1, 5) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Transfer Bank Shards fails for negative shardsToAddToReceiver")
|
||||
fun transferBankShardsNegativeAdd() = runTest {
|
||||
assertThrows<IllegalArgumentException> { manager.transferBankShards(playerUuid, otherPlayerUuid, 5, -1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Transfer Bank Shards fails for equal sender and receiver")
|
||||
fun transferBankShardsEqualSenderReceiver() = runTest {
|
||||
assertThrows<IllegalArgumentException> { manager.transferBankShards(playerUuid, playerUuid, 5, 5) }
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Get Shard Type Shards (cache miss then hit)")
|
||||
@CsvSource("Bank Shards, BANK", "Inventory Shards, INVENTORY", "Ender Chest Shards, ENDER_CHEST")
|
||||
fun getShardTypeShardsCacheMissThenHit(@Suppress("UNUSED_PARAMETER") name: String, enumName: String) = runTest {
|
||||
val shardType = ShardType.valueOf(enumName)
|
||||
coEvery { postgreSQL.getShardTypeShards(playerUuid, shardType) } returns Result.success(7)
|
||||
|
||||
val first = manager.getShardTypeShards(playerUuid, shardType)
|
||||
val second = manager.getShardTypeShards(playerUuid, shardType)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(7, first.getOrNull()) },
|
||||
{ assertEquals(7, second.getOrNull()) },
|
||||
{ coVerify(exactly = 1) { postgreSQL.getShardTypeShards(playerUuid, shardType) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get Total Shards (cache miss then hit)")
|
||||
fun getTotalShardsCacheMissThenHit() = runTest {
|
||||
coEvery { postgreSQL.getAllShards(playerUuid) } returns Result.success(PlayerShards(5, 6, 4))
|
||||
|
||||
val first = manager.getTotalShards(playerUuid)
|
||||
val second = manager.getTotalShards(playerUuid)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(15, first.getOrNull()) },
|
||||
{ assertEquals(15, second.getOrNull()) },
|
||||
{ coVerify(exactly = 1) { postgreSQL.getAllShards(playerUuid) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get All Shards (cache miss then hit)")
|
||||
fun getAllShardsCacheMissThenHit() = runTest {
|
||||
coEvery { postgreSQL.getAllShards(playerUuid) } returns Result.success(PlayerShards(1, 2, 3))
|
||||
|
||||
val first = manager.getAllShards(playerUuid)
|
||||
val second = manager.getAllShards(playerUuid)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(PlayerShards(1, 2, 3), first.getOrNull()) },
|
||||
{ assertEquals(PlayerShards(1, 2, 3), second.getOrNull()) },
|
||||
{ coVerify(exactly = 1) { postgreSQL.getAllShards(playerUuid) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get All Shards partial cache miss still hits DB")
|
||||
fun getAllShardsPartialCacheMiss() = runTest {
|
||||
cache.setBalance(playerUuid, 1, ShardType.BANK)
|
||||
coEvery { postgreSQL.getAllShards(playerUuid) } returns Result.success(PlayerShards(1, 2, 3))
|
||||
|
||||
val result = manager.getAllShards(playerUuid)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(PlayerShards(1, 2, 3), result.getOrNull()) },
|
||||
{ coVerify(exactly = 1) { postgreSQL.getAllShards(playerUuid) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cache For Player")
|
||||
fun cacheForPlayer() = runTest {
|
||||
coEvery { postgreSQL.getAllShards(playerUuid) } returns Result.success(PlayerShards(1, 2, 3))
|
||||
|
||||
val result = manager.cacheForPlayer(playerUuid)
|
||||
|
||||
assertAll(
|
||||
{ assertFalse(result.isFailure, "cacheForPlayer result should not be a failure") },
|
||||
{ assertEquals(1, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ assertEquals(2, cache.getBalance(playerUuid, ShardType.INVENTORY).getOrNull()) },
|
||||
{ assertEquals(3, cache.getBalance(playerUuid, ShardType.ENDER_CHEST).getOrNull()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Remove Cache For Player")
|
||||
suspend fun removeCacheForPlayer() {
|
||||
cache.setBalance(playerUuid, 1, ShardType.BANK)
|
||||
cache.setBalance(playerUuid, 2, ShardType.INVENTORY)
|
||||
cache.setBalance(playerUuid, 3, ShardType.ENDER_CHEST)
|
||||
|
||||
manager.removeCacheForPlayer(playerUuid)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(-1, cache.getBalance(playerUuid, ShardType.BANK).getOrNull()) },
|
||||
{ assertEquals(-1, cache.getBalance(playerUuid, ShardType.INVENTORY).getOrNull()) },
|
||||
{ assertEquals(-1, cache.getBalance(playerUuid, ShardType.ENDER_CHEST).getOrNull()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import java.util.*
|
||||
|
||||
object Constants {
|
||||
val playerUuid: UUID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||||
val otherPlayerUuid: UUID = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package net.trueog.diamondbankog
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.PlayerInventory
|
||||
|
||||
object Utils {
|
||||
suspend fun waitForCoroutines(scope: CoroutineScope) {
|
||||
scope.coroutineContext[Job]?.children?.forEach { it.join() }
|
||||
}
|
||||
|
||||
fun allImpl(contents: Array<ItemStack?>, material: Material): HashMap<Int, out ItemStack> =
|
||||
HashMap(
|
||||
contents.mapIndexedNotNull { index, item -> if (item?.type == material) index to item else null }.toMap()
|
||||
)
|
||||
|
||||
fun mockPlayerInventory(player: Player, uuid: UUID, contents: Array<ItemStack?>): PlayerInventory {
|
||||
val playerInventory = mockk<PlayerInventory>()
|
||||
var contents = contents.copyOf(41)
|
||||
every { playerInventory.contents } answers { contents }
|
||||
every { playerInventory.contents = any() } answers { contents = firstArg() }
|
||||
every { playerInventory.armorContents } answers { contents.dropLast(1).takeLast(4).toTypedArray() }
|
||||
every { playerInventory.itemInOffHand } answers { contents[40] ?: ItemStack(Material.AIR) }
|
||||
every { playerInventory.all(any<Material>()) } answers
|
||||
{
|
||||
val material = firstArg<Material>()
|
||||
allImpl(contents.take(36).toTypedArray(), material)
|
||||
}
|
||||
every { playerInventory.heldItemSlot } returns 0
|
||||
every { playerInventory.holder!!.uniqueId } returns uuid
|
||||
every { player.inventory } returns playerInventory
|
||||
return playerInventory
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.command.Balance
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertAll
|
||||
|
||||
class BalanceTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.balance") } returns true
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { Bukkit.getPlayer(playerUuid) } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Balance with no permission should fail")
|
||||
fun balanceNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.balance") } returns false
|
||||
|
||||
val balance = Balance(config, balanceManager, mm, scope)
|
||||
balance.onCommand(player, command, "balance", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.hasPermission("diamondbank-og.balance") } },
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ confirmVerified(player) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.command.Balancetop
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertAll
|
||||
|
||||
class BalancetopTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.balance") } returns true
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { Bukkit.getPlayer(playerUuid) } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Balancetop with no permission should fail")
|
||||
fun balancetopNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.balancetop") } returns false
|
||||
|
||||
val balancetop = Balancetop(config, balanceManager, mm, scope)
|
||||
balancetop.onCommand(player, command, "balancetop", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.hasPermission("diamondbank-og.balancetop") } },
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ confirmVerified(player) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import io.mockk.just
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.mockPlayerInventory
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.isInventoryLocked
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.transaction.command.Compress
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.InventoryExtensions.countShards
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.block.ShulkerBox
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.BlockStateMeta
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertAll
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
class CompressTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
@SpyK private var transactionLock = TransactionLock()
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.compress") } returns true
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { Bukkit.getPlayer(playerUuid) } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
|
||||
coEvery { balanceManager.setPlayerShards(any(), any(), ShardType.INVENTORY) } returns Result.success(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun compressData() =
|
||||
listOf(
|
||||
Arguments.of("Nothing", arrayOf<ItemStack>(), "<#FFA500>Nothing found to compress.", 0, 0, 0),
|
||||
Arguments.of(
|
||||
"9 shards to 1 diamond",
|
||||
arrayOf(Shard.createItemStack(9)),
|
||||
"Compression Summary:\n<red>-9 Diamond Shards\n<green>+1 Diamond",
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"9 diamonds to 1 diamond block",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 9)),
|
||||
"Compression Summary:\n<red>-9 Diamonds\n<green>+1 Diamond Block",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
Arguments.of(
|
||||
"9 diamonds and 9 shards to 1 diamond block and 1 diamond",
|
||||
arrayOf(Shard.createItemStack(9), ItemStack(Material.DIAMOND, 9)),
|
||||
"Compression Summary:\n<red>-9 Diamond Shards\n<red>-8 Diamonds\n<green>+1 Diamond Block",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun overflowCompressData() =
|
||||
listOf(
|
||||
Arguments.of(
|
||||
"10 shards",
|
||||
Array(36) { ItemStack(Material.DIRT, 1) } +
|
||||
Array(4) { ItemStack(Material.AIR, 1) } +
|
||||
arrayOf(Shard.createItemStack(10)),
|
||||
"You do not have enough space in your inventory to compress all the Diamond currency items (<green>+1 <aqua>Diamonds<red>).",
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"10 diamonds",
|
||||
Array(36) { ItemStack(Material.DIRT, 1) } +
|
||||
Array(4) { ItemStack(Material.AIR, 1) } +
|
||||
arrayOf(ItemStack(Material.DIAMOND, 10)),
|
||||
"You do not have enough space in your inventory to compress all the Diamond currency items (<green>+1 <aqua>Diamond Blocks<red>).",
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Successful Compress")
|
||||
@MethodSource("compressData")
|
||||
fun successfulCompress(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
inventoryContents: Array<ItemStack?>,
|
||||
compressionSummary: String,
|
||||
invShardCount: Long,
|
||||
invDiamondCount: Long,
|
||||
invDiamondBlockCount: Long,
|
||||
) = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $compressionSummary")) } },
|
||||
{ assertEquals(invShardCount, inventory.countShards(), "Shard count") },
|
||||
{ assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Shulker box Successful Compress")
|
||||
@MethodSource("compressData")
|
||||
fun successfulShulkerBoxCompress(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
inventoryContents: Array<ItemStack>,
|
||||
compressionSummary: String,
|
||||
invShardCount: Long,
|
||||
invDiamondCount: Long,
|
||||
invDiamondBlockCount: Long,
|
||||
) = runTest {
|
||||
val shulkerBox = ItemStack(Material.SHULKER_BOX)
|
||||
val blockStateMeta = shulkerBox.itemMeta as BlockStateMeta
|
||||
val shulkerBoxState = blockStateMeta.blockState as ShulkerBox
|
||||
inventoryContents.forEach { shulkerBoxState.inventory.addItem(it) }
|
||||
blockStateMeta.blockState = shulkerBoxState
|
||||
shulkerBox.itemMeta = blockStateMeta
|
||||
|
||||
mockPlayerInventory(player, playerUuid, arrayOf(shulkerBox))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("yes"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $compressionSummary")) } },
|
||||
{ assertEquals(invShardCount, shulkerBoxState.inventory.countShards(), "Shard count") },
|
||||
{ assertEquals(invDiamondCount, shulkerBoxState.inventory.countDiamonds(), "Diamond count") },
|
||||
{
|
||||
assertEquals(
|
||||
invDiamondBlockCount,
|
||||
shulkerBoxState.inventory.countDiamondBlocks(),
|
||||
"Diamond block count",
|
||||
)
|
||||
},
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Overflow Compress")
|
||||
@MethodSource("overflowCompressData")
|
||||
fun overflowCompress(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
inventoryContents: Array<ItemStack?>,
|
||||
error: String,
|
||||
invShardCount: Long,
|
||||
invDiamondCount: Long,
|
||||
invDiamondBlockCount: Long,
|
||||
) = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>$error")) } },
|
||||
{ assertEquals(invShardCount, inventory.countShards(), "Shard count") },
|
||||
{ assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress while in minigame should fail")
|
||||
fun compressWhileInMinigame() = runTest {
|
||||
every { player.world.name } returns "minigame_world"
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("all"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You cannot use /compress when in a minigame.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress while locked should fail")
|
||||
fun compressWhileLocked() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
|
||||
|
||||
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("all"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /compress.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress too many arguments should fail")
|
||||
fun compressTooManyArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("1", "2"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>Do not provide more arguments than \"yes\" if you want to compress the items in the shulker box you're holding."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress with invalid argument should fail")
|
||||
fun compressInvalidArg() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("invalid"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>Invalid argument.")) } },
|
||||
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress with no permission should fail")
|
||||
fun compressNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.compress") } returns false
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress with yes while not holding a shulker box should fail")
|
||||
fun compressYesNotHoldingShulkerBox() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf("yes"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>You are not holding a shulker box."))
|
||||
}
|
||||
},
|
||||
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compress without yes while holding a shulker box should fail")
|
||||
fun compressNoYesHoldingShulkerBox() = runTest {
|
||||
mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.SHULKER_BOX)))
|
||||
|
||||
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
|
||||
compress.onCommand(player, command, "compress", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <#FFA500>Are you sure you want to compress the Diamond currency items in the shulker box you're holding? If so, run \"/compress yes\""
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.*
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.mockPlayerInventory
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.isInventoryLocked
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.transaction.command.Deposit
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
class DepositTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
@SpyK private var transactionLock = TransactionLock()
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.deposit") } returns true
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { Bukkit.getPlayer(playerUuid) } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
coEvery { balanceManager.addToBankShards(any(), any()) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.setPlayerShards(any(), any(), ShardType.INVENTORY) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.insertTransactionLog(any(), any(), any(), any(), any()) } returns Result.success(Unit)
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun depositData() =
|
||||
listOf(
|
||||
Arguments.of(
|
||||
"1 Diamond",
|
||||
"1.0 <aqua>Diamond",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 1)),
|
||||
"1",
|
||||
arrayOf<Long>(9),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2 Diamonds",
|
||||
"2.0 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 3)),
|
||||
"2",
|
||||
arrayOf<Long>(2 * 9),
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.2 Diamonds only diamonds",
|
||||
"2.2 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 3)),
|
||||
"2.2",
|
||||
arrayOf<Long>(20),
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.2 Diamonds",
|
||||
"2.2 <aqua>Diamonds",
|
||||
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 3)),
|
||||
"2.2",
|
||||
arrayOf<Long>(20),
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.2 Diamonds with blocks",
|
||||
"2.2 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND_BLOCK, 1)),
|
||||
"2.2",
|
||||
arrayOf<Long>(20),
|
||||
7,
|
||||
6,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.9 Diamonds",
|
||||
"3.0 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 4)),
|
||||
"2.9",
|
||||
arrayOf<Long>(27),
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"all with 5 Diamonds",
|
||||
"5.0 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 5)),
|
||||
"all",
|
||||
arrayOf<Long>(45),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"all with 5 Diamonds and 8 shards",
|
||||
"5.8 <aqua>Diamonds",
|
||||
arrayOf(Shard.createItemStack(8), ItemStack(Material.DIAMOND, 5)),
|
||||
"all",
|
||||
arrayOf<Long>(53),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"1.8 Diamonds with overflow",
|
||||
"1.8 <aqua>Diamonds",
|
||||
Array(36) { ItemStack(Material.DIRT, 1) } +
|
||||
Array(4) { ItemStack(Material.AIR, 1) } +
|
||||
arrayOf(ItemStack(Material.DIAMOND, 3)),
|
||||
"1.8",
|
||||
arrayOf<Long>(17, 1),
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"1.1 Diamonds with diamond block with overflow",
|
||||
"1.1 <aqua>Diamonds",
|
||||
Array(36) { ItemStack(Material.DIRT, 1) } +
|
||||
Array(4) { ItemStack(Material.AIR, 1) } +
|
||||
arrayOf(ItemStack(Material.DIAMOND_BLOCK, 2)),
|
||||
"1.1",
|
||||
arrayOf<Long>(10, 63, 8),
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
Arguments.of(
|
||||
"2 renamed Diamond Shards",
|
||||
"0.2 <aqua>Diamonds",
|
||||
arrayOf(
|
||||
Shard.createItemStack(2).apply {
|
||||
val meta = itemMeta
|
||||
meta.displayName(Component.text("Renamed Diamond Shard"))
|
||||
itemMeta = meta
|
||||
}
|
||||
),
|
||||
"0.2",
|
||||
arrayOf<Long>(2),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Successful Deposit")
|
||||
@MethodSource("depositData")
|
||||
fun successfulDeposit(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
successDiamondText: String,
|
||||
inventoryContents: Array<ItemStack?>,
|
||||
commandArg: String,
|
||||
addedShards: Array<Long>,
|
||||
invShardCount: Long,
|
||||
invDiamondCount: Long,
|
||||
invDiamondBlockCount: Long,
|
||||
) = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf(commandArg))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
buildList {
|
||||
add {
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <green>Successfully deposited <yellow>$successDiamondText <green>into your bank account."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
addedShards.forEach { add { coVerify { balanceManager.addToBankShards(playerUuid, it) } } }
|
||||
add { assertEquals(invShardCount, inventory.countShards(), "Shard count") }
|
||||
add { assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") }
|
||||
add { assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") }
|
||||
add { assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") }
|
||||
add { assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deposit 2 Diamonds while only having 1")
|
||||
fun depositWholeDiamondsWhileShort() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 1)))
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf("2"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>You do not have <yellow>2.0 <aqua>Diamonds <red>to deposit."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(1, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deposit while in minigame should fail")
|
||||
fun depositWhileInMinigame() = runTest {
|
||||
every { player.world.name } returns "minigame_world"
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf("all"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You cannot use /deposit when in a minigame.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deposit while locked should fail")
|
||||
fun depositWhileLocked() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
|
||||
|
||||
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf("all"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /deposit.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deposit no arguments should fail")
|
||||
fun depositNoArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to deposit."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deposit too many arguments should fail")
|
||||
fun depositTooManyArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf("1", "2"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to deposit. Either a number or \"all\"."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Deposit with invalid argument should fail")
|
||||
@CsvSource(
|
||||
"Not a number, abc, <red>Invalid argument",
|
||||
"Negative, -1, <red>You cannot deposit a negative or zero amount",
|
||||
"Zero, 0, <red>You cannot deposit a negative or zero amount",
|
||||
"More than one decimal digit, 1.11, <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information",
|
||||
)
|
||||
fun depositInvalidArg(@Suppress("UNUSED_PARAMETER") name: String, argument: String, errorMessage: String) =
|
||||
runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf(argument))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $errorMessage.")) } },
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deposit with no permission should fail")
|
||||
fun depositNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.deposit") } returns false
|
||||
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
|
||||
|
||||
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
|
||||
deposit.onCommand(player, command, "deposit", arrayOf("1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
|
||||
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.*
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.luckperms.api.LuckPerms
|
||||
import net.luckperms.api.model.user.User
|
||||
import net.luckperms.api.node.Node
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.otherPlayerUuid
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.mockPlayerInventory
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.Shard
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.isInventoryLocked
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.transaction.command.Pay
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamondBlocks
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countTotal
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
class PayTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var otherPlayer: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
@MockK private lateinit var luckPerms: LuckPerms
|
||||
|
||||
@SpyK private var transactionLock = TransactionLock()
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.name } returns "Player"
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.pay") } returns true
|
||||
every { otherPlayer.uniqueId } returns otherPlayerUuid
|
||||
every { otherPlayer.name } returns "OtherPlayer"
|
||||
every { otherPlayer.isOnline } returns true
|
||||
every { otherPlayer.player } returns otherPlayer
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { Bukkit.getPlayer(playerUuid) } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
every { Bukkit.getPlayer("otherplayer") } returns otherPlayer
|
||||
every { server.getPlayer("otherplayer") } returns otherPlayer
|
||||
every { Bukkit.getPlayer(otherPlayerUuid) } returns otherPlayer
|
||||
every { server.getPlayer(otherPlayerUuid) } returns otherPlayer
|
||||
|
||||
every { otherPlayer.hasPlayedBefore() } returns true
|
||||
coEvery { balanceManager.transferBankShards(any(), any(), any(), any()) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.addToBankShards(any(), any()) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.setPlayerShards(any(), any(), ShardType.INVENTORY) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.subtractFromBankShards(any(), any()) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.insertTransactionLog(any(), any(), any(), any(), any()) } returns Result.success(Unit)
|
||||
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
every { otherPlayer.sendMessage(any<Component>()) } just Runs
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun payData() =
|
||||
listOf(
|
||||
Arguments.of(
|
||||
"1 Diamond from inventory",
|
||||
"1.0 <aqua>Diamond",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 1)),
|
||||
0,
|
||||
0,
|
||||
"1",
|
||||
arrayOf<Long>(),
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2 Diamonds from inventory",
|
||||
"2.0 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 3)),
|
||||
0,
|
||||
0,
|
||||
"2",
|
||||
arrayOf<Long>(),
|
||||
2 * 9,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.2 Diamonds from inventory only diamonds",
|
||||
"2.2 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 3)),
|
||||
0,
|
||||
0,
|
||||
"2.2",
|
||||
arrayOf<Long>(),
|
||||
20,
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.2 Diamonds from inventory",
|
||||
"2.2 <aqua>Diamonds",
|
||||
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 3)),
|
||||
0,
|
||||
0,
|
||||
"2.2",
|
||||
arrayOf<Long>(),
|
||||
20,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.2 Diamonds from inventory with blocks",
|
||||
"2.2 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND_BLOCK, 1)),
|
||||
0,
|
||||
0,
|
||||
"2.2",
|
||||
arrayOf<Long>(),
|
||||
20,
|
||||
7,
|
||||
6,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"2.9 Diamonds from inventory",
|
||||
"3.0 <aqua>Diamonds",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 4)),
|
||||
0,
|
||||
0,
|
||||
"2.9",
|
||||
arrayOf<Long>(),
|
||||
27,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"1.8 Diamonds from inventory with overflow",
|
||||
"1.8 <aqua>Diamonds",
|
||||
Array(36) { ItemStack(Material.DIRT, 1) } +
|
||||
Array(4) { ItemStack(Material.AIR, 1) } +
|
||||
arrayOf(ItemStack(Material.DIAMOND, 3)),
|
||||
0,
|
||||
0,
|
||||
"1.8",
|
||||
arrayOf<Long>(1),
|
||||
17,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"1.1 Diamonds from inventory with diamond block with overflow",
|
||||
"1.1 <aqua>Diamonds",
|
||||
Array(36) { ItemStack(Material.DIRT, 1) } +
|
||||
Array(4) { ItemStack(Material.AIR, 1) } +
|
||||
arrayOf(ItemStack(Material.DIAMOND_BLOCK, 2)),
|
||||
0,
|
||||
0,
|
||||
"1.1",
|
||||
arrayOf<Long>(63, 8),
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
Arguments.of(
|
||||
"1 Diamond: 0.5 Diamonds from bank and 1 Diamond in inventory",
|
||||
"1.0 <aqua>Diamond",
|
||||
arrayOf(ItemStack(Material.DIAMOND, 1)),
|
||||
5,
|
||||
5,
|
||||
"1",
|
||||
arrayOf<Long>(),
|
||||
9,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
Arguments.of(
|
||||
"5 Diamonds from bank",
|
||||
"5.0 <aqua>Diamonds",
|
||||
arrayOf<ItemStack>(),
|
||||
45,
|
||||
45,
|
||||
"5",
|
||||
arrayOf<Long>(),
|
||||
45,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Successful Pay")
|
||||
@MethodSource("payData")
|
||||
fun successfulPay(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
successDiamondText: String,
|
||||
inventoryContents: Array<ItemStack?>,
|
||||
bankShards: Long,
|
||||
bankShardsRemoved: Long,
|
||||
commandArg: String,
|
||||
addedShardsPlayer: Array<Long>,
|
||||
addedShardsOtherPlayer: Long,
|
||||
invShardCount: Long,
|
||||
invDiamondCount: Long,
|
||||
invDiamondBlockCount: Long,
|
||||
) = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
|
||||
|
||||
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(bankShards)
|
||||
|
||||
val user = mockk<User>()
|
||||
every { user.nodes } returns mutableListOf<Node>()
|
||||
every { luckPerms.userManager.getUser(any<UUID>()) } returns user
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("otherplayer", commandArg))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
buildList {
|
||||
add {
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <green>Successfully paid <yellow>$successDiamondText <green>to OtherPlayer<reset><green>."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
add {
|
||||
verify {
|
||||
otherPlayer.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <green>Player<reset> <green>has paid you <yellow>$successDiamondText<green>."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
addedShardsPlayer.forEach { add { coVerify { balanceManager.addToBankShards(playerUuid, it) } } }
|
||||
add {
|
||||
coVerify {
|
||||
balanceManager.transferBankShards(
|
||||
playerUuid,
|
||||
otherPlayerUuid,
|
||||
bankShardsRemoved,
|
||||
addedShardsOtherPlayer,
|
||||
)
|
||||
}
|
||||
}
|
||||
add { assertEquals(invShardCount, inventory.countShards(), "Shard count") }
|
||||
add { assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") }
|
||||
add { assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") }
|
||||
add { assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") }
|
||||
add { assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pay while in minigame should fail")
|
||||
fun payWhileInMinigame() = runTest {
|
||||
every { player.world.name } returns "minigame_world"
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("otherplayer", "1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You cannot use /pay when in a minigame.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pay while locked should fail")
|
||||
fun payWhileLocked() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("otherplayer", "1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /pay.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pay no arguments should fail")
|
||||
fun payNoArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>You did not provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pay too few arguments should fail")
|
||||
fun payTooFewArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pay too many arguments should fail")
|
||||
fun payTooManyArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("1", "2", "3"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Pay with invalid argument should fail")
|
||||
@CsvSource(
|
||||
"Not a number, abc, <red>Invalid argument",
|
||||
"Negative, -1, <red>You cannot pay a negative or zero amount",
|
||||
"Zero, 0, <red>You cannot pay a negative or zero amount",
|
||||
"More than one decimal digit, 1.11, <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information",
|
||||
)
|
||||
fun payInvalidArg(@Suppress("UNUSED_PARAMETER") name: String, argument: String, errorMessage: String) = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("otherplayer", argument))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $errorMessage.")) } },
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Pay with no permission should fail")
|
||||
fun payNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.pay") } returns false
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
|
||||
pay.onCommand(player, command, "pay", arrayOf("1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.command.SetBankBalance
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertAll
|
||||
|
||||
class SetBankBalanceTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.setbankbalance") } returns true
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { Bukkit.getPlayer(playerUuid) } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SetBankBalance with no permission should fail")
|
||||
fun setBankBalanceNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.setbankbalance") } returns false
|
||||
|
||||
val setBankBalance = SetBankBalance(config, balanceManager, mm, scope)
|
||||
setBankBalance.onCommand(player, command, "setbankbalance", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.hasPermission("diamondbank-og.setbankbalance") } },
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.setPlayerShards(any(), any(), any()) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
package net.trueog.diamondbankog.commands
|
||||
|
||||
import io.mockk.*
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.trueog.diamondbankog.BukkitMock
|
||||
import net.trueog.diamondbankog.Constants.playerUuid
|
||||
import net.trueog.diamondbankog.Utils.mockPlayerInventory
|
||||
import net.trueog.diamondbankog.Utils.waitForCoroutines
|
||||
import net.trueog.diamondbankog.balance.BalanceManager
|
||||
import net.trueog.diamondbankog.balance.shard.ShardType
|
||||
import net.trueog.diamondbankog.config.Config
|
||||
import net.trueog.diamondbankog.transaction.InventoryLockExtensions.isInventoryLocked
|
||||
import net.trueog.diamondbankog.transaction.TransactionLock
|
||||
import net.trueog.diamondbankog.transaction.command.Withdraw
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countDiamonds
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countShards
|
||||
import net.trueog.diamondbankog.util.PlayerInventoryExtensions.countTotal
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Server
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
|
||||
class WithdrawTest {
|
||||
@MockK private lateinit var config: Config
|
||||
|
||||
@MockK private lateinit var balanceManager: BalanceManager
|
||||
|
||||
@MockK private lateinit var mm: MiniMessage
|
||||
|
||||
@MockK private lateinit var player: Player
|
||||
|
||||
@MockK private lateinit var command: Command
|
||||
|
||||
@SpyK private var transactionLock = TransactionLock()
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
every { config.prefix } returns "DiamondBank-OG"
|
||||
|
||||
val inputSlot = slot<String>()
|
||||
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
|
||||
|
||||
every { player.uniqueId } returns playerUuid
|
||||
every { player.world.name } returns "world"
|
||||
every { player.hasPermission("diamondbank-og.withdraw") } returns true
|
||||
|
||||
every { Bukkit.getPlayer("player") } returns player
|
||||
every { server.getPlayer("player") } returns player
|
||||
every { server.getPlayer(playerUuid) } returns player
|
||||
|
||||
coEvery { balanceManager.addToBankShards(any(), any()) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.setPlayerShards(any(), any(), ShardType.INVENTORY) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.subtractFromBankShards(any(), any()) } returns Result.success(Unit)
|
||||
coEvery { balanceManager.insertTransactionLog(any(), any(), any(), any(), any()) } returns Result.success(Unit)
|
||||
every { player.sendMessage(any<Component>()) } just Runs
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var server: Server
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setupBukkit() {
|
||||
server = BukkitMock.mockBukkit()
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Successful withdraw")
|
||||
@CsvSource(
|
||||
"2 Diamonds, 2.0 <aqua>Diamonds, 2, 0, 2, 18",
|
||||
"2.2 Diamonds, 2.2 <aqua>Diamonds, 2.2, 2, 2, 20",
|
||||
"2.9 Diamonds, 3.0 <aqua>Diamonds, 2.9, 0, 3, 27",
|
||||
"All, 11.1 <aqua>Diamonds, all, 1, 11, 100",
|
||||
)
|
||||
fun successfulWithdraw(
|
||||
@Suppress("UNUSED_PARAMETER") name: String,
|
||||
successDiamondText: String,
|
||||
commandArg: String,
|
||||
invShardsCount: Long,
|
||||
invDiamondsCount: Long,
|
||||
removedShardCount: Long,
|
||||
) = runTest {
|
||||
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(100)
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf(commandArg))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <green>Successfully withdrew <yellow>$successDiamondText <green>from your bank account."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify { balanceManager.subtractFromBankShards(playerUuid, removedShardCount) } },
|
||||
{ assertEquals(invShardsCount, inventory.countShards(), "Shard count") },
|
||||
{ assertEquals(invDiamondsCount, inventory.countDiamonds(), "Diamond count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw while in minigame should fail")
|
||||
fun withdrawWhileInMinigame() = runTest {
|
||||
every { player.world.name } returns "minigame_world"
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf("all"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You cannot use /withdraw when in a minigame.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw while locked should fail")
|
||||
fun withdrawWhileLocked() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf("all"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /withdraw.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw no arguments should fail")
|
||||
fun withdrawNoArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf())
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to withdraw."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw too many arguments should fail")
|
||||
fun withdrawTooManyArgs() = runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf("1", "2"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to withdraw. Either a number or \"all\"."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@DisplayName("Withdraw with invalid argument should fail")
|
||||
@CsvSource(
|
||||
"Not a number, abc, <red>Invalid argument",
|
||||
"Negative, -1, <red>You cannot withdraw a negative or zero amount",
|
||||
"Zero, 0, <red>You cannot withdraw a negative or zero amount",
|
||||
"More than one decimal digit, 1.11, <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information",
|
||||
)
|
||||
fun withdrawInvalidArg(@Suppress("UNUSED_PARAMETER") name: String, argument: String, errorMessage: String) =
|
||||
runTest {
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf(argument))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $errorMessage.")) } },
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw with no permission should fail")
|
||||
fun withdrawNoPermission() = runTest {
|
||||
every { player.hasPermission("diamondbank-og.withdraw") } returns false
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf("1"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw when not enough Diamonds in bank should fail")
|
||||
fun withdrawNotEnoughDiamondsInBank() = runTest {
|
||||
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(18)
|
||||
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf("5"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>Cannot withdraw <yellow>5.0 <aqua>Diamonds <red>because your bank only contains <yellow>2.0 <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Withdraw when not enough inventory space should fail")
|
||||
fun withdrawNotEnoughInventorySpace() = runTest {
|
||||
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(18)
|
||||
val inventory = mockPlayerInventory(player, playerUuid, Array(41) { ItemStack(Material.DIRT, 1) })
|
||||
|
||||
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
|
||||
withdraw.onCommand(player, command, "withdraw", arrayOf("2"))
|
||||
waitForCoroutines(scope)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
verify {
|
||||
player.sendMessage(
|
||||
Component.text(
|
||||
"DiamondBank-OG<reset>: <red>You don't have enough inventory space to withdraw <yellow>2.0 <aqua>Diamonds<red>."
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
|
||||
{ assertEquals(0, inventory.countTotal(), "Total count") },
|
||||
{ assertFalse(playerUuid.isInventoryLocked(), "Inventory locked") },
|
||||
{ assertFalse(transactionLock.isLocked(playerUuid), "Transaction lock") },
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user