diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000000..c73b64aed2 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +docs/adr diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..0be416846f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Automatically normalize line endings (to LF) for all text-based files. +* text=auto eol=lf diff --git a/.github/workflows/discord-webhook.yml b/.github/workflows/discord-webhook.yml index f4c018a140..3364a75bb1 100644 --- a/.github/workflows/discord-webhook.yml +++ b/.github/workflows/discord-webhook.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 - name: Set up Java JDK 17 - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'adopt' java-version: '17' diff --git a/.github/workflows/e2e-testing.yml b/.github/workflows/e2e-testing.yml new file mode 100644 index 0000000000..0f7cc57413 --- /dev/null +++ b/.github/workflows/e2e-testing.yml @@ -0,0 +1,77 @@ +name: End to End Testing + +on: + workflow_call: + inputs: + artifact-name: + description: 'Slimefun artifact name' + required: true + type: string + +jobs: + e2e-testing: + name: End to End Testing + runs-on: ubuntu-latest + timeout-minutes: 5 + + strategy: + matrix: + include: + - mcVersion: '1.16.5' + javaVersion: '16' + - mcVersion: '1.17.1' + javaVersion: '17' + - mcVersion: '1.18.2' + javaVersion: '18' + - mcVersion: '1.19.4' + javaVersion: '19' + - mcVersion: 'latest' + javaVersion: '20' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4.0.0 + with: + distribution: temurin + java-version: ${{ matrix.javaVersion }} + java-package: jdk + architecture: x64 + + - name: Setup server + run: | + echo 'eula=true' > eula.txt + mkdir plugins + + - name: Download ${{ matrix.mcVersion }} Paper + run: | + VERSION="${{ matrix.mcVersion }}" + if [ "$VERSION" == "latest" ]; then + VERSION=$(curl https://api.papermc.io/v2/projects/paper/ -s | jq -r '.versions[-1]') + fi + + BUILD_JAR=$(curl -s "https://api.papermc.io/v2/projects/paper/versions/$VERSION/builds" \ + | jq '.builds[-1] | "\(.build) \(.downloads.application.name)"' -r) + BUILD=$(echo "$BUILD_JAR" | awk '{print $1}') + JAR_FILE=$(echo "$BUILD_JAR" | awk '{print $2}') + + echo "Downloading... https://api.papermc.io/v2/projects/paper/versions/$VERSION/builds/$BUILD/downloads/$JAR_FILE" + curl -o paper.jar \ + "https://api.papermc.io/v2/projects/paper/versions/$VERSION/builds/$BUILD/downloads/$JAR_FILE" + + - name: Download Slimefun + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.artifact-name }} + path: plugins/ + + - name: Download e2e-tester + run: | + curl -o e2e-tester.jar https://preview-builds.walshy.dev/download/e2e-tester/main/latest + mv e2e-tester.jar plugins/e2e-tester.jar + + - name: Run server + run: | + java -jar paper.jar --nogui diff --git a/.github/workflows/json-validator.yml b/.github/workflows/json-validator.yml index 9d8364ceda..512a484828 100644 --- a/.github/workflows/json-validator.yml +++ b/.github/workflows/json-validator.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Validate wiki.json uses: docker://orrosenblatt/validate-json-action:latest@sha256:02370758b8b199e0477da11ecfdd498c75c561685056b5c31b925a4ab95df7f4 env: diff --git a/.github/workflows/maven-compiler.yml b/.github/workflows/maven-compiler.yml index ab13b31e5c..7529fdbd77 100644 --- a/.github/workflows/maven-compiler.yml +++ b/.github/workflows/maven-compiler.yml @@ -25,10 +25,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'adopt' java-version: '17' diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index d6e0720bb2..0636851f90 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -31,7 +31,7 @@ jobs: api: '🔧 API' compatibility: '🤝 Compatibility' - - uses: thollander/actions-comment-pull-request@v2.4.0 + - uses: thollander/actions-comment-pull-request@v2.4.3 name: Leave a comment about the applied label if: ${{ steps.labeller.outputs.applied != 0 }} with: @@ -40,7 +40,7 @@ jobs: Your Pull Request was automatically labelled as: "${{ steps.labeller.outputs.applied }}" Thank you for contributing to this project! ❤️ - - uses: thollander/actions-comment-pull-request@v2.4.0 + - uses: thollander/actions-comment-pull-request@v2.4.3 name: Leave a comment about our branch naming convention if: ${{ steps.labeller.outputs.applied == 0 }} with: diff --git a/.github/workflows/preview-builds.yml b/.github/workflows/preview-builds.yml index 947ded7153..b9e7b5d26a 100644 --- a/.github/workflows/preview-builds.yml +++ b/.github/workflows/preview-builds.yml @@ -20,7 +20,7 @@ jobs: # Kinda jank way to grab the PR and commit hash and then download the artifact # TODO: Move this code to our own mini-action - name: Grab PR & run ID and download the artifact - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index cb76b7412d..89b9374dcf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -14,13 +14,15 @@ jobs: setup-preview-build: name: Preview build runs-on: ubuntu-latest + outputs: + short-commit-hash: ${{ steps.env-setup.outputs.SHORT_COMMIT_HASH }} steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'adopt' java-version: '17' @@ -35,10 +37,12 @@ jobs: restore-keys: ${{ runner.os }}-m2 # Setup for the preview build - - run: | + - id: env-setup + run: | SHORT_COMMIT_HASH=$(git rev-parse --short=8 ${{ github.sha }}) JAR_VERSION="Preview Build #${{ github.event.number }}-$SHORT_COMMIT_HASH" echo "SHORT_COMMIT_HASH=$SHORT_COMMIT_HASH" >> "$GITHUB_ENV" + echo "SHORT_COMMIT_HASH=$SHORT_COMMIT_HASH" >> "$GITHUB_OUTPUT" echo "JAR_VERSION=$JAR_VERSION" >> "$GITHUB_ENV" sed -i "s/4.9-UNOFFICIAL<\/version>/$JAR_VERSION<\/version>/g" pom.xml @@ -50,3 +54,9 @@ jobs: with: name: slimefun-${{ github.event.number }}-${{ env.SHORT_COMMIT_HASH }} path: 'target/Slimefun v${{ env.JAR_VERSION }}.jar' + + call-workflows: + needs: [setup-preview-build] + uses: ./.github/workflows/e2e-testing.yml + with: + artifact-name: slimefun-${{ github.event.number }}-${{ needs.setup-preview-build.outputs.short-commit-hash }} diff --git a/.github/workflows/release-candidates.yml b/.github/workflows/release-candidates.yml index 1c279e172e..1f4bda442e 100644 --- a/.github/workflows/release-candidates.yml +++ b/.github/workflows/release-candidates.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: 'stable' diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 8454eaac19..6fe3823205 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.0.0 with: distribution: 'adopt' java-version: '17' diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index 3d87edf772..a1e22c99c5 100644 --- a/.github/workflows/yaml-linter.yml +++ b/.github/workflows/yaml-linter.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: YAML Linter uses: ibiqlik/action-yamllint@v3.1.1 with: diff --git a/.gitignore b/.gitignore index fda2f4a052..f025c1e196 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /.settings/ /.idea/ /.vscode/ +/data-store/ dependency-reduced-pom.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd01fbb11..fd52f7a3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Table of contents -- [Release Candidate 36 (TBD)](#release-candidate-36-tbd) -- [Release Candidate 35 (7 Jul 2023)](#release-candidate-35-7-jul-2023) +- [Release Candidate 37 (TBD)](#release-candidate-37-tbd) +- [Release Candidate 36 (20 Dec 2023)](#release-candidate-36-20-dec-2023) +- [Release Candidate 35 (07 Jul 2023)](#release-candidate-35-07-jul-2023) - [Release Candidate 34 (20 Jun 2023)](#release-candidate-34-20-jun-2023) - [Release Candidate 33 (07 Jan 2023)](#release-candidate-33-07-jan-2023) - [Release Candidate 32 (26 Jun 2022)](#release-candidate-32-26-jun-2022) @@ -36,15 +37,55 @@ - [Release Candidate 2 (29 Sep 2019)](#release-candidate-2-29-sep-2019) - [Release Candidate 1 (26 Sep 2019)](#release-candidate-1-26-sep-2019) -## Release Candidate 36 (TBD) +## Release Candidate 37 (TBD) + +## Release Candidate 36 (20 Dec 2023) #### Additions +* Added e2e testing to PRs to better ensure compatibility +* Added compatibility to 1.20+ +* Added rainbow armor +* Added grace periods to radiation +* Added cherry log to android woodcutter +* Added blackstone recipes to Grindstone and Ore Crusher (#3912) +* Added Enchanted Golden Apple recipe (suggestion #2147 from punished_Garett) (#3591) +* Added new flags for timings (#3246) +* Added yaw to GPS Waypoints +* (API) Add MultiBlockCraftEvent (#3928) +* (API) Add TalismanActivateEvent (#4045) #### Changes +* Changed the radiation system +* Removed backwards compatibility +* (API) Improve performance for clearAllBlockInfoAtChunk +* Change Energized GPS Transmitter values to follow the pattern of previous tiers (#3915) +* Allowed the sword of beheading to drop piglin heads +* Improvements to BlockStorage handling (#3911) +* Moved builds to https://blob.build #### Fixes - -## Release Candidate 35 (7 Jul 2023) +* Fix #3444 +* Fix #3507 +* Fix possible enchantment duplication +* Fix Different Time of Pan Recipes +* Fix some of the reported blocks not working (#3848) +* Fix Soulbound Runes not working (#3932) +* Fix #3836 +* Fix unable to craft soulbound backpack with woven backpack with id (#3939) +* Fix getting radiated when not supposed to +* Fix geo miner voiding resources +* Fix sensitive blocks attached to sf blocks not dropping (1.19+) +* Fix breaking sf block with not unlocked item duping contents (#3976) +* Fix the case of SlimefunItem#itemhandlers +* Fix taking damage on head collision while wearing elytra cap (#3760) +* Fix heads showing as steve (#4027) +* Fix grappling hook not working due to bat dying (#3926) +* Fix freezer material +* Fix auto update +* Fix rate limiting issues (#4042) +* Fix orebfuscator plugin with blocks when gold panning (#3921) + +## Release Candidate 35 (07 Jul 2023) #### Additions * Added `sounds.yml` file to configure sound effects for Slimefun diff --git a/README.md b/README.md index 72a597409f..46ce4c0096 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Here is a full summary of the differences between the two different versions of | **Bug Reports** | :heavy_check_mark: | :x: | | **testing before release** | :x: | :heavy_check_mark: | | **change logs** | :x: | :memo: **[change log](https://github.com/Slimefun/Slimefun4/blob/master/CHANGELOG.md)** | -| **Download link** | :floppy_disk: **[download latest](https://thebusybiscuit.github.io/builds/TheBusyBiscuit/Slimefun4/master/)** | :floppy_disk: **[download "stable"](https://thebusybiscuit.github.io/builds/TheBusyBiscuit/Slimefun4/stable/)** | +| **Download links** | :floppy_disk: **[download latest](https://blob.build/project/Slimefun4/Dev)** | :floppy_disk: **[download "stable"](https://blob.build/project/Slimefun4/RC)** | **:exclamation: We wholeheartedly recommend you to use _development builds_, they are the most recent version of Slimefun and also receive the most frequent updates! In fact, "stable" builds are so outdated that we won't accept bug reports from them at all.**
diff --git a/docs/adr/0001-storage-layer.md b/docs/adr/0001-storage-layer.md new file mode 100644 index 0000000000..0809ed04a2 --- /dev/null +++ b/docs/adr/0001-storage-layer.md @@ -0,0 +1,129 @@ +# 1. Storage layer + +Date: 2023-11-15 +Last update: 2023-12-27 + +**DO NOT rely on any APIs introduced until we finish the work completely!** + +## Status + +Work in progress + +## Context + +Slimefun has been around for a very long time and due to that, the way we +wrote persistence of data has also been around for a very long time. +While Slimefun has grown, the storage layer has never been adapted. +This means that even all these years later, it's using the same old saving/loading. +This isn't necessarily always bad, however, as Slimefun has grown both in terms of content +and the servers using it - we've seen some issues. + +Today, files are saved as YAML files (sometimes with just a JSON object per line), +which is good for a config format but not good for a data store. It can create very large files +that can get corrupted, the way we've been saving data often means loading it all at once as well +rather than lazy-loading and generally isn't very performant. + +For a long time we've been talking about rewriting our data storage in multiple forms +(you may have seen this referenced for "BlockStorage rewrite" or "SQL for PlayerProfiles", etc.). +Now is the time we start to do this, this will be a very large change and will not be done quickly or rushed. + +This ADR talks about the future of our data persistence. + +## Decision + +We want to create a new storage layer abstraction and implementations +which will be backwards-compatible but open up new ways of storing data +within Slimefun. The end end goal is we can quickly and easily support +new storage backends (such as binary storage, SQL, etc.) for things like +[PlayerProfile](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java), [BlockStorage](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java), etc. + +We also want to be generally more efficient in the way we save and load data. +Today, we load way more than is required. +We can improve memory usage by only loading what we need, when we need it. + +We will do this incrementally and at first, in an experimental context. +In that regard, we should aim to minimise the blast radius and lift as much +as possible. + +### Quick changes overview + +* New abstraction over storage to easily support multiple backends. +* Work towards moving away from the legacy YAML based storage. +* Lazy load and save data to more efficiently handle the data life cycle. + +### Implementation details + +There is a new interface called [`Storage`](TBD) which is what all storage +backends will implement. +This will have methods for loading and saving things like +[`PlayerProfile`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java) and [`BlockStorage`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java). + +Then, backends will implement these +(e.g. [`LegacyStorageBackend`](TBD) (today's YAML situation)) +in order to support these functions. +Not all storage backends are required support each data type. +e.g. SQL may not support [`BlockStorage`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java). + + +## Addons + +The goal is that Addons will be able to use and implement new storage backends +if they wish and also be extended so they can load/save things as they wish. + +The first few iterations will not focus on Addon support. We want to ensure +this new storage layer will work and supports what we need it to today. + +This ADR will be updated when we get to supporting Addons properly. + +## Considerations + +This will be a big change therefore we will be doing it as incrementally as +possible. +Changes will be tested while in the PR stage and merged into the Dev releases when possible. +We may do an experimental release if required. + +Phases do not (and very likely will not) be done within a single PR. They will also not have any timeframe attached to them. + +The current plan looks like this: + +* Phase 1 - Implement legacy data backend for [`PlayerProfile`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java). + * We want to load player data using the new storage layer with the current + data system. + * We'll want to monitor for any possible issues and generally refine + how this system should look +* Phase 2 - Implement new experimental binary backend for [`PlayerProfile`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java). + * Create a new backend for binary storage + * Implement in an experimental capacity and allow users to opt-in + * Provide a warning that this is **experimental** and there will be bugs. + * Implement new metric for storage backend being used +* Phase 3 - Mark the new backend as stable for [`PlayerProfile`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java). + * Mark it as stable and remove the warnings once we're sure things are + working correctly + * Create a migration path for users currently using "legacy". + * Enable by default for new servers +* Phase 4 - Move [`BlockStorage`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java) to new storage layer. + * The big one! We're gonna tackle adding this to BlockStorage. + This will probably be a large change and we'll want to be as + careful as possible here. + * Implement `legacy` and `binary` as experimental storage backends + for BlockStorage and allow users to opt-in + * Provide a warning that this is **experimental** and there will be bugs. +* Phase 5 - Mark the new storage layer as stable for [`BlockStorage`](https://github.com/Slimefun/Slimefun4/blob/bbfb9734b9f549d7e82291eff041f9b666a61b63/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java). + * Mark it as stable and remove the warnings once we're sure things are + working correctly + * Ensure migration path works here too. + * Enable by default for new servers +* Phase 6 - Finish up and move anything else we want over + * Move over any other data stores we have to the new layer + * We should probably still do experimental -> stable but it should have + less of a lead time. + +## State of work + +* Phase 1: In progress + * https://github.com/Slimefun/Slimefun4/pull/4065 +* Phase 2: Not started +* Phase 3: Not started +* Phase 4: Not started +* Phase 5: Not started +* Phase 6: Not started diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000000..1762af11cd --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,11 @@ +# ADR + +An ADR (Architecture Decision Record) is a document describing large changes, why we made them, etc. + +## Making a new ADR + +If you're making a large change to Slimefun, we recommend creating an ADR +in order to document why this is being made and how it works for future contributors. + +Please follow the general format of the former ADRs or use a tool +such as [`adr-tools`](https://github.com/npryce/adr-tools) to generate a new document. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8e4149c4e6..8fa8a7be1d 100644 --- a/pom.xml +++ b/pom.xml @@ -115,7 +115,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.12.0 @@ -146,7 +146,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.3 org.junit.jupiter:junit-jupiter @@ -158,14 +158,14 @@ org.sonarsource.scanner.maven sonar-maven-plugin - 3.9.1.2184 + 3.10.0.2594 org.jacoco jacoco-maven-plugin - 0.8.10 + 0.8.11 @@ -191,7 +191,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.0 + 3.5.1 @@ -239,7 +239,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.3 ${project.basedir} @@ -355,7 +355,7 @@ com.github.baked-libs.dough dough-api - 39856a32c4 + da42c2f268 compile @@ -383,19 +383,19 @@ org.junit.jupiter junit-jupiter - 5.10.0 + 5.10.1 test org.mockito mockito-core - 5.4.0 + 5.8.0 test org.slf4j slf4j-simple - 2.0.7 + 2.0.9 test @@ -418,7 +418,7 @@ com.sk89q.worldedit worldedit-core - 7.2.15 + 7.2.18 provided @@ -432,7 +432,7 @@ com.sk89q.worldedit worldedit-bukkit - 7.2.15 + 7.2.18 provided @@ -446,7 +446,7 @@ com.gmail.nossr50.mcMMO mcMMO - 2.1.222 + 2.1.226 provided @@ -460,7 +460,7 @@ me.clip placeholderapi - 2.11.3 + 2.11.5 provided @@ -488,7 +488,7 @@ com.github.LoneDev6 itemsadder-api - 3.5.0b + 3.6.1 provided @@ -502,7 +502,7 @@ net.imprex orebfuscator-api - 5.3.3 + 5.4.0 provided @@ -513,6 +513,12 @@ + + com.mojang + authlib + 1.5.25 + provided + commons-lang diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/BlockPlacerPlaceEvent.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/BlockPlacerPlaceEvent.java index 4ea35c510d..59ee4bdf51 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/BlockPlacerPlaceEvent.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/BlockPlacerPlaceEvent.java @@ -116,4 +116,4 @@ public HandlerList getHandlers() { return getHandlerList(); } -} \ No newline at end of file +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/MultiBlockCraftEvent.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/MultiBlockCraftEvent.java new file mode 100644 index 0000000000..7a3fc3c535 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/MultiBlockCraftEvent.java @@ -0,0 +1,120 @@ +package io.github.thebusybiscuit.slimefun4.api.events; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.inventory.ItemStack; + +import io.github.thebusybiscuit.slimefun4.core.multiblocks.MultiBlockMachine; + +/** + * This {@link Event} is called when a {@link Player} crafts an item using a {@link MultiBlockMachine}. + * Unlike the {@link MultiBlockInteractEvent}, this event only fires if an output to a craft is expected. + * If this event is cancelled, ingredients will not be consumed and no output item results. + * + * @author char321 + * @author JustAHuman + */ +public class MultiBlockCraftEvent extends PlayerEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + private final MultiBlockMachine machine; + private final ItemStack[] input; + private ItemStack output; + private boolean cancelled; + + /** + * Creates a new {@link MultiBlockCraftEvent}. + * + * @param p The player that crafts using a multiblock + * @param machine The multiblock machine used to craft + * @param input The input items of the craft + * @param output The resulting item of the craft + */ + @ParametersAreNonnullByDefault + public MultiBlockCraftEvent(Player p, MultiBlockMachine machine, ItemStack[] input, ItemStack output) { + super(p); + this.machine = machine; + this.input = input; + this.output = output; + } + + /** + * Creates a new {@link MultiBlockCraftEvent}. + * + * @param p The player that crafts using a multiblock + * @param machine The multiblock machine used to craft + * @param input The input item of the craft + * @param output The resulting item of the craft + */ + @ParametersAreNonnullByDefault + public MultiBlockCraftEvent(Player p, MultiBlockMachine machine, ItemStack input, ItemStack output) { + this(p, machine, new ItemStack[]{input}, output); + } + + /** + * Gets the machine that was used to craft. + * + * @return The {@link MultiBlockMachine} used to craft. + */ + public @Nonnull MultiBlockMachine getMachine() { + return machine; + } + + /** + * Gets the input of the craft. + * + * @return The {@link ItemStack ItemStack[]} input that is used in the craft. + */ + public @Nonnull ItemStack[] getInput() { + return input; + } + + /** + * Gets the output of the craft. + * + * @return The {@link ItemStack} output that results from the craft. + */ + public @Nonnull ItemStack getOutput() { + return output; + } + + /** + * Sets the output of the craft. Keep in mind that this overwrites any existing output. + * + * @param output + * The new item for the event to produce. + * + * @return The previous {@link ItemStack} output that was replaced. + */ + public @Nullable ItemStack setOutput(@Nullable ItemStack output) { + ItemStack oldOutput = this.output; + this.output = output; + return oldOutput; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static @Nonnull HandlerList getHandlerList() { + return handlers; + } + + @Override + public @Nonnull HandlerList getHandlers() { + return getHandlerList(); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunGuideOpenEvent.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunGuideOpenEvent.java index f4892242a4..ecf46a5311 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunGuideOpenEvent.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunGuideOpenEvent.java @@ -1,104 +1,104 @@ -package io.github.thebusybiscuit.slimefun4.api.events; - -import javax.annotation.Nonnull; - -import org.apache.commons.lang.Validate; -import org.bukkit.entity.Player; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.inventory.ItemStack; - -import io.github.thebusybiscuit.slimefun4.core.guide.SlimefunGuideMode; - -/** - * This {@link Event} is called whenever a {@link Player} tries to open the Slimefun Guide book. - * - * @author Linox - * - * @see SlimefunGuideMode - */ -public class SlimefunGuideOpenEvent extends Event implements Cancellable { - - private static final HandlerList handlers = new HandlerList(); - - private final Player player; - private final ItemStack guide; - private SlimefunGuideMode layout; - private boolean cancelled; - - public SlimefunGuideOpenEvent(@Nonnull Player p, @Nonnull ItemStack guide, @Nonnull SlimefunGuideMode layout) { - Validate.notNull(p, "The Player cannot be null"); - Validate.notNull(guide, "Guide cannot be null"); - Validate.notNull(layout, "Layout cannot be null"); - this.player = p; - this.guide = guide; - this.layout = layout; - } - - /** - * This returns the {@link Player} that tries to open - * the Slimefun Guide. - * - * @return The {@link Player} - */ - @Nonnull - public Player getPlayer() { - return player; - } - - /** - * This returns the {@link ItemStack} that {@link Player} - * tries to open the Slimefun Guide with. - * - * @return The {@link ItemStack} - */ - @Nonnull - public ItemStack getGuide() { - return guide; - } - - /** - * This returns the {@link SlimefunGuideMode} of the Slimefun Guide - * that {@link Player} tries to open. - * - * @return The {@link SlimefunGuideMode} - */ - @Nonnull - public SlimefunGuideMode getGuideLayout() { - return layout; - } - - /** - * Changes the {@link SlimefunGuideMode} that was tried to be opened with. - * - * @param layout - * The new {@link SlimefunGuideMode} - */ - public void setGuideLayout(@Nonnull SlimefunGuideMode layout) { - Validate.notNull(layout, "You must specify a layout that is not-null!"); - this.layout = layout; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public void setCancelled(boolean cancel) { - this.cancelled = cancel; - } - - @Nonnull - public static HandlerList getHandlerList() { - return handlers; - } - - @Nonnull - @Override - public HandlerList getHandlers() { - return getHandlerList(); - } - -} +package io.github.thebusybiscuit.slimefun4.api.events; + +import javax.annotation.Nonnull; + +import org.apache.commons.lang.Validate; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; + +import io.github.thebusybiscuit.slimefun4.core.guide.SlimefunGuideMode; + +/** + * This {@link Event} is called whenever a {@link Player} tries to open the Slimefun Guide book. + * + * @author Linox + * + * @see SlimefunGuideMode + */ +public class SlimefunGuideOpenEvent extends Event implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + + private final Player player; + private final ItemStack guide; + private SlimefunGuideMode layout; + private boolean cancelled; + + public SlimefunGuideOpenEvent(@Nonnull Player p, @Nonnull ItemStack guide, @Nonnull SlimefunGuideMode layout) { + Validate.notNull(p, "The Player cannot be null"); + Validate.notNull(guide, "Guide cannot be null"); + Validate.notNull(layout, "Layout cannot be null"); + this.player = p; + this.guide = guide; + this.layout = layout; + } + + /** + * This returns the {@link Player} that tries to open + * the Slimefun Guide. + * + * @return The {@link Player} + */ + @Nonnull + public Player getPlayer() { + return player; + } + + /** + * This returns the {@link ItemStack} that {@link Player} + * tries to open the Slimefun Guide with. + * + * @return The {@link ItemStack} + */ + @Nonnull + public ItemStack getGuide() { + return guide; + } + + /** + * This returns the {@link SlimefunGuideMode} of the Slimefun Guide + * that {@link Player} tries to open. + * + * @return The {@link SlimefunGuideMode} + */ + @Nonnull + public SlimefunGuideMode getGuideLayout() { + return layout; + } + + /** + * Changes the {@link SlimefunGuideMode} that was tried to be opened with. + * + * @param layout + * The new {@link SlimefunGuideMode} + */ + public void setGuideLayout(@Nonnull SlimefunGuideMode layout) { + Validate.notNull(layout, "You must specify a layout that is not-null!"); + this.layout = layout; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Nonnull + public static HandlerList getHandlerList() { + return handlers; + } + + @Nonnull + @Override + public HandlerList getHandlers() { + return getHandlerList(); + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunItemSpawnEvent.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunItemSpawnEvent.java index 27f544e7e8..95b5fadf6f 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunItemSpawnEvent.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/SlimefunItemSpawnEvent.java @@ -1,10 +1,14 @@ package io.github.thebusybiscuit.slimefun4.api.events; +import java.util.Optional; + import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import org.apache.commons.lang.Validate; import org.bukkit.Location; +import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; @@ -29,13 +33,29 @@ public class SlimefunItemSpawnEvent extends Event implements Cancellable { private ItemStack itemStack; private boolean cancelled; private final ItemSpawnReason itemSpawnReason; + private final Player player; @ParametersAreNonnullByDefault - public SlimefunItemSpawnEvent(Location location, ItemStack itemStack, ItemSpawnReason itemSpawnReason) { + public SlimefunItemSpawnEvent(@Nullable Player player, Location location, ItemStack itemStack, ItemSpawnReason itemSpawnReason) { this.location = location; this.itemStack = itemStack; this.itemSpawnReason = itemSpawnReason; this.cancelled = false; + this.player = player; + } + + @ParametersAreNonnullByDefault + public SlimefunItemSpawnEvent(Location location, ItemStack itemStack, ItemSpawnReason itemSpawnReason) { + this(null, location, itemStack, itemSpawnReason); + } + + /** + * Optionally returns the {@link Player} responsible for this spawn reason. + * + * @return The player responsible if applicable. + */ + public @Nonnull Optional getPlayer() { + return Optional.ofNullable(player); } /** diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/TalismanActivateEvent.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/TalismanActivateEvent.java new file mode 100644 index 0000000000..775691a0a6 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/events/TalismanActivateEvent.java @@ -0,0 +1,96 @@ +package io.github.thebusybiscuit.slimefun4.api.events; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.inventory.ItemStack; + +import io.github.thebusybiscuit.slimefun4.implementation.items.magical.talismans.Talisman; + +/** + * This {@link PlayerEvent} is called when a {@link Player} activates a {@link Talisman} + * + * @author cworldstar + */ +public class TalismanActivateEvent extends PlayerEvent implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + private final Talisman talisman; + private final ItemStack talismanItemStack; + private boolean preventConsumption = false; + private boolean cancelled = false; + + /** + * @param player + * The {@link Player} who activated the talisman. + * + * @param talisman + * The {@link Talisman} that was activated. + * + * @param talismanItem + * The {@link ItemStack} corresponding to the Talisman. + */ + @ParametersAreNonnullByDefault + public TalismanActivateEvent(Player player, Talisman talisman, ItemStack talismanItem) { + super(player); + this.talisman = talisman; + this.talismanItemStack = talismanItem; + } + + /** + * @return The {@link Talisman} used. + */ + public @Nonnull Talisman getTalisman() { + return this.talisman; + } + + /** + * @return The {@link ItemStack} of the used {@link Talisman}. + */ + public @Nonnull ItemStack getTalismanItem() { + return this.talismanItemStack; + } + + /** + * Only applies if {@link Talisman#isConsumable()} is true. + * Defaults to false. + * + * @return Whether the {@link ItemStack} should not be consumed. + */ + public boolean preventsConsumption() { + return this.preventConsumption; + } + + /** + * Only applies if {@link Talisman#isConsumable()} is true. + * + * @param preventConsumption + * Whether the {@link ItemStack} should not be consumed. + */ + public void setPreventConsumption(boolean preventConsumption) { + this.preventConsumption = preventConsumption; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @Nonnull HandlerList getHandlers() { + return getHandlerList(); + } + + public static @Nonnull HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/geo/ResourceManager.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/geo/ResourceManager.java index d15024440d..ce4fea9b02 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/geo/ResourceManager.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/geo/ResourceManager.java @@ -28,6 +28,7 @@ import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.items.geo.GEOMiner; import io.github.thebusybiscuit.slimefun4.implementation.items.geo.GEOScanner; +import io.github.thebusybiscuit.slimefun4.utils.ChatUtils; import io.github.thebusybiscuit.slimefun4.utils.ChestMenuUtils; import io.github.thebusybiscuit.slimefun4.utils.HeadTexture; @@ -236,13 +237,13 @@ public void scan(@Nonnull Player p, @Nonnull Block block, int page) { for (int i = page * 28; i < resources.size() && i < (page + 1) * 28; i++) { GEOResource resource = resources.get(i); OptionalInt optional = getSupplies(resource, block.getWorld(), x, z); - int supplies = optional.isPresent() ? optional.getAsInt() : generate(resource, block.getWorld(), x, block.getY(), z); - String suffix = Slimefun.getLocalization().getResourceString(p, supplies == 1 ? "tooltips.unit" : "tooltips.units"); + int supplies = optional.orElseGet(() -> generate(resource, block.getWorld(), x, block.getY(), z)); + String suffix = Slimefun.getLocalization().getResourceString(p, ChatUtils.checkPlurality("tooltips.unit", supplies)); ItemStack item = new CustomItemStack(resource.getItem(), "&f" + resource.getName(p), "&8\u21E8 &e" + supplies + ' ' + suffix); if (supplies > 1) { - item.setAmount(supplies > item.getMaxStackSize() ? item.getMaxStackSize() : supplies); + item.setAmount(Math.min(supplies, item.getMaxStackSize())); } menu.addItem(index, item, ChestMenuUtils.getEmptyClickHandler()); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/GPSNetwork.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/GPSNetwork.java index d2d9a3c1ee..47574fb2ca 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/GPSNetwork.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/GPSNetwork.java @@ -331,7 +331,7 @@ public void addWaypoint(@Nonnull Player p, @Nonnull String name, @Nonnull Locati } } - profile.addWaypoint(new Waypoint(profile, id, event.getLocation(), event.getName())); + profile.addWaypoint(new Waypoint(p.getUniqueId(), id, event.getLocation(), event.getName())); SoundEffect.GPS_NETWORK_ADD_WAYPOINT.playFor(p); Slimefun.getLocalization().sendMessage(p, "gps.waypoint.added", true); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/Waypoint.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/Waypoint.java index 13c8b42561..468b70af6b 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/Waypoint.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/gps/Waypoint.java @@ -1,11 +1,13 @@ package io.github.thebusybiscuit.slimefun4.api.gps; import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World.Environment; import org.bukkit.entity.Player; @@ -30,14 +32,14 @@ */ public class Waypoint { - private final PlayerProfile profile; + private final UUID ownerId; private final String id; private final String name; private final Location location; /** * This constructs a new {@link Waypoint} object. - * + * * @param profile * The owning {@link PlayerProfile} * @param id @@ -46,28 +48,62 @@ public class Waypoint { * The {@link Location} of the {@link Waypoint} * @param name * The name of this {@link Waypoint} + * + * @deprecated Use {@link #Waypoint(UUID, String, Location, String)} instead */ + @Deprecated @ParametersAreNonnullByDefault public Waypoint(PlayerProfile profile, String id, Location loc, String name) { - Validate.notNull(profile, "Profile must never be null!"); + this(profile.getUUID(), id, loc, name); + } + + /** + * This constructs a new {@link Waypoint} object. + * + * @param ownerId + * The owning {@link Player}'s {@link UUID} + * @param id + * The unique id for this {@link Waypoint} + * @param loc + * The {@link Location} of the {@link Waypoint} + * @param name + * The name of this {@link Waypoint} + */ + @ParametersAreNonnullByDefault + public Waypoint(UUID ownerId, String id, Location loc, String name) { + Validate.notNull(ownerId, "owner ID must never be null!"); Validate.notNull(id, "id must never be null!"); Validate.notNull(loc, "Location must never be null!"); Validate.notNull(name, "Name must never be null!"); - this.profile = profile; + this.ownerId = ownerId; this.id = id; this.location = loc; this.name = name; } /** - * This returns the owner of the {@link Waypoint}. + * This returns the owner's {@link UUID} of the {@link Waypoint}. * + * @return The corresponding owner's {@link UUID} + */ + @Nonnull + public UUID getOwnerId() { + return this.ownerId; + } + + /** + * This returns the owner of the {@link Waypoint}. + * * @return The corresponding {@link PlayerProfile} + * + * @deprecated Use {@link #getOwnerId()} instead */ @Nonnull + @Deprecated public PlayerProfile getOwner() { - return profile; + // This is jank and should never actually return null + return PlayerProfile.find(Bukkit.getOfflinePlayer(ownerId)).orElse(null); } /** @@ -126,7 +162,7 @@ public ItemStack getIcon() { */ @Override public int hashCode() { - return Objects.hash(profile.getUUID(), id, name, location); + return Objects.hash(this.ownerId, this.id, this.name, this.location); } /** @@ -139,7 +175,9 @@ public boolean equals(Object obj) { } Waypoint waypoint = (Waypoint) obj; - return profile.getUUID().equals(waypoint.getOwner().getUUID()) && id.equals(waypoint.getId()) && location.equals(waypoint.getLocation()) && name.equals(waypoint.getName()); + return this.ownerId.equals(waypoint.getOwnerId()) + && id.equals(waypoint.getId()) + && location.equals(waypoint.getLocation()) + && name.equals(waypoint.getName()); } - } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItem.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItem.java index 215c8270d8..28dc680ff1 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItem.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/items/SlimefunItem.java @@ -109,7 +109,7 @@ public class SlimefunItem implements Placeable { private Optional wikiURL = Optional.empty(); - private final OptionalMap, ItemHandler> itemhandlers = new OptionalMap<>(HashMap::new); + private final OptionalMap, ItemHandler> itemHandlers = new OptionalMap<>(HashMap::new); private final Set> itemSettings = new HashSet<>(); private boolean ticking = false; @@ -477,12 +477,12 @@ public void register(@Nonnull SlimefunAddon addon) { onEnable(); } else { // Clear item handlers if we are disabled so that calling them isn't possible later on - for (ItemHandler handler : this.itemhandlers.values()) { + for (ItemHandler handler : this.itemHandlers.values()) { if (handler instanceof BlockTicker) { Slimefun.getRegistry().getTickerBlocks().remove(getId()); } } - this.itemhandlers.clear(); + this.itemHandlers.clear(); } // Lock the SlimefunItemStack from any accidental manipulations @@ -540,7 +540,7 @@ private final void onEnable() { } private void loadItemHandlers() { - for (ItemHandler handler : itemhandlers.values()) { + for (ItemHandler handler : itemHandlers.values()) { Optional exception = handler.validate(this); // Check if the validation caused an exception. @@ -802,7 +802,7 @@ public final void addItemHandler(ItemHandler... handlers) { } for (ItemHandler handler : handlers) { - itemhandlers.put(handler.getIdentifier(), handler); + itemHandlers.put(handler.getIdentifier(), handler); // Tickers are a special case (at the moment at least) if (handler instanceof BlockTicker ticker) { @@ -914,7 +914,7 @@ public final void addOfficialWikipage(@Nonnull String page) { * @return The Set of item handlers */ public @Nonnull Collection getHandlers() { - return itemhandlers.values(); + return itemHandlers.values(); } /** @@ -932,7 +932,7 @@ public final void addOfficialWikipage(@Nonnull String page) { */ @ParametersAreNonnullByDefault public boolean callItemHandler(Class c, Consumer callable) { - Optional handler = itemhandlers.get(c); + Optional handler = itemHandlers.get(c); if (handler.isPresent()) { try { @@ -1159,6 +1159,17 @@ public final int hashCode() { return Slimefun.getRegistry().getSlimefunItemIds().get(id); } + /** + * Retrieve a {@link Optional} {@link SlimefunItem} by its id. + * + * @param id + * The id of the {@link SlimefunItem} + * @return The {@link Optional} {@link SlimefunItem} associated with that id. Empty if non-existent + */ + public static @Nonnull Optional getOptionalById(@Nonnull String id) { + return Optional.ofNullable(getById(id)); + } + /** * Retrieve a {@link SlimefunItem} from an {@link ItemStack}. * @@ -1181,4 +1192,14 @@ public final int hashCode() { } + /** + * Retrieve a {@link Optional} {@link SlimefunItem} from an {@link ItemStack}. + * + * @param item + * The {@link ItemStack} to check + * @return The {@link Optional} {@link SlimefunItem} associated with this {@link ItemStack} if present, otherwise empty + */ + public @Nonnull Optional getOptionalByItem(@Nullable ItemStack item) { + return Optional.ofNullable(getByItem(item)); + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerBackpack.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerBackpack.java index 4960a7c9b3..c0886778df 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerBackpack.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerBackpack.java @@ -2,10 +2,13 @@ import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; import org.bukkit.Bukkit; import org.bukkit.entity.HumanEntity; @@ -34,13 +37,22 @@ public class PlayerBackpack { private static final String CONFIG_PREFIX = "backpacks."; - private final PlayerProfile profile; + private final UUID ownerId; private final int id; - private final Config cfg; + @Deprecated + private PlayerProfile profile; + @Deprecated + private Config cfg; private Inventory inventory; private int size; + private PlayerBackpack(@Nonnull UUID ownerId, int id, int size) { + this.ownerId = ownerId; + this.id = id; + this.size = size; + } + /** * This constructor loads an existing Backpack * @@ -48,7 +60,10 @@ public class PlayerBackpack { * The {@link PlayerProfile} of this Backpack * @param id * The id of this Backpack + * + * @deprecated Use {@link PlayerBackpack#load(UUID, int, int, HashMap)} instead */ + @Deprecated public PlayerBackpack(@Nonnull PlayerProfile profile, int id) { this(profile, id, profile.getConfig().getInt(CONFIG_PREFIX + id + ".size")); @@ -66,12 +81,16 @@ public PlayerBackpack(@Nonnull PlayerProfile profile, int id) { * The id of this Backpack * @param size * The size of this Backpack + * + * @deprecated Use {@link PlayerBackpack#newBackpack(UUID, int, int)} instead */ + @Deprecated public PlayerBackpack(@Nonnull PlayerProfile profile, int id, int size) { if (size < 9 || size > 54 || size % 9 != 0) { throw new IllegalArgumentException("Invalid size! Size must be one of: [9, 18, 27, 36, 45, 54]"); } + this.ownerId = profile.getUUID(); this.profile = profile; this.id = id; this.cfg = profile.getConfig(); @@ -96,10 +115,17 @@ public int getId() { * This method returns the {@link PlayerProfile} this {@link PlayerBackpack} belongs to * * @return The owning {@link PlayerProfile} + * + * @deprecated Use {@link PlayerBackpack#getOwnerId()} instead */ + @Deprecated @Nonnull public PlayerProfile getOwner() { - return profile; + return profile != null ? profile : PlayerProfile.find(Bukkit.getOfflinePlayer(ownerId)).orElse(null); + } + + public UUID getOwnerId() { + return this.ownerId; } /** @@ -172,7 +198,6 @@ public void setSize(int size) { } this.size = size; - cfg.setValue(CONFIG_PREFIX + id + ".size", size); Inventory inv = Bukkit.createInventory(null, size, "Backpack [" + size + " Slots]"); @@ -187,7 +212,10 @@ public void setSize(int size) { /** * This method will save the contents of this backpack to a {@link File}. + * + * @deprecated Handled by {@link PlayerProfile#save()} now */ + @Deprecated public void save() { for (int i = 0; i < size; i++) { cfg.setValue(CONFIG_PREFIX + id + ".contents." + i, inventory.getItem(i)); @@ -199,7 +227,40 @@ public void save() { * using {@link PlayerBackpack#save()} */ public void markDirty() { - profile.markDirty(); + if (profile != null) { + profile.markDirty(); + } } + private void setContents(int size, HashMap contents) { + if (this.inventory == null) { + this.inventory = Bukkit.createInventory(null, size, "Backpack [" + size + " Slots]"); + } + + for (int i = 0; i < size; i++) { + this.inventory.setItem(i, contents.get(i)); + } + } + + @ParametersAreNonnullByDefault + public static PlayerBackpack load(UUID ownerId, int id, int size, HashMap contents) { + PlayerBackpack backpack = new PlayerBackpack(ownerId, id, size); + + backpack.setContents(size, contents); + + return backpack; + } + + @ParametersAreNonnullByDefault + public static PlayerBackpack newBackpack(UUID ownerId, int id, int size) { + if (size < 9 || size > 54 || size % 9 != 0) { + throw new IllegalArgumentException("Invalid size! Size must be one of: [9, 18, 27, 36, 45, 54]"); + } + + PlayerBackpack backpack = new PlayerBackpack(ownerId, id, size); + + backpack.setContents(size, new HashMap<>()); + + return backpack; + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java index 29accb9509..c51888a1d0 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/player/PlayerProfile.java @@ -1,26 +1,22 @@ package io.github.thebusybiscuit.slimefun4.api.player; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; +import java.util.Collection; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; -import java.util.logging.Level; import java.util.stream.IntStream; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import io.github.thebusybiscuit.slimefun4.storage.data.PlayerData; import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.ChatColor; -import org.bukkit.Location; import org.bukkit.NamespacedKey; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; @@ -36,7 +32,6 @@ import io.github.thebusybiscuit.slimefun4.api.events.AsyncProfileLoadEvent; import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint; import io.github.thebusybiscuit.slimefun4.api.items.HashedArmorpiece; -import io.github.thebusybiscuit.slimefun4.api.items.ItemState; import io.github.thebusybiscuit.slimefun4.api.researches.Research; import io.github.thebusybiscuit.slimefun4.core.attributes.ProtectionType; import io.github.thebusybiscuit.slimefun4.core.attributes.ProtectiveArmor; @@ -59,50 +54,26 @@ */ public class PlayerProfile { - private final UUID uuid; + private final UUID ownerId; private final String name; private final Config configFile; - private final Config waypointsFile; private boolean dirty = false; private boolean markedForDeletion = false; - private final Set researches = new HashSet<>(); - private final List waypoints = new ArrayList<>(); - private final Map backpacks = new HashMap<>(); private final GuideHistory guideHistory = new GuideHistory(this); private final HashedArmorpiece[] armor = { new HashedArmorpiece(), new HashedArmorpiece(), new HashedArmorpiece(), new HashedArmorpiece() }; - protected PlayerProfile(@Nonnull OfflinePlayer p) { - this.uuid = p.getUniqueId(); - this.name = p.getName(); - - configFile = new Config("data-storage/Slimefun/Players/" + uuid.toString() + ".yml"); - waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid.toString() + ".yml"); - - loadProfileData(); - } + private final PlayerData data; - private void loadProfileData() { - for (Research research : Slimefun.getRegistry().getResearches()) { - if (configFile.contains("researches." + research.getID())) { - researches.add(research); - } - } + protected PlayerProfile(@Nonnull OfflinePlayer p, PlayerData data) { + this.ownerId = p.getUniqueId(); + this.name = p.getName(); + this.data = data; - for (String key : waypointsFile.getKeys()) { - try { - if (waypointsFile.contains(key + ".world") && Bukkit.getWorld(waypointsFile.getString(key + ".world")) != null) { - String waypointName = waypointsFile.getString(key + ".name"); - Location loc = waypointsFile.getLocation(key); - waypoints.add(new Waypoint(this, key, loc, waypointName)); - } - } catch (Exception x) { - Slimefun.logger().log(Level.WARNING, x, () -> "Could not load Waypoint \"" + key + "\" for Player \"" + name + '"'); - } - } + configFile = new Config("data-storage/Slimefun/Players/" + ownerId.toString() + ".yml"); } /** @@ -131,7 +102,7 @@ private void loadProfileData() { * @return The {@link UUID} of our {@link PlayerProfile} */ public @Nonnull UUID getUUID() { - return uuid; + return ownerId; } /** @@ -157,12 +128,7 @@ public boolean isDirty() { * This method will save the Player's Researches and Backpacks to the hard drive */ public void save() { - for (PlayerBackpack backpack : backpacks.values()) { - backpack.save(); - } - - waypointsFile.save(); - configFile.save(); + Slimefun.getPlayerStorage().savePlayerData(this.ownerId, this.data); dirty = false; } @@ -180,11 +146,9 @@ public void setResearched(@Nonnull Research research, boolean unlock) { dirty = true; if (unlock) { - configFile.setValue("researches." + research.getID(), true); - researches.add(research); + data.addResearch(research); } else { - configFile.setValue("researches." + research.getID(), null); - researches.remove(research); + data.removeResearch(research); } } @@ -202,7 +166,7 @@ public boolean hasUnlocked(@Nullable Research research) { return true; } - return !research.isEnabled() || researches.contains(research); + return !research.isEnabled() || data.getResearches().contains(research); } /** @@ -228,7 +192,7 @@ public boolean hasUnlockedEverything() { * @return A {@code Hashset} of all Researches this {@link Player} has unlocked */ public @Nonnull Set getResearches() { - return ImmutableSet.copyOf(researches); + return ImmutableSet.copyOf(this.data.getResearches()); } /** @@ -238,7 +202,7 @@ public boolean hasUnlockedEverything() { * @return A {@link List} containing every {@link Waypoint} */ public @Nonnull List getWaypoints() { - return ImmutableList.copyOf(waypoints); + return ImmutableList.copyOf(this.data.getWaypoints()); } /** @@ -249,21 +213,8 @@ public boolean hasUnlockedEverything() { * The {@link Waypoint} to add */ public void addWaypoint(@Nonnull Waypoint waypoint) { - Validate.notNull(waypoint, "Cannot add a 'null' waypoint!"); - - for (Waypoint wp : waypoints) { - if (wp.getId().equals(waypoint.getId())) { - throw new IllegalArgumentException("A Waypoint with that id already exists for this Player"); - } - } - - if (waypoints.size() < 21) { - waypoints.add(waypoint); - - waypointsFile.setValue(waypoint.getId(), waypoint.getLocation()); - waypointsFile.setValue(waypoint.getId() + ".name", waypoint.getName()); - markDirty(); - } + this.data.addWaypoint(waypoint); + markDirty(); } /** @@ -274,12 +225,8 @@ public void addWaypoint(@Nonnull Waypoint waypoint) { * The {@link Waypoint} to remove */ public void removeWaypoint(@Nonnull Waypoint waypoint) { - Validate.notNull(waypoint, "Cannot remove a 'null' waypoint!"); - - if (waypoints.remove(waypoint)) { - waypointsFile.setValue(waypoint.getId(), null); - markDirty(); - } + this.data.removeWaypoint(waypoint); + markDirty(); } /** @@ -301,8 +248,10 @@ public final void markDirty() { IntStream stream = IntStream.iterate(0, i -> i + 1).filter(i -> !configFile.contains("backpacks." + i + ".size")); int id = stream.findFirst().getAsInt(); - PlayerBackpack backpack = new PlayerBackpack(this, id, size); - backpacks.put(id, backpack); + PlayerBackpack backpack = PlayerBackpack.newBackpack(this.ownerId, id, size); + this.data.addBackpack(backpack); + + markDirty(); return backpack; } @@ -312,48 +261,62 @@ public final void markDirty() { throw new IllegalArgumentException("Backpacks cannot have negative ids!"); } - PlayerBackpack backpack = backpacks.get(id); + PlayerBackpack backpack = data.getBackpack(id); if (backpack != null) { - return Optional.of(backpack); - } else if (configFile.contains("backpacks." + id + ".size")) { - backpack = new PlayerBackpack(this, id); - backpacks.put(id, backpack); + markDirty(); return Optional.of(backpack); } return Optional.empty(); } - // returns the amount of researches with at least 1 enabled item - private int nonEmptyResearches() { - return (int) Slimefun.getRegistry().getResearches() - .stream() - .filter(research -> research.getAffectedItems().stream().anyMatch(item -> item.getState() == ItemState.ENABLED)) - .count(); + private int countNonEmptyResearches(@Nonnull Collection researches) { + int count = 0; + for (Research research : researches) { + if (research.hasEnabledItems()) { + count++; + } + } + return count; } + /** + * This method gets the research title, as defined in {@code config.yml}, + * of this {@link PlayerProfile} based on the fraction + * of unlocked {@link Research}es of this player. + * + * @return The research title of this {@link PlayerProfile} + */ public @Nonnull String getTitle() { List titles = Slimefun.getRegistry().getResearchRanks(); - float fraction = (float) researches.size() / nonEmptyResearches(); + int allResearches = countNonEmptyResearches(Slimefun.getRegistry().getResearches()); + float fraction = (float) countNonEmptyResearches(getResearches()) / allResearches; int index = (int) (fraction * (titles.size() - 1)); return titles.get(index); } + /** + * This sends the statistics for the specified {@link CommandSender} + * to the {@link CommandSender}. This includes research title, research progress + * and total xp spent. + * + * @param sender The {@link CommandSender} for which to get the statistics and send them to. + */ public void sendStats(@Nonnull CommandSender sender) { - Set unlockedResearches = getResearches(); - int levels = unlockedResearches.stream().mapToInt(Research::getCost).sum(); - int allResearches = nonEmptyResearches(); + int unlockedResearches = countNonEmptyResearches(getResearches()); + int levels = getResearches().stream().mapToInt(Research::getCost).sum(); + int allResearches = countNonEmptyResearches(Slimefun.getRegistry().getResearches()); - float progress = Math.round(((unlockedResearches.size() * 100.0F) / allResearches) * 100.0F) / 100.0F; + float progress = Math.round(((unlockedResearches * 100.0F) / allResearches) * 100.0F) / 100.0F; sender.sendMessage(""); sender.sendMessage(ChatColors.color("&7Statistics for Player: &b" + name)); sender.sendMessage(""); sender.sendMessage(ChatColors.color("&7Title: " + ChatColor.AQUA + getTitle())); - sender.sendMessage(ChatColors.color("&7Research Progress: " + NumberUtils.getColorFromPercentage(progress) + progress + " &r% " + ChatColor.YELLOW + '(' + unlockedResearches.size() + " / " + allResearches + ')')); + sender.sendMessage(ChatColors.color("&7Research Progress: " + NumberUtils.getColorFromPercentage(progress) + progress + " &r% " + ChatColor.YELLOW + '(' + unlockedResearches + " / " + allResearches + ')')); sender.sendMessage(ChatColors.color("&7Total XP Levels spent: " + ChatColor.AQUA + levels)); } @@ -403,7 +366,9 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer { - AsyncProfileLoadEvent event = new AsyncProfileLoadEvent(new PlayerProfile(p)); + PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId()); + + AsyncProfileLoadEvent event = new AsyncProfileLoadEvent(new PlayerProfile(p, data)); Bukkit.getPluginManager().callEvent(event); Slimefun.getRegistry().getPlayerProfiles().put(uuid, event.getProfile()); @@ -428,7 +393,9 @@ public static boolean request(@Nonnull OfflinePlayer p) { if (!Slimefun.getRegistry().getPlayerProfiles().containsKey(p.getUniqueId())) { // Should probably prevent multiple requests for the same profile in the future Bukkit.getScheduler().runTaskAsynchronously(Slimefun.instance(), () -> { - PlayerProfile pp = new PlayerProfile(p); + PlayerData data = Slimefun.getPlayerStorage().loadPlayerData(p.getUniqueId()); + + PlayerProfile pp = new PlayerProfile(p, data); Slimefun.getRegistry().getPlayerProfiles().put(p.getUniqueId(), pp); }); @@ -493,43 +460,40 @@ public boolean hasFullProtectionAgainst(@Nonnull ProtectionType type) { for (HashedArmorpiece armorpiece : armor) { Optional armorPiece = armorpiece.getItem(); - - if (!armorPiece.isPresent()) { - setId = null; - } else if (armorPiece.get() instanceof ProtectiveArmor protectedArmor) { - if (setId == null && protectedArmor.isFullSetRequired()) { - setId = protectedArmor.getArmorSetId(); - } - - for (ProtectionType protectionType : protectedArmor.getProtectionTypes()) { + if (armorPiece.isPresent() && armorPiece.get() instanceof ProtectiveArmor protectiveArmor) { + for (ProtectionType protectionType : protectiveArmor.getProtectionTypes()) { if (protectionType == type) { - if (setId == null) { + if (!protectiveArmor.isFullSetRequired()) { return true; - } else if (setId.equals(protectedArmor.getArmorSetId())) { + } else if (setId == null || setId.equals(protectiveArmor.getArmorSetId())) { armorCount++; + setId = protectiveArmor.getArmorSetId(); } } } - } } return armorCount == 4; } + public PlayerData getPlayerData() { + return this.data; + } + @Override public int hashCode() { - return uuid.hashCode(); + return ownerId.hashCode(); } @Override public boolean equals(Object obj) { - return obj instanceof PlayerProfile profile && uuid.equals(profile.uuid); + return obj instanceof PlayerProfile profile && ownerId.equals(profile.ownerId); } @Override public String toString() { - return "PlayerProfile {" + uuid + "}"; + return "PlayerProfile {" + ownerId + "}"; } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/researches/Research.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/researches/Research.java index 6a4ee6b528..c4c96e4e79 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/researches/Research.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/researches/Research.java @@ -22,6 +22,7 @@ import io.github.thebusybiscuit.slimefun4.api.events.PlayerPreResearchEvent; import io.github.thebusybiscuit.slimefun4.api.events.ResearchUnlockEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; +import io.github.thebusybiscuit.slimefun4.api.items.ItemState; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; import io.github.thebusybiscuit.slimefun4.core.guide.SlimefunGuideImplementation; @@ -197,6 +198,22 @@ public List getAffectedItems() { return items; } + /** + * This method checks whether there is at least one enabled {@link SlimefunItem} + * included in this {@link Research}. + * + * @return whether there is at least one enabled {@link SlimefunItem} + * included in this {@link Research}. + */ + public boolean hasEnabledItems() { + for (SlimefunItem item : items) { + if (item.getState() == ItemState.ENABLED) { + return true; + } + } + return false; + } + /** * Handle what to do when a {@link Player} clicks on an un-researched item in * a {@link SlimefunGuideImplementation}. diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/attributes/RandomMobDrop.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/attributes/RandomMobDrop.java index 9643760391..a0e51fe32d 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/attributes/RandomMobDrop.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/attributes/RandomMobDrop.java @@ -1,35 +1,35 @@ -package io.github.thebusybiscuit.slimefun4.core.attributes; - -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.event.entity.EntityDeathEvent; - -import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; -import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; -import io.github.thebusybiscuit.slimefun4.implementation.items.misc.BasicCircuitBoard; -import io.github.thebusybiscuit.slimefun4.implementation.listeners.entity.MobDropListener; - -/** - * This interface, when attached to a {@link SlimefunItem}, provides an easy method for adding - * a % chance to drop for an {@link SlimefunItem} on {@link EntityDeathEvent}, this chance is 0-100 - * and used in conjunction with the {@link RecipeType#MOB_DROP}. - * - * @author dNiym - * - * @see BasicCircuitBoard - * @see MobDropListener - * @see PiglinBarterDrop - * - */ -public interface RandomMobDrop extends ItemAttribute { - - /** - * Implement this method to make the object have a variable chance of being - * added to the dropList when {@link EntityType} specified in - * the {@link RecipeType#MOB_DROP} is killed by the {@link Player}. - * - * @return The integer chance (0-100%) {@link SlimefunItem} has to drop. - */ - int getMobDropChance(); - -} +package io.github.thebusybiscuit.slimefun4.core.attributes; + +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityDeathEvent; + +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; +import io.github.thebusybiscuit.slimefun4.implementation.items.misc.BasicCircuitBoard; +import io.github.thebusybiscuit.slimefun4.implementation.listeners.entity.MobDropListener; + +/** + * This interface, when attached to a {@link SlimefunItem}, provides an easy method for adding + * a % chance to drop for an {@link SlimefunItem} on {@link EntityDeathEvent}, this chance is 0-100 + * and used in conjunction with the {@link RecipeType#MOB_DROP}. + * + * @author dNiym + * + * @see BasicCircuitBoard + * @see MobDropListener + * @see PiglinBarterDrop + * + */ +public interface RandomMobDrop extends ItemAttribute { + + /** + * Implement this method to make the object have a variable chance of being + * added to the dropList when {@link EntityType} specified in + * the {@link RecipeType#MOB_DROP} is killed by the {@link Player}. + * + * @return The integer chance (0-100%) {@link SlimefunItem} has to drop. + */ + int getMobDropChance(); + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/Debug.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/Debug.java index 433ec5e1cd..50a05c309e 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/Debug.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/debug/Debug.java @@ -106,7 +106,7 @@ public static void log(@Nonnull String test, @Nonnull String msg, @Nonnull Objec while ((i = msg.indexOf('{', i)) != -1 && msg.charAt(i + 1) == '}') { // Substring up to the opening brace `{`, add the variable for this and add the rest of the message msg = msg.substring(0, i) + vars[idx] + msg.substring(i + 2); - idx++; + i += String.valueOf(vars[idx++]).length(); } return msg; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/handlers/RainbowTickHandler.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/handlers/RainbowTickHandler.java index 38900d8524..6fa656f6d0 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/handlers/RainbowTickHandler.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/handlers/RainbowTickHandler.java @@ -1,134 +1,134 @@ -package io.github.thebusybiscuit.slimefun4.core.handlers; - -import java.util.Arrays; -import java.util.List; - -import javax.annotation.Nonnull; - -import org.apache.commons.lang.Validate; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.GlassPane; - -import io.github.bakedlibs.dough.collections.LoopIterator; -import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; -import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; -import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; -import io.github.thebusybiscuit.slimefun4.implementation.items.blocks.RainbowBlock; -import io.github.thebusybiscuit.slimefun4.utils.ColoredMaterial; - -import me.mrCookieSlime.CSCoreLibPlugin.Configuration.Config; -import me.mrCookieSlime.Slimefun.Objects.handlers.BlockTicker; - -/** - * This is a {@link BlockTicker} that is exclusively used for Rainbow blocks. - * On every tick it cycles through the {@link LoopIterator} and chooses the next {@link Material} - * and sets itself to that. - * - * @author TheBusyBiscuit - * - * @see RainbowBlock - * - */ -public class RainbowTickHandler extends BlockTicker { - - private final LoopIterator iterator; - private final boolean glassPanes; - private Material material; - - public RainbowTickHandler(@Nonnull List materials) { - Validate.noNullElements(materials, "A RainbowTicker cannot have a Material that is null!"); - - if (materials.isEmpty()) { - throw new IllegalArgumentException("A RainbowTicker must have at least one Material associated with it!"); - } - - glassPanes = containsGlassPanes(materials); - iterator = new LoopIterator<>(materials); - material = iterator.next(); - } - - public RainbowTickHandler(@Nonnull Material... materials) { - this(Arrays.asList(materials)); - } - - public RainbowTickHandler(@Nonnull ColoredMaterial material) { - this(material.asList()); - } - - /** - * This method checks whether a given {@link Material} array contains any {@link Material} - * that would result in a {@link GlassPane} {@link BlockData}. - * This is done to save performance, so we don't have to validate {@link BlockData} at - * runtime. - * - * @param materials - * The {@link Material} Array to check - * - * @return Whether the array contained any {@link GlassPane} materials - */ - private boolean containsGlassPanes(@Nonnull List materials) { - if (Slimefun.getMinecraftVersion() == MinecraftVersion.UNIT_TEST) { - // BlockData is not available to us during Unit Tests :/ - return false; - } - - for (Material type : materials) { - /* - This BlockData is purely virtual and only created on startup, it should have - no impact on performance, in fact it should save performance as it preloads - the data but also saves heavy calls for other Materials - */ - if (type.createBlockData() instanceof GlassPane) { - return true; - } - } - - return false; - } - - @Override - public void tick(Block b, SlimefunItem item, Config data) { - if (b.getType().isAir()) { - /* - The block was broken, setting the Material now would result in a - duplication glitch - */ - return; - } - - if (glassPanes) { - BlockData blockData = b.getBlockData(); - - if (blockData instanceof GlassPane previousData) { - BlockData block = material.createBlockData(bd -> { - if (bd instanceof GlassPane nextData) { - nextData.setWaterlogged(previousData.isWaterlogged()); - - for (BlockFace face : previousData.getAllowedFaces()) { - nextData.setFace(face, previousData.hasFace(face)); - } - } - }); - - b.setBlockData(block, false); - return; - } - } - - b.setType(material, false); - } - - @Override - public void uniqueTick() { - material = iterator.next(); - } - - @Override - public boolean isSynchronized() { - return true; - } - -} +package io.github.thebusybiscuit.slimefun4.core.handlers; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.commons.lang.Validate; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.GlassPane; + +import io.github.bakedlibs.dough.collections.LoopIterator; +import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.implementation.items.blocks.RainbowBlock; +import io.github.thebusybiscuit.slimefun4.utils.ColoredMaterial; + +import me.mrCookieSlime.CSCoreLibPlugin.Configuration.Config; +import me.mrCookieSlime.Slimefun.Objects.handlers.BlockTicker; + +/** + * This is a {@link BlockTicker} that is exclusively used for Rainbow blocks. + * On every tick it cycles through the {@link LoopIterator} and chooses the next {@link Material} + * and sets itself to that. + * + * @author TheBusyBiscuit + * + * @see RainbowBlock + * + */ +public class RainbowTickHandler extends BlockTicker { + + private final LoopIterator iterator; + private final boolean glassPanes; + private Material material; + + public RainbowTickHandler(@Nonnull List materials) { + Validate.noNullElements(materials, "A RainbowTicker cannot have a Material that is null!"); + + if (materials.isEmpty()) { + throw new IllegalArgumentException("A RainbowTicker must have at least one Material associated with it!"); + } + + glassPanes = containsGlassPanes(materials); + iterator = new LoopIterator<>(materials); + material = iterator.next(); + } + + public RainbowTickHandler(@Nonnull Material... materials) { + this(Arrays.asList(materials)); + } + + public RainbowTickHandler(@Nonnull ColoredMaterial material) { + this(material.asList()); + } + + /** + * This method checks whether a given {@link Material} array contains any {@link Material} + * that would result in a {@link GlassPane} {@link BlockData}. + * This is done to save performance, so we don't have to validate {@link BlockData} at + * runtime. + * + * @param materials + * The {@link Material} Array to check + * + * @return Whether the array contained any {@link GlassPane} materials + */ + private boolean containsGlassPanes(@Nonnull List materials) { + if (Slimefun.getMinecraftVersion() == MinecraftVersion.UNIT_TEST) { + // BlockData is not available to us during Unit Tests :/ + return false; + } + + for (Material type : materials) { + /* + This BlockData is purely virtual and only created on startup, it should have + no impact on performance, in fact it should save performance as it preloads + the data but also saves heavy calls for other Materials + */ + if (type.createBlockData() instanceof GlassPane) { + return true; + } + } + + return false; + } + + @Override + public void tick(Block b, SlimefunItem item, Config data) { + if (b.getType().isAir()) { + /* + The block was broken, setting the Material now would result in a + duplication glitch + */ + return; + } + + if (glassPanes) { + BlockData blockData = b.getBlockData(); + + if (blockData instanceof GlassPane previousData) { + BlockData block = material.createBlockData(bd -> { + if (bd instanceof GlassPane nextData) { + nextData.setWaterlogged(previousData.isWaterlogged()); + + for (BlockFace face : previousData.getAllowedFaces()) { + nextData.setFace(face, previousData.hasFace(face)); + } + } + }); + + b.setBlockData(block, false); + return; + } + } + + b.setType(material, false); + } + + @Override + public void uniqueTick() { + material = iterator.next(); + } + + @Override + public boolean isSynchronized() { + return true; + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineOperation.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineOperation.java index 6ee741882a..3da6569b6c 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineOperation.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineOperation.java @@ -1,5 +1,6 @@ package io.github.thebusybiscuit.slimefun4.core.machines; +import io.github.bakedlibs.dough.blocks.BlockPosition; import io.github.thebusybiscuit.slimefun4.core.attributes.MachineProcessHolder; /** @@ -57,4 +58,10 @@ default boolean isFinished() { return getRemainingTicks() <= 0; } + /** + * This method is called when a {@link MachineOperation} is interrupted before finishing. + * Implement to specify behaviour that should happen in this case. + */ + default void onCancel(BlockPosition position) {} + } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineProcessor.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineProcessor.java index 33ccb4111d..9ec92b578a 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineProcessor.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/machines/MachineProcessor.java @@ -165,8 +165,6 @@ public boolean startOperation(@Nonnull BlockPosition pos, @Nonnull T operation) /** * This returns the current {@link MachineOperation} at that given {@link BlockPosition}. - * We don't need to validate our input here as that is already - * covered in our public methods. * * @param pos * The {@link BlockPosition} at which our machine is located. @@ -231,6 +229,8 @@ public boolean endOperation(@Nonnull BlockPosition pos) { if (operation.isFinished()) { Event event = new AsyncMachineOperationFinishEvent(pos, this, operation); Bukkit.getPluginManager().callEvent(event); + } else { + operation.onCancel(pos); } return true; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java index bea541fc28..b556789a91 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/UpdaterService.java @@ -5,10 +5,10 @@ import javax.annotation.Nonnull; +import io.github.bakedlibs.dough.updater.BlobBuildUpdater; import org.bukkit.plugin.Plugin; import io.github.bakedlibs.dough.config.Config; -import io.github.bakedlibs.dough.updater.GitHubBuildsUpdater; import io.github.bakedlibs.dough.updater.PluginUpdater; import io.github.bakedlibs.dough.versions.PrefixedVersion; import io.github.thebusybiscuit.slimefun4.api.SlimefunBranch; @@ -16,7 +16,7 @@ /** * This Class represents our {@link PluginUpdater} Service. - * If enabled, it will automatically connect to https://thebusybiscuit.github.io/builds/ + * If enabled, it will automatically connect to https://blob.build/ * to check for updates and to download them automatically. * * @author TheBusyBiscuit @@ -53,15 +53,15 @@ public class UpdaterService { */ public UpdaterService(@Nonnull Slimefun plugin, @Nonnull String version, @Nonnull File file) { this.plugin = plugin; - GitHubBuildsUpdater autoUpdater = null; + BlobBuildUpdater autoUpdater = null; if (version.contains("UNOFFICIAL")) { // This Server is using a modified build that is not a public release. branch = SlimefunBranch.UNOFFICIAL; - } else if (version.startsWith("DEV - ")) { + } else if (version.startsWith("Dev - ")) { // If we are using a development build, we want to switch to our custom try { - autoUpdater = new GitHubBuildsUpdater(plugin, file, "TheBusyBiscuit/Slimefun4/master"); + autoUpdater = new BlobBuildUpdater(plugin, file, "Slimefun4", "Dev"); } catch (Exception x) { plugin.getLogger().log(Level.SEVERE, "Failed to create AutoUpdater", x); } @@ -70,7 +70,7 @@ public UpdaterService(@Nonnull Slimefun plugin, @Nonnull String version, @Nonnul } else if (version.startsWith("RC - ")) { // If we are using a "stable" build, we want to switch to our custom try { - autoUpdater = new GitHubBuildsUpdater(plugin, file, "TheBusyBiscuit/Slimefun4/stable", "RC - "); + autoUpdater = new BlobBuildUpdater(plugin, file, "Slimefun4", "RC"); } catch (Exception x) { plugin.getLogger().log(Level.SEVERE, "Failed to create AutoUpdater", x); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java index 37a45e4315..8b065e3ff4 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/github/GitHubTask.java @@ -133,7 +133,7 @@ private int requestTexture(@Nonnull Contributor contributor, @Nonnull Map uuid = contributor.getUniqueId(); if (!uuid.isPresent()) { - CompletableFuture future = UUIDLookup.forUsername(Slimefun.instance(), contributor.getMinecraftName()); + CompletableFuture future = UUIDLookup.getUuidFromUsername(Slimefun.instance(), contributor.getMinecraftName()); // Fixes #3241 - Do not wait for more than 30 seconds uuid = Optional.ofNullable(future.get(30, TimeUnit.SECONDS)); @@ -142,7 +142,7 @@ private int requestTexture(@Nonnull Contributor contributor, @Nonnull Map future = PlayerSkin.fromPlayerUUID(Slimefun.instance(), uuid.get()); - Optional skin = Optional.of(future.get().toString()); + Optional skin = Optional.of(future.get().getProfile().getBase64Texture()); skins.put(contributor.getMinecraftName(), skin.orElse("")); return skin.orElse(null); } else { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java index f1fffb2b83..8667eed682 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java @@ -15,6 +15,9 @@ import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; +import io.github.thebusybiscuit.slimefun4.storage.Storage; +import io.github.thebusybiscuit.slimefun4.storage.backend.legacy.LegacyStorage; + import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; import org.bukkit.Server; @@ -82,6 +85,7 @@ import io.github.thebusybiscuit.slimefun4.implementation.listeners.HopperListener; import io.github.thebusybiscuit.slimefun4.implementation.listeners.ItemDropListener; import io.github.thebusybiscuit.slimefun4.implementation.listeners.ItemPickupListener; +import io.github.thebusybiscuit.slimefun4.implementation.listeners.JoinListener; import io.github.thebusybiscuit.slimefun4.implementation.listeners.MiddleClickListener; import io.github.thebusybiscuit.slimefun4.implementation.listeners.MiningAndroidListener; import io.github.thebusybiscuit.slimefun4.implementation.listeners.MultiBlockListener; @@ -196,6 +200,9 @@ public final class Slimefun extends JavaPlugin implements SlimefunAddon { private final Config items = new Config(this, "Items.yml"); private final Config researches = new Config(this, "Researches.yml"); + // Data storage + private Storage playerStorage; + // Listeners that need to be accessed elsewhere private final GrapplingHookListener grapplingHookListener = new GrapplingHookListener(); private final BackpackListener backpackListener = new BackpackListener(); @@ -257,6 +264,9 @@ private void onUnitTestStart() { registry.load(this, config); loadTags(); soundService.reload(false); + // TODO: What do we do if tests want to use another storage backend (e.g. testing new feature on legacy + sql)? + // Do we have a way to override this? + playerStorage = new LegacyStorage(); } /** @@ -311,6 +321,10 @@ private void onPluginStart() { networkManager = new NetworkManager(networkSize, config.getBoolean("networks.enable-visualizer"), config.getBoolean("networks.delete-excess-items")); + // Data storage + playerStorage = new LegacyStorage(); + logger.log(Level.INFO, "Using legacy storage for player data"); + // Setting up bStats new Thread(metricsService::start, "Slimefun Metrics").start(); @@ -366,9 +380,13 @@ private void onPluginStart() { // Armor Update Task if (config.getBoolean("options.enable-armor-effects")) { new SlimefunArmorTask().schedule(this, config.getInt("options.armor-update-interval") * 20L); - new RadiationTask().schedule(this, config.getInt("options.radiation-update-interval") * 20L); + if (config.getBoolean("options.enable-radiation")) { + new RadiationTask().schedule(this, config.getInt("options.radiation-update-interval") * 20L); + } new RainbowArmorTask().schedule(this, config.getInt("options.rainbow-armor-update-interval") * 20L); new SolarHelmetTask().schedule(this, config.getInt("options.armor-update-interval")); + } else if (config.getBoolean("options.enable-radiation")) { + logger.log(Level.WARNING, "Cannot enable radiation while armor effects are disabled."); } // Starting our tasks @@ -646,6 +664,7 @@ private void registerListeners() { new BeeWingsListener(this, (BeeWings) SlimefunItems.BEE_WINGS.getItem()); new PiglinListener(this); new SmithingTableListener(this); + new JoinListener(this); // Item-specific Listeners new CoolerListener(this, (Cooler) SlimefunItems.COOLER.getItem()); @@ -1062,4 +1081,7 @@ public static boolean isNewlyInstalled() { return instance.getServer().getScheduler().runTask(instance, runnable); } + public static @Nonnull Storage getPlayerStorage() { + return instance().playerStorage; + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/SlimefunItems.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/SlimefunItems.java index d65a67bd34..8695759316 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/SlimefunItems.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/SlimefunItems.java @@ -830,7 +830,7 @@ private SlimefunItems() {} public static final SlimefunItemStack FREEZER = new SlimefunItemStack("FREEZER", Material.LIGHT_BLUE_STAINED_GLASS, "&bFreezer", "", LoreBuilder.machine(MachineTier.ADVANCED, MachineType.MACHINE), LoreBuilder.speed(1), LoreBuilder.powerBuffer(256), LoreBuilder.powerPerSecond(18)); public static final SlimefunItemStack FREEZER_2 = new SlimefunItemStack("FREEZER_2", Material.LIGHT_BLUE_STAINED_GLASS, "&bFreezer &7(&eII&7)", "", LoreBuilder.machine(MachineTier.END_GAME, MachineType.MACHINE), LoreBuilder.speed(2), LoreBuilder.powerBuffer(256), LoreBuilder.powerPerSecond(30)); - public static final SlimefunItemStack FREEZER_3 = new SlimefunItemStack("FREEZER_3", Material.LIGHT_GRAY_STAINED_GLASS, "&bFreezer &7(&eIII&7)", "", LoreBuilder.machine(MachineTier.END_GAME, MachineType.MACHINE), LoreBuilder.speed(3), LoreBuilder.powerBuffer(256), LoreBuilder.powerPerSecond(42)); + public static final SlimefunItemStack FREEZER_3 = new SlimefunItemStack("FREEZER_3", Material.LIGHT_BLUE_STAINED_GLASS, "&bFreezer &7(&eIII&7)", "", LoreBuilder.machine(MachineTier.END_GAME, MachineType.MACHINE), LoreBuilder.speed(3), LoreBuilder.powerBuffer(256), LoreBuilder.powerPerSecond(42)); public static final SlimefunItemStack ELECTRIC_GOLD_PAN = new SlimefunItemStack("ELECTRIC_GOLD_PAN", Material.BROWN_TERRACOTTA, "&6Electric Gold Pan", "", LoreBuilder.machine(MachineTier.BASIC, MachineType.MACHINE), LoreBuilder.speed(1), LoreBuilder.powerPerSecond(2)); public static final SlimefunItemStack ELECTRIC_GOLD_PAN_2 = new SlimefunItemStack("ELECTRIC_GOLD_PAN_2", Material.BROWN_TERRACOTTA, "&6Electric Gold Pan &7(&eII&7)", "", LoreBuilder.machine(MachineTier.BASIC, MachineType.MACHINE), LoreBuilder.speed(3), LoreBuilder.powerPerSecond(4)); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/SurvivalSlimefunGuide.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/SurvivalSlimefunGuide.java index 21cee1d006..154150d979 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/SurvivalSlimefunGuide.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/guide/SurvivalSlimefunGuide.java @@ -339,7 +339,7 @@ public void openSearch(PlayerProfile profile, String input, boolean addToHistory } ChestMenu menu = new ChestMenu(Slimefun.getLocalization().getMessage(p, "guide.search.inventory").replace("%item%", ChatUtils.crop(ChatColor.WHITE, input))); - String searchTerm = input.toLowerCase(Locale.ROOT); + String searchTerm = ChatColor.stripColor(input.toLowerCase(Locale.ROOT)); if (addToHistory) { profile.getGuideHistory().add(searchTerm); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/altar/AncientPedestal.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/altar/AncientPedestal.java index 99a7b15585..bbf19d36d8 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/altar/AncientPedestal.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/altar/AncientPedestal.java @@ -167,7 +167,7 @@ public void placeItem(@Nonnull Player p, @Nonnull Block b) { ItemUtils.consumeItem(hand, false); } - Item entity = SlimefunUtils.spawnItem(b.getLocation().add(0.5, 1.2, 0.5), displayItem, ItemSpawnReason.ANCIENT_PEDESTAL_PLACE_ITEM); + Item entity = SlimefunUtils.spawnItem(b.getLocation().add(0.5, 1.2, 0.5), displayItem, ItemSpawnReason.ANCIENT_PEDESTAL_PLACE_ITEM, false, p); if (entity != null) { ArmorStand armorStand = getArmorStand(b, true); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/WoodcutterAndroid.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/WoodcutterAndroid.java index 8bc33808c1..0133cb3b25 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/WoodcutterAndroid.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/androids/WoodcutterAndroid.java @@ -162,6 +162,17 @@ private void replant(@Nonnull Block block) { } } + if (Slimefun.getMinecraftVersion().isAtLeast(MinecraftVersion.MINECRAFT_1_20)) { + switch (logType) { + case CHERRY_LOG, + STRIPPED_CHERRY_LOG -> { + saplingType = Material.CHERRY_SAPLING; + soilRequirement = SlimefunTag.DIRT_VARIANTS::isTagged; + } + default -> {} + } + } + if (saplingType != null && soilRequirement != null) { if (soilRequirement.test(block.getRelative(BlockFace.DOWN).getType())) { // Replant the block diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/machines/enchanting/BookBinder.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/machines/enchanting/BookBinder.java index c85e86070b..4ac521faa2 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/machines/enchanting/BookBinder.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/electric/machines/enchanting/BookBinder.java @@ -57,6 +57,10 @@ protected MachineRecipe findNextRecipe(BlockMenu menu) { // Just return if no enchantments exist. This shouldn't ever happen. :NotLikeThis: if (enchantments.size() > 0) { + if (hasIllegalEnchants(storedItemEnchantments) || hasIllegalEnchants(storedTargetEnchantments)) { + return null; + } + ItemStack book = new ItemStack(Material.ENCHANTED_BOOK); EnchantmentStorageMeta enchantMeta = (EnchantmentStorageMeta) book.getItemMeta(); @@ -70,6 +74,11 @@ protected MachineRecipe findNextRecipe(BlockMenu menu) { return null; } + // If the output is the same as one of the inputs: don't consume items + if (enchantMeta.getStoredEnchants().equals(storedItemEnchantments) || enchantMeta.getStoredEnchants().equals(storedTargetEnchantments)) { + return null; + } + book.setItemMeta(enchantMeta); MachineRecipe recipe = new MachineRecipe(25 * (enchantments.size() / this.getSpeed()), new ItemStack[] { target, item }, new ItemStack[] { book }); @@ -97,6 +106,19 @@ private boolean isCompatible(@Nullable ItemStack item) { return item != null && item.getType() == Material.ENCHANTED_BOOK; } + private boolean hasIllegalEnchants(@Nullable Map enchantments) { + if (enchantments == null) { + return false; + } + + for (Map.Entry entry : enchantments.entrySet()) { + if (bypassVanillaMaxLevel.getValue() && entry.getValue() > customMaxLevel.getValue() || !bypassVanillaMaxLevel.getValue() && entry.getValue() > entry.getKey().getMaxLevel()) { + return true; + } + } + return false; + } + @Override public ItemStack getProgressBar() { return new ItemStack(Material.IRON_CHESTPLATE); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/geo/GEOMiner.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/geo/GEOMiner.java index e9b5d104ca..cc09eb0ac6 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/geo/GEOMiner.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/geo/GEOMiner.java @@ -34,7 +34,7 @@ import io.github.thebusybiscuit.slimefun4.core.networks.energy.EnergyNetComponentType; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.handlers.SimpleBlockBreakHandler; -import io.github.thebusybiscuit.slimefun4.implementation.operations.MiningOperation; +import io.github.thebusybiscuit.slimefun4.implementation.operations.GEOMiningOperation; import io.github.thebusybiscuit.slimefun4.utils.ChestMenuUtils; import me.mrCookieSlime.CSCoreLibPlugin.Configuration.Config; @@ -53,7 +53,7 @@ * * @see GEOResource */ -public class GEOMiner extends SlimefunItem implements RecipeDisplayItem, EnergyNetComponent, InventoryBlock, HologramOwner, MachineProcessHolder { +public class GEOMiner extends SlimefunItem implements RecipeDisplayItem, EnergyNetComponent, InventoryBlock, HologramOwner, MachineProcessHolder { private static final int[] BORDER = { 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 26, 27, 35, 36, 44, 45, 53 }; private static final int[] OUTPUT_BORDER = { 19, 20, 21, 22, 23, 24, 25, 28, 34, 37, 43, 46, 47, 48, 49, 50, 51, 52 }; @@ -61,7 +61,7 @@ public class GEOMiner extends SlimefunItem implements RecipeDisplayItem, EnergyN private static final int PROCESSING_TIME = 14; - private final MachineProcessor processor = new MachineProcessor<>(this); + private final MachineProcessor processor = new MachineProcessor<>(this); private int energyConsumedPerTick = -1; private int energyCapacity = -1; @@ -77,7 +77,7 @@ public GEOMiner(ItemGroup itemGroup, SlimefunItemStack item, RecipeType recipeTy } @Override - public MachineProcessor getMachineProcessor() { + public @Nonnull MachineProcessor getMachineProcessor() { return processor; } @@ -298,7 +298,7 @@ public boolean isSynchronized() { protected void tick(@Nonnull Block b) { BlockMenu inv = BlockStorage.getInventory(b); - MiningOperation operation = processor.getOperation(b); + GEOMiningOperation operation = processor.getOperation(b); if (operation != null) { if (!operation.isFinished()) { @@ -339,7 +339,7 @@ private void start(@Nonnull Block b, @Nonnull BlockMenu inv) { return; } - processor.startOperation(b, new MiningOperation(resource.getItem().clone(), PROCESSING_TIME)); + processor.startOperation(b, new GEOMiningOperation(resource, PROCESSING_TIME)); Slimefun.getGPSNetwork().getResourceManager().setSupplies(resource, b.getWorld(), b.getX() >> 4, b.getZ() >> 4, supplies - 1); updateHologram(b, "&7Mining: &r" + resource.getName()); return; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/gps/GPSMarkerTool.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/gps/GPSMarkerTool.java index d677eea5b5..65c430237a 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/gps/GPSMarkerTool.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/gps/GPSMarkerTool.java @@ -36,7 +36,9 @@ public ItemUseHandler getItemHandler() { if (e.getClickedBlock().isPresent()) { Block b = e.getClickedBlock().get().getRelative(e.getClickedFace()); - Slimefun.getGPSNetwork().createWaypoint(e.getPlayer(), b.getLocation()); + Location l = b.getLocation(); + l.setYaw(e.getPlayer().getLocation().getYaw()); + Slimefun.getGPSNetwork().createWaypoint(e.getPlayer(), l); } }; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/magical/talismans/Talisman.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/magical/talismans/Talisman.java index 7ecc6f6fab..9f97f19892 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/magical/talismans/Talisman.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/magical/talismans/Talisman.java @@ -10,6 +10,7 @@ import javax.annotation.ParametersAreNonnullByDefault; import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; @@ -27,6 +28,7 @@ import io.github.bakedlibs.dough.items.CustomItemStack; import io.github.bakedlibs.dough.items.ItemUtils; +import io.github.thebusybiscuit.slimefun4.api.events.TalismanActivateEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; @@ -186,11 +188,15 @@ public static boolean trigger(Event e, SlimefunItem item, boolean sendMessage) { return false; } } else { - ItemStack enderTalisman = talisman.getEnderVariant(); + SlimefunItemStack enderTalismanItem = talisman.getEnderVariant(); + if (enderTalismanItem == null) { + return false; + } - if (SlimefunUtils.containsSimilarItem(p.getEnderChest(), enderTalisman, true)) { + EnderTalisman enderTalisman = enderTalismanItem.getItem(EnderTalisman.class); + if (enderTalisman != null && SlimefunUtils.containsSimilarItem(p.getEnderChest(), enderTalismanItem, true)) { if (talisman.canUse(p, true)) { - activateTalisman(e, p, p.getEnderChest(), talisman, enderTalisman, sendMessage); + activateTalisman(e, p, p.getEnderChest(), enderTalisman, enderTalismanItem, sendMessage); return true; } else { return false; @@ -203,12 +209,19 @@ public static boolean trigger(Event e, SlimefunItem item, boolean sendMessage) { @ParametersAreNonnullByDefault private static void activateTalisman(Event e, Player p, Inventory inv, Talisman talisman, ItemStack talismanItem, boolean sendMessage) { - consumeItem(inv, talisman, talismanItem); - applyTalismanEffects(p, talisman); - cancelEvent(e, talisman); + TalismanActivateEvent talismanEvent = new TalismanActivateEvent(p, talisman, talismanItem); + Bukkit.getPluginManager().callEvent(talismanEvent); + if (!talismanEvent.isCancelled()) { + if (!talismanEvent.preventsConsumption()) { + consumeItem(inv, talisman, talismanItem); + } - if (sendMessage) { - talisman.sendMessage(p); + applyTalismanEffects(p, talisman); + cancelEvent(e, talisman); + + if (sendMessage) { + talisman.sendMessage(p); + } } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AbstractSmeltery.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AbstractSmeltery.java index e11e76b3c8..7f2f27f0cb 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AbstractSmeltery.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AbstractSmeltery.java @@ -4,6 +4,7 @@ import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -14,6 +15,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.inventory.InvUtils; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; @@ -48,12 +50,14 @@ public void onInteract(Player p, Block b) { for (int i = 0; i < inputs.size(); i++) { if (canCraft(inv, inputs, i)) { ItemStack output = RecipeType.getRecipeOutputList(this, inputs.get(i)).clone(); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, inputs.get(i), output); - if (SlimefunUtils.canPlayerUseItem(p, output, true)) { + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled() && SlimefunUtils.canPlayerUseItem(p, output, true)) { Inventory outputInv = findOutputInventory(output, possibleDispenser, inv); if (outputInv != null) { - craft(p, b, inv, inputs.get(i), output, outputInv); + craft(p, b, inv, inputs.get(i), event.getOutput(), outputInv); } else { Slimefun.getLocalization().sendMessage(p, "machines.full-inventory", true); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java index d8e7dae44b..4ee70ac722 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/ArmorForge.java @@ -4,6 +4,7 @@ import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -13,9 +14,9 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -import io.github.thebusybiscuit.slimefun4.core.services.sounds.SoundEffect; import io.github.bakedlibs.dough.items.CustomItemStack; import io.github.bakedlibs.dough.items.ItemUtils; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; @@ -43,9 +44,11 @@ public void onInteract(Player p, Block b) { for (ItemStack[] input : inputs) { if (isCraftable(inv, input)) { ItemStack output = RecipeType.getRecipeOutputList(this, input).clone(); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); - if (SlimefunUtils.canPlayerUseItem(p, output, true)) { - craft(p, output, inv, possibleDispenser); + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled() && SlimefunUtils.canPlayerUseItem(p, output, true)) { + craft(p, event.getOutput(), inv, possibleDispenser); } return; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AutomatedPanningMachine.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AutomatedPanningMachine.java index c5e407404d..ca2d0c7124 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AutomatedPanningMachine.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/AutomatedPanningMachine.java @@ -7,6 +7,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.GameMode; import org.bukkit.Material; @@ -18,6 +19,7 @@ import io.github.bakedlibs.dough.items.ItemUtils; import io.github.bakedlibs.dough.scheduling.TaskQueue; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.core.multiblocks.MultiBlockMachine; @@ -74,6 +76,14 @@ public void onInteract(Player p, Block b) { return; } + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } + + ItemStack finalOutput = event.getOutput(); if (p.getGameMode() != GameMode.CREATIVE) { ItemUtils.consumeItem(input, false); } @@ -82,13 +92,13 @@ public void onInteract(Player p, Block b) { queue.thenRepeatEvery(20, 5, () -> b.getWorld().playEffect(b.getRelative(BlockFace.DOWN).getLocation(), Effect.STEP_SOUND, material)); queue.thenRun(20, () -> { - if (output.getType() != Material.AIR) { + if (finalOutput.getType() != Material.AIR) { Optional outputChest = OutputChest.findOutputChestFor(b.getRelative(BlockFace.DOWN), output); if (outputChest.isPresent()) { - outputChest.get().addItem(output.clone()); + outputChest.get().addItem(finalOutput.clone()); } else { - b.getWorld().dropItemNaturally(b.getLocation(), output.clone()); + b.getWorld().dropItemNaturally(b.getLocation(), finalOutput.clone()); } SoundEffect.AUTOMATED_PANNING_MACHINE_SUCCESS_SOUND.playAt(b); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Compressor.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Compressor.java index c799ae879c..f0b48882e6 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Compressor.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Compressor.java @@ -6,6 +6,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -16,6 +17,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.CustomItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; @@ -66,13 +68,19 @@ public void onInteract(Player p, Block b) { if (recipeInput != null && SlimefunUtils.isItemSimilar(item, recipeInput, true)) { ItemStack output = RecipeType.getRecipeOutput(this, recipeInput); Inventory outputInv = findOutputInventory(output, dispBlock, inv); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, item, output); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } if (outputInv != null) { ItemStack removing = item.clone(); removing.setAmount(recipeInput.getAmount()); inv.removeItem(removing); - craft(p, output, dispBlock, inv); + craft(p, event.getOutput(), dispBlock, inv); } else { Slimefun.getLocalization().sendMessage(p, "machines.full-inventory", true); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/EnhancedCraftingTable.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/EnhancedCraftingTable.java index 67b1ebbf58..4486197b62 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/EnhancedCraftingTable.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/EnhancedCraftingTable.java @@ -4,6 +4,7 @@ import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -13,8 +14,8 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -import io.github.thebusybiscuit.slimefun4.core.services.sounds.SoundEffect; import io.github.bakedlibs.dough.items.ItemUtils; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; @@ -44,9 +45,11 @@ public void onInteract(Player p, Block b) { for (ItemStack[] input : inputs) { if (isCraftable(inv, input)) { ItemStack output = RecipeType.getRecipeOutputList(this, input).clone(); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); - if (SlimefunUtils.canPlayerUseItem(p, output, true)) { - craft(inv, possibleDispenser, p, b, output); + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled() && SlimefunUtils.canPlayerUseItem(p, output, true)) { + craft(inv, possibleDispenser, p, b, event.getOutput()); } return; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/GrindStone.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/GrindStone.java index 140aa83f8d..f149093042 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/GrindStone.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/GrindStone.java @@ -6,6 +6,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -16,6 +17,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.CustomItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; @@ -126,12 +128,18 @@ public void onInteract(Player p, Block b) { if (convert != null && SlimefunUtils.isItemSimilar(current, convert, true)) { ItemStack output = RecipeType.getRecipeOutput(this, convert); Inventory outputInv = findOutputInventory(output, possibleDispenser, inv); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, current, output); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } if (outputInv != null) { ItemStack removing = current.clone(); removing.setAmount(1); inv.removeItem(removing); - outputInv.addItem(output); + outputInv.addItem(event.getOutput()); SoundEffect.GRIND_STONE_INTERACT_SOUND.playAt(b); } else { Slimefun.getLocalization().sendMessage(p, "machines.full-inventory", true); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Juicer.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Juicer.java index 40978b05ca..a81c731434 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Juicer.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/Juicer.java @@ -6,6 +6,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.Material; import org.bukkit.block.Block; @@ -17,6 +18,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.CustomItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; @@ -62,12 +64,18 @@ public void onInteract(Player p, Block b) { if (convert != null && SlimefunUtils.isItemSimilar(current, convert, true)) { ItemStack adding = RecipeType.getRecipeOutput(this, convert); Inventory outputInv = findOutputInventory(adding, possibleDispenser, inv); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, current, adding); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } if (outputInv != null) { ItemStack removing = current.clone(); removing.setAmount(1); inv.removeItem(removing); - outputInv.addItem(adding); + outputInv.addItem(event.getOutput()); SoundEffect.JUICER_USE_SOUND.playAt(b); // Not changed since this is supposed to be a natural sound. diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/MagicWorkbench.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/MagicWorkbench.java index 029dd66a07..25ce8cdcc8 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/MagicWorkbench.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/MagicWorkbench.java @@ -4,6 +4,7 @@ import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.Material; import org.bukkit.block.Block; @@ -15,6 +16,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.CustomItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; @@ -50,9 +52,11 @@ public void onInteract(Player p, Block b) { for (ItemStack[] input : inputs) { if (isCraftable(inv, input)) { ItemStack output = RecipeType.getRecipeOutputList(this, input).clone(); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); - if (SlimefunUtils.canPlayerUseItem(p, output, true)) { - craft(inv, possibleDispener, p, b, output); + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled() && SlimefunUtils.canPlayerUseItem(p, output, true)) { + craft(inv, possibleDispener, p, b, event.getOutput()); } return; @@ -130,9 +134,9 @@ private Block locateDispenser(Block b) { private boolean isCraftable(Inventory inv, ItemStack[] recipe) { for (int j = 0; j < inv.getContents().length; j++) { - if (!SlimefunUtils.isItemSimilar(inv.getContents()[j], recipe[j], true)) { + if (!SlimefunUtils.isItemSimilar(inv.getContents()[j], recipe[j], true, true, false)) { if (SlimefunItem.getByItem(recipe[j]) instanceof SlimefunBackpack) { - if (!SlimefunUtils.isItemSimilar(inv.getContents()[j], recipe[j], false)) { + if (!SlimefunUtils.isItemSimilar(inv.getContents()[j], recipe[j], false, true, false)) { return false; } } else { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreCrusher.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreCrusher.java index 2a0dad3d61..ea5141f099 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreCrusher.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreCrusher.java @@ -7,6 +7,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.Material; import org.bukkit.block.Block; @@ -18,6 +19,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.CustomItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.ItemSetting; @@ -190,13 +192,15 @@ public void onInteract(Player p, Block b) { if (convert != null && SlimefunUtils.isItemSimilar(current, convert, true)) { ItemStack adding = RecipeType.getRecipeOutput(this, convert); Inventory outputInv = findOutputInventory(adding, possibleDispenser, inv); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, current, adding); - if (SlimefunUtils.canPlayerUseItem(p, adding, true)) { + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled() && SlimefunUtils.canPlayerUseItem(p, adding, true)) { if (outputInv != null) { ItemStack removing = current.clone(); removing.setAmount(convert.getAmount()); inv.removeItem(removing); - outputInv.addItem(adding); + outputInv.addItem(event.getOutput()); p.getWorld().playEffect(b.getLocation(), Effect.STEP_SOUND, 1); } else { Slimefun.getLocalization().sendMessage(p, "machines.full-inventory", true); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreWasher.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreWasher.java index 80dc6c6cc3..25a3282369 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreWasher.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/OreWasher.java @@ -2,12 +2,12 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.Material; import org.bukkit.block.Block; @@ -18,6 +18,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.core.multiblocks.MultiBlockMachine; @@ -84,8 +85,8 @@ protected void registerDefaultRecipes(List recipes) { } @Override - public List getDisplayRecipes() { - return recipes.stream().map(items -> items[0]).collect(Collectors.toList()); + public @Nonnull List getDisplayRecipes() { + return recipes.stream().map(items -> items[0]).toList(); } @Override @@ -93,15 +94,14 @@ public void onInteract(Player p, Block b) { Block dispBlock = b.getRelative(BlockFace.UP); BlockState state = PaperLib.getBlockState(dispBlock, false).getState(); - if (state instanceof Dispenser) { - Dispenser disp = (Dispenser) state; + if (state instanceof Dispenser disp) { Inventory inv = disp.getInventory(); for (ItemStack input : inv.getContents()) { if (input != null) { if (SlimefunUtils.isItemSimilar(input, SlimefunItems.SIFTED_ORE, true)) { ItemStack output = getRandomDust(); - Inventory outputInv = null; + Inventory outputInv; if (!legacyMode) { /* @@ -119,7 +119,12 @@ public void onInteract(Player p, Block b) { outputInv = findOutputInventory(output, dispBlock, inv); } - removeItem(p, b, inv, outputInv, input, output, 1); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); + if (event.isCancelled()) { + return; + } + + removeItem(p, b, inv, outputInv, input, event.getOutput(), 1); if (outputInv != null) { outputInv.addItem(SlimefunItems.STONE_CHUNK); @@ -130,14 +135,25 @@ public void onInteract(Player p, Block b) { ItemStack output = SlimefunItems.SALT; Inventory outputInv = findOutputInventory(output, dispBlock, inv); - removeItem(p, b, inv, outputInv, input, output, 2); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); + if (event.isCancelled()) { + return; + } + + removeItem(p, b, inv, outputInv, input, event.getOutput(), 2); return; } else if (SlimefunUtils.isItemSimilar(input, SlimefunItems.PULVERIZED_ORE, true)) { ItemStack output = SlimefunItems.PURE_ORE_CLUSTER; Inventory outputInv = findOutputInventory(output, dispBlock, inv); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, input, output); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } - removeItem(p, b, inv, outputInv, input, output, 1); + removeItem(p, b, inv, outputInv, input, event.getOutput(), 1); return; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/PressureChamber.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/PressureChamber.java index 4080aa31a0..c8b75de5a3 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/PressureChamber.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/PressureChamber.java @@ -6,6 +6,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.Material; import org.bukkit.block.Block; @@ -17,6 +18,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.CustomItemStack; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; @@ -51,13 +53,19 @@ public void onInteract(Player p, Block b) { if (convert != null && SlimefunUtils.isItemSimilar(current, convert, true)) { ItemStack output = RecipeType.getRecipeOutput(this, convert); Inventory outputInv = findOutputInventory(output, possibleDispenser, inv); + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, current, output); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } if (outputInv != null) { ItemStack removing = current.clone(); removing.setAmount(convert.getAmount()); inv.removeItem(removing); - craft(p, b, output, inv, possibleDispenser); + craft(p, b, event.getOutput(), inv, possibleDispenser); } else { Slimefun.getLocalization().sendMessage(p, "machines.full-inventory", true); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/TableSaw.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/TableSaw.java index 0e920cbe04..40bb1b2fdd 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/TableSaw.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/multiblocks/TableSaw.java @@ -8,6 +8,7 @@ import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.GameMode; import org.bukkit.Material; @@ -19,6 +20,7 @@ import org.bukkit.inventory.ItemStack; import io.github.bakedlibs.dough.items.ItemUtils; +import io.github.thebusybiscuit.slimefun4.api.events.MultiBlockCraftEvent; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.core.multiblocks.MultiBlockMachine; @@ -107,11 +109,18 @@ public void onInteract(@Nonnull Player p, @Nonnull Block b) { return; } + MultiBlockCraftEvent event = new MultiBlockCraftEvent(p, this, item, output); + + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } + if (p.getGameMode() != GameMode.CREATIVE) { ItemUtils.consumeItem(item, true); } - outputItems(b, output); + outputItems(b, event.getOutput()); b.getWorld().playEffect(b.getLocation(), Effect.STEP_SOUND, item.getType()); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/seasonal/ChristmasPresent.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/seasonal/ChristmasPresent.java index f4cc010628..37ec6cc244 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/seasonal/ChristmasPresent.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/seasonal/ChristmasPresent.java @@ -55,7 +55,7 @@ public ChristmasPresent(ItemGroup itemGroup, SlimefunItemStack item, RecipeType Block b = block.getRelative(e.getClickedFace()); ItemStack gift = gifts[ThreadLocalRandom.current().nextInt(gifts.length)].clone(); - SlimefunUtils.spawnItem(b.getLocation(), gift, ItemSpawnReason.CHRISTMAS_PRESENT_OPENED, true); + SlimefunUtils.spawnItem(b.getLocation(), gift, ItemSpawnReason.CHRISTMAS_PRESENT_OPENED, true, e.getPlayer()); }); }; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/ExplosiveTool.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/ExplosiveTool.java index db990c64c9..6fe1174e4f 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/ExplosiveTool.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/ExplosiveTool.java @@ -6,6 +6,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import dev.lone.itemsadder.api.CustomBlock; import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.Material; @@ -35,9 +36,9 @@ /** * This {@link SlimefunItem} is a super class for items like the {@link ExplosivePickaxe} or {@link ExplosiveShovel}. - * + * * @author TheBusyBiscuit - * + * * @see ExplosivePickaxe * @see ExplosiveShovel * @@ -83,6 +84,10 @@ private void breakBlocks(BlockBreakEvent e, Player p, ItemStack item, Block b, L if (!blockExplodeEvent.isCancelled()) { for (Block block : blockExplodeEvent.blockList()) { if (canBreak(p, block)) { + if (Slimefun.getIntegrations().isCustomBlock(block)) { + drops.addAll(CustomBlock.byAlreadyPlaced(block).getLoot()); + CustomBlock.remove(block.getLocation()); + } blocksToDestroy.add(block); } } @@ -90,6 +95,10 @@ private void breakBlocks(BlockBreakEvent e, Player p, ItemStack item, Block b, L } else { for (Block block : blocks) { if (canBreak(p, block)) { + if (Slimefun.getIntegrations().isCustomBlock(block)) { + drops.addAll(CustomBlock.byAlreadyPlaced(block).getLoot()); + CustomBlock.remove(block.getLocation()); + } blocksToDestroy.add(block); } } @@ -137,8 +146,6 @@ protected boolean canBreak(@Nonnull Player p, @Nonnull Block b) { return false; } else if (!b.getWorld().getWorldBorder().isInside(b.getLocation())) { return false; - } else if (Slimefun.getIntegrations().isCustomBlock(b)) { - return false; } else { return Slimefun.getProtectionManager().hasPermission(p, b.getLocation(), Interaction.BREAK_BLOCK); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/GoldPan.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/GoldPan.java index 01478526cb..08f69ec27a 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/GoldPan.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/GoldPan.java @@ -154,7 +154,7 @@ public void updateRandomizer() { // Make sure that the randomly selected item is not air if (output.getType() != Material.AIR) { - SlimefunUtils.spawnItem(b.getLocation(), output.clone(), ItemSpawnReason.GOLD_PAN_USE, true); + SlimefunUtils.spawnItem(b.getLocation(), output.clone(), ItemSpawnReason.GOLD_PAN_USE, true, e.getPlayer()); } } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/PickaxeOfContainment.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/PickaxeOfContainment.java index c8064299a0..86499f2bee 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/PickaxeOfContainment.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/tools/PickaxeOfContainment.java @@ -50,7 +50,7 @@ public PickaxeOfContainment(ItemGroup itemGroup, SlimefunItemStack item, RecipeT if (b.getType() == Material.SPAWNER) { ItemStack spawner = breakSpawner(b); - SlimefunUtils.spawnItem(b.getLocation(), spawner, ItemSpawnReason.BROKEN_SPAWNER_DROP, true); + SlimefunUtils.spawnItem(b.getLocation(), spawner, ItemSpawnReason.BROKEN_SPAWNER_DROP, true, e.getPlayer()); e.setExpToDrop(0); e.setDropItems(false); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/weapons/SwordOfBeheading.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/weapons/SwordOfBeheading.java index 42842880cc..ce40980ded 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/weapons/SwordOfBeheading.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/items/weapons/SwordOfBeheading.java @@ -8,6 +8,7 @@ import org.bukkit.Material; import org.bukkit.entity.Creeper; import org.bukkit.entity.Monster; +import org.bukkit.entity.Piglin; import org.bukkit.entity.Player; import org.bukkit.entity.Skeleton; import org.bukkit.entity.WitherSkeleton; @@ -16,17 +17,20 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; +import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; import io.github.thebusybiscuit.slimefun4.api.items.ItemSetting; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.items.settings.IntRangeSetting; import io.github.thebusybiscuit.slimefun4.api.recipes.RecipeType; import io.github.thebusybiscuit.slimefun4.core.handlers.EntityKillHandler; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.items.SimpleSlimefunItem; /** * The {@link SwordOfBeheading} is a special kind of sword which allows you to obtain - * {@link Zombie}, {@link Skeleton} and {@link Creeper} skulls when killing the respective {@link Monster}. + * {@link Zombie}, {@link Skeleton}, {@link Creeper} and {@link Piglin} skulls when killing the respective + * {@link Monster}. * Additionally, you can also obtain the head of a {@link Player} by killing them too. * This sword also allows you to have a higher chance of getting the skull of a {@link WitherSkeleton} too. * @@ -41,13 +45,14 @@ public class SwordOfBeheading extends SimpleSlimefunItem { private final ItemSetting chanceSkeleton = new IntRangeSetting(this, "chance.SKELETON", 0, 40, 100); private final ItemSetting chanceWitherSkeleton = new IntRangeSetting(this, "chance.WITHER_SKELETON", 0, 25, 100); private final ItemSetting chanceCreeper = new IntRangeSetting(this, "chance.CREEPER", 0, 40, 100); + private final ItemSetting chancePiglin = new IntRangeSetting(this, "chance.PIGLIN", 0, 40, 100); private final ItemSetting chancePlayer = new IntRangeSetting(this, "chance.PLAYER", 0, 70, 100); @ParametersAreNonnullByDefault public SwordOfBeheading(ItemGroup itemGroup, SlimefunItemStack item, RecipeType recipeType, ItemStack[] recipe) { super(itemGroup, item, recipeType, recipe); - addItemSetting(chanceZombie, chanceSkeleton, chanceWitherSkeleton, chanceCreeper, chancePlayer); + addItemSetting(chanceZombie, chanceSkeleton, chanceWitherSkeleton, chanceCreeper, chancePiglin, chancePlayer); } @Override @@ -56,27 +61,33 @@ public EntityKillHandler getItemHandler() { Random random = ThreadLocalRandom.current(); switch (e.getEntityType()) { - case ZOMBIE: + case ZOMBIE -> { if (random.nextInt(100) < chanceZombie.getValue()) { e.getDrops().add(new ItemStack(Material.ZOMBIE_HEAD)); } - break; - case SKELETON: + } + case SKELETON -> { if (random.nextInt(100) < chanceSkeleton.getValue()) { e.getDrops().add(new ItemStack(Material.SKELETON_SKULL)); } - break; - case CREEPER: + } + case CREEPER -> { if (random.nextInt(100) < chanceCreeper.getValue()) { e.getDrops().add(new ItemStack(Material.CREEPER_HEAD)); } - break; - case WITHER_SKELETON: + } + case WITHER_SKELETON -> { if (random.nextInt(100) < chanceWitherSkeleton.getValue()) { e.getDrops().add(new ItemStack(Material.WITHER_SKELETON_SKULL)); } - break; - case PLAYER: + } + case PIGLIN -> { + if (Slimefun.getMinecraftVersion().isAtLeast(MinecraftVersion.MINECRAFT_1_20) && + random.nextInt(100) < chancePiglin.getValue()) { + e.getDrops().add(new ItemStack(Material.PIGLIN_HEAD)); + } + } + case PLAYER -> { if (random.nextInt(100) < chancePlayer.getValue()) { ItemStack skull = new ItemStack(Material.PLAYER_HEAD); @@ -86,9 +97,8 @@ public EntityKillHandler getItemHandler() { e.getDrops().add(skull); } - break; - default: - break; + } + default -> {} } }; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/BlockListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/BlockListener.java index a8dcfc7ccb..c214c33a2c 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/BlockListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/BlockListener.java @@ -11,9 +11,12 @@ import javax.annotation.ParametersAreNonnullByDefault; import org.bukkit.Bukkit; +import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -25,9 +28,11 @@ import org.bukkit.inventory.meta.ItemMeta; import io.github.bakedlibs.dough.protection.Interaction; +import io.github.thebusybiscuit.slimefun4.api.events.ExplosiveToolBreakBlocksEvent; import io.github.thebusybiscuit.slimefun4.api.events.SlimefunBlockBreakEvent; import io.github.thebusybiscuit.slimefun4.api.events.SlimefunBlockPlaceEvent; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.core.attributes.NotPlaceable; import io.github.thebusybiscuit.slimefun4.core.handlers.BlockBreakHandler; import io.github.thebusybiscuit.slimefun4.core.handlers.BlockPlaceHandler; @@ -52,6 +57,8 @@ */ public class BlockListener implements Listener { + private static final BlockFace[] CARDINAL_BLOCKFACES = new BlockFace[]{BlockFace.WEST, BlockFace.EAST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.DOWN, BlockFace.UP}; + public BlockListener(@Nonnull Slimefun plugin) { plugin.getServer().getPluginManager().registerEvents(this, plugin); } @@ -83,26 +90,40 @@ public void onBlockPlaceExisting(BlockPlaceEvent e) { public void onBlockPlace(BlockPlaceEvent e) { ItemStack item = e.getItemInHand(); SlimefunItem sfItem = SlimefunItem.getByItem(item); - + // TODO: Protection manager is null in testing environment. if (!Slimefun.instance().isUnitTest()) { - Slimefun.getProtectionManager().logAction(e.getPlayer(), e.getBlock(), Interaction.BREAK_BLOCK); + Slimefun.getProtectionManager().logAction(e.getPlayer(), e.getBlock(), Interaction.PLACE_BLOCK); } if (sfItem != null && !(sfItem instanceof NotPlaceable)) { - if (!sfItem.canUse(e.getPlayer(), true)) { + Player player = e.getPlayer(); + + if (!sfItem.canUse(player, true)) { e.setCancelled(true); } else { - SlimefunBlockPlaceEvent placeEvent = new SlimefunBlockPlaceEvent(e.getPlayer(), item, e.getBlock(), sfItem); + Block block = e.getBlockPlaced(); + + /* + * Resolves an issue when placing a block in a location currently in the deletion queue + * TODO This can be safely removed if/when the deletion no longer has a delay associated with it. + */ + if (Slimefun.getTickerTask().isDeletedSoon(block.getLocation())) { + Slimefun.getLocalization().sendMessage(player, "messages.await-deletion"); + e.setCancelled(true); + return; + } + + SlimefunBlockPlaceEvent placeEvent = new SlimefunBlockPlaceEvent(player, item, block, sfItem); Bukkit.getPluginManager().callEvent(placeEvent); if (placeEvent.isCancelled()) { e.setCancelled(true); } else { - if (Slimefun.getBlockDataService().isTileEntity(e.getBlock().getType())) { - Slimefun.getBlockDataService().setBlockData(e.getBlock(), sfItem.getId()); + if (Slimefun.getBlockDataService().isTileEntity(block.getType())) { + Slimefun.getBlockDataService().setBlockData(block, sfItem.getId()); } - BlockStorage.addBlockInfo(e.getBlock(), "id", sfItem.getId(), true); + BlockStorage.addBlockInfo(block, "id", sfItem.getId(), true); sfItem.callItemHandler(BlockPlaceHandler.class, handler -> handler.onPlayerPlace(e)); } } @@ -140,18 +161,31 @@ public void onBlockBreak(BlockBreakEvent e) { } } - if (!e.isCancelled()) { - checkForSensitiveBlockAbove(e, item); + List drops = new ArrayList<>(); + if (!item.getType().isAir()) { int fortune = getBonusDropsWithFortune(item, e.getBlock()); - List drops = new ArrayList<>(); + callToolHandler(e, item, fortune, drops); + } - if (!item.getType().isAir()) { - callToolHandler(e, item, fortune, drops); - } + if (!e.isCancelled()) { + // Checks for Slimefun sensitive blocks above, using Slimefun Tags + // TODO: merge this with the vanilla sensitive block check (when 1.18- is dropped) + checkForSensitiveBlockAbove(e.getPlayer(), e.getBlock(), item); callBlockHandler(e, item, drops, sfItem); + dropItems(e, drops); + + // Checks for vanilla sensitive blocks everywhere + checkForSensitiveBlocks(e.getBlock(), 0, e.isDropItems()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onExplosiveToolBlockBreak(ExplosiveToolBreakBlocksEvent e) { + for (Block block : e.getAdditionalBlocks()) { + checkForSensitiveBlockAbove(e.getPlayer(), block, e.getItemInHand()); } } @@ -207,7 +241,9 @@ private void dropItems(BlockBreakEvent e, List drops) { for (ItemStack drop : drops) { // Prevent null or air from being dropped if (drop != null && drop.getType() != Material.AIR) { - e.getBlock().getWorld().dropItemNaturally(e.getBlock().getLocation(), drop); + if (e.getPlayer().getGameMode() != GameMode.CREATIVE || Slimefun.getCfg().getBoolean("options.drop-block-creative")) { + e.getBlock().getWorld().dropItemNaturally(e.getBlock().getLocation(), drop); + } } } } @@ -219,14 +255,16 @@ private void dropItems(BlockBreakEvent e, List drops) { * Sensitive {@link Block Blocks} are pressure plates or saplings, which should be broken * when the block beneath is broken as well. * - * @param p + * @param player * The {@link Player} who broke this {@link Block} - * @param b + * @param block * The {@link Block} that was broken + * @param item + * The {@link ItemStack} that was used to break the {@link Block} */ @ParametersAreNonnullByDefault - private void checkForSensitiveBlockAbove(BlockBreakEvent e, ItemStack item) { - Block blockAbove = e.getBlock().getRelative(BlockFace.UP); + private void checkForSensitiveBlockAbove(Player player, Block block, ItemStack item) { + Block blockAbove = block.getRelative(BlockFace.UP); if (SlimefunTag.SENSITIVE_MATERIALS.isTagged(blockAbove.getType())) { SlimefunItem sfItem = BlockStorage.check(blockAbove); @@ -236,9 +274,8 @@ private void checkForSensitiveBlockAbove(BlockBreakEvent e, ItemStack item) { * We create a dummy here to pass onto the BlockBreakHandler. * This will set the correct block context. */ - BlockBreakEvent dummyEvent = new BlockBreakEvent(blockAbove, e.getPlayer()); - List drops = new ArrayList<>(); - drops.addAll(sfItem.getDrops(e.getPlayer())); + BlockBreakEvent dummyEvent = new BlockBreakEvent(blockAbove, player); + List drops = new ArrayList<>(sfItem.getDrops(player)); sfItem.callItemHandler(BlockBreakHandler.class, handler -> handler.onPlayerBreak(dummyEvent, item, drops)); blockAbove.setType(Material.AIR); @@ -257,6 +294,65 @@ private void checkForSensitiveBlockAbove(BlockBreakEvent e, ItemStack item) { } } + /** + * This method checks recursively for any sensitive blocks + * that are no longer supported due to this block breaking + * + * @param block + * The {@link Block} in question + * @param count + * The amount of times this has been recursively called + */ + // Disabled for now due to #4069 - Servers crashing due to this check + // There is additionally a second bug with `getMaxChainedNeighborUpdates` not existing in 1.17 + @ParametersAreNonnullByDefault + private void checkForSensitiveBlocks(Block block, Integer count, boolean isDropItems) { + /* + if (count >= Bukkit.getServer().getMaxChainedNeighborUpdates()) { + return; + } + + BlockState state = block.getState(); + // We set the block to air to make use of BlockData#isSupported. + block.setType(Material.AIR, false); + for (BlockFace face : CARDINAL_BLOCKFACES) { + if (!isSupported(block.getRelative(face).getBlockData(), block.getRelative(face))) { + Block relative = block.getRelative(face); + if (!isDropItems) { + for (ItemStack drop : relative.getDrops()) { + block.getWorld().dropItemNaturally(relative.getLocation(), drop); + } + } + checkForSensitiveBlocks(relative, ++count, isDropItems); + } + } + // Set the BlockData back: this makes it so containers and spawners drop correctly. This is a hacky fix. + block.setBlockData(state.getBlockData(), false); + state.update(true, false); + */ + } + + /** + * This method checks if the {@link BlockData} would be + * supported at the given {@link Block}. + * + * @param blockData + * The {@link BlockData} to check + * @param block + * The {@link Block} the {@link BlockData} would be at + * @return + * Whether the {@link BlockData} would be supported at the given {@link Block} + */ + @ParametersAreNonnullByDefault + private boolean isSupported(BlockData blockData, Block block) { + if (Slimefun.getMinecraftVersion().isAtLeast(MinecraftVersion.MINECRAFT_1_19)) { + return blockData.isSupported(block); + } else { + // TODO: Make 1.16-1.18 version. BlockData::isSupported is 1.19+. + return true; + } + } + private int getBonusDropsWithFortune(@Nullable ItemStack item, @Nonnull Block b) { int amount = 1; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ButcherAndroidListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ButcherAndroidListener.java index 1432edd1dd..9e63107680 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ButcherAndroidListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ButcherAndroidListener.java @@ -88,5 +88,9 @@ private void addExtraDrops(List drops, EntityType entityType) { if (entityType == EntityType.BLAZE) { drops.add(new ItemStack(Material.BLAZE_ROD, 1 + random.nextInt(1))); } + + if (entityType == EntityType.VINDICATOR) { + drops.add(new ItemStack(Material.EMERALD, 1 + random.nextInt(2))); + } } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ElytraImpactListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ElytraImpactListener.java index 344fe14471..02fc80c56c 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ElytraImpactListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/ElytraImpactListener.java @@ -1,9 +1,14 @@ package io.github.thebusybiscuit.slimefun4.implementation.listeners; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import javax.annotation.Nonnull; +import org.bukkit.Sound; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -18,20 +23,35 @@ import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.implementation.items.armor.ElytraCap; import io.github.thebusybiscuit.slimefun4.implementation.items.armor.SlimefunArmorPiece; +import org.bukkit.event.entity.EntityToggleGlideEvent; /** * The {@link Listener} for the {@link ElytraCap}. * * @author Seggan + * @author J3fftw1 * * @see ElytraCap */ public class ElytraImpactListener implements Listener { + private final Set gliding = new HashSet<>(); + public ElytraImpactListener(@Nonnull Slimefun plugin) { plugin.getServer().getPluginManager().registerEvents(this, plugin); } + @EventHandler + public void onGlideToggle(EntityToggleGlideEvent event) { + Entity entity = event.getEntity(); + if (entity instanceof Player player && player.isGliding()) { + UUID uuid = player.getUniqueId(); + gliding.add(uuid); + } + // We tick 1 tick later because the player is being toggled of at the same tick as it takes damage. + Slimefun.instance().getServer().getScheduler().runTaskLater(Slimefun.instance(), gliding::clear, 1); + } + @EventHandler public void onPlayerCrash(EntityDamageEvent e) { if (!(e.getEntity() instanceof Player p)) { @@ -39,7 +59,9 @@ public void onPlayerCrash(EntityDamageEvent e) { return; } - if (e.getCause() == DamageCause.FALL || e.getCause() == DamageCause.FLY_INTO_WALL && p.isGliding()) { + if ((e.getCause() == DamageCause.FALL || e.getCause() == DamageCause.FLY_INTO_WALL) + && (p.isGliding() || gliding.contains(p.getUniqueId())) + ) { Optional optional = PlayerProfile.find(p); if (optional.isEmpty()) { @@ -48,7 +70,7 @@ public void onPlayerCrash(EntityDamageEvent e) { } PlayerProfile profile = optional.get(); - Optional helmet = profile.getArmor()[0].getItem(); + Optional helmet = profile.getArmor()[3].getItem(); if (helmet.isPresent()) { SlimefunItem item = helmet.get(); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/JoinListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/JoinListener.java new file mode 100644 index 0000000000..28d15851bd --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/JoinListener.java @@ -0,0 +1,44 @@ +package io.github.thebusybiscuit.slimefun4.implementation.listeners; + +import javax.annotation.Nonnull; + +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.ItemStack; + +import io.github.thebusybiscuit.slimefun4.api.items.HashedArmorpiece; +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.implementation.items.armor.SlimefunArmorPiece; +import io.github.thebusybiscuit.slimefun4.implementation.tasks.armor.RadiationTask; + +/** + * This {@link Listener} caches the armor of the player on join. + * This is mainly for the {@link RadiationTask}. + * + * @author iTwins + */ +public class JoinListener implements Listener { + + public JoinListener(@Nonnull Slimefun plugin) { + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + @EventHandler + public void onJoin(@Nonnull PlayerJoinEvent e) { + PlayerProfile.get(e.getPlayer(), playerProfile -> { + final ItemStack[] armorContents = e.getPlayer().getInventory().getArmorContents(); + final HashedArmorpiece[] hashedArmorpieces = playerProfile.getArmor(); + for (int i = 0; i < 4; i++) { + final ItemStack armorPiece = armorContents[i]; + if (armorPiece != null && armorPiece.getType() != Material.AIR && SlimefunItem.getByItem(armorPiece) instanceof SlimefunArmorPiece sfArmorPiece) { + hashedArmorpieces[i].update(armorPiece, sfArmorPiece); + } + } + }); + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunGuideListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunGuideListener.java index 783dcf59f8..53897013a2 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunGuideListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/SlimefunGuideListener.java @@ -81,7 +81,7 @@ private void openGuide(Player p, PlayerRightClickEvent e, SlimefunGuideMode layo @ParametersAreNonnullByDefault private Result tryOpenGuide(Player p, PlayerRightClickEvent e, SlimefunGuideMode layout) { ItemStack item = e.getItem(); - if (SlimefunUtils.isItemSimilar(item, SlimefunGuide.getItem(layout), true, false)) { + if (SlimefunUtils.isItemSimilar(item, SlimefunGuide.getItem(layout), false, false)) { if (!Slimefun.getWorldSettingsService().isWorldEnabled(p.getWorld())) { Slimefun.getLocalization().sendMessage(p, "messages.disabled-item", true); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/crafting/SmithingTableListener.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/crafting/SmithingTableListener.java index 745a63736a..dd1567f778 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/crafting/SmithingTableListener.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/crafting/SmithingTableListener.java @@ -2,14 +2,13 @@ import javax.annotation.Nonnull; -import org.bukkit.entity.Player; import org.bukkit.event.Event.Result; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.ItemStack; +import org.bukkit.event.inventory.PrepareSmithingEvent; +import org.bukkit.event.inventory.SmithItemEvent; +import io.github.thebusybiscuit.slimefun4.api.MinecraftVersion; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; @@ -18,6 +17,7 @@ * smithing table. * * @author Sefiraat + * @author iTwins */ public class SmithingTableListener implements SlimefunCraftingListener { @@ -26,15 +26,29 @@ public SmithingTableListener(@Nonnull Slimefun plugin) { } @EventHandler(ignoreCancelled = true) - public void onSmith(InventoryClickEvent e) { - if (e.getInventory().getType() == InventoryType.SMITHING && e.getRawSlot() == 2 && e.getWhoClicked() instanceof Player) { - ItemStack materialItem = e.getInventory().getContents()[1]; - - // Checks if the item in the Material/Netherite slot is allowed to be used. - if (isUnallowed(materialItem)) { - e.setResult(Result.DENY); - Slimefun.getLocalization().sendMessage(e.getWhoClicked(), "smithing_table.not-working", true); + public void onSmith(SmithItemEvent e) { + SlimefunItem sfItem = SlimefunItem.getByItem(e.getInventory().getContents()[materialSlot()]); + if (sfItem != null && !sfItem.isUseableInWorkbench()) { + e.setResult(Result.DENY); + Slimefun.getLocalization().sendMessage(e.getWhoClicked(), "smithing_table.not-working", true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onPrepareSmith(PrepareSmithingEvent e) { + if (e.getInventory().getResult() != null) { + SlimefunItem sfItem = SlimefunItem.getByItem(e.getInventory().getContents()[materialSlot()]); + if (sfItem != null && !sfItem.isUseableInWorkbench()) { + e.setResult(null); } } } + + private int materialSlot() { + if (Slimefun.getMinecraftVersion().isAtLeast(MinecraftVersion.MINECRAFT_1_20)) { + return 2; + } + return 1; + } + } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/GEOMiningOperation.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/GEOMiningOperation.java new file mode 100644 index 0000000000..372f9f502c --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/GEOMiningOperation.java @@ -0,0 +1,42 @@ +package io.github.thebusybiscuit.slimefun4.implementation.operations; + +import java.util.OptionalInt; + +import javax.annotation.Nonnull; + +import io.github.bakedlibs.dough.blocks.BlockPosition; +import io.github.thebusybiscuit.slimefun4.api.geo.GEOResource; +import io.github.thebusybiscuit.slimefun4.api.geo.ResourceManager; +import io.github.thebusybiscuit.slimefun4.core.machines.MachineOperation; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.implementation.items.geo.GEOMiner; + +/** + * This {@link MachineOperation} represents a {@link GEOMiner} + * mining a {@link GEOResource}. + * + * @author iTwins + * + * @see GEOMiner + */ +public class GEOMiningOperation extends MiningOperation { + + private final GEOResource resource; + + public GEOMiningOperation(@Nonnull GEOResource resource, int totalTicks) { + super(resource.getItem().clone(), totalTicks); + this.resource = resource; + } + + /** + * This returns the {@link GEOResource} back to the chunk + * when the {@link GEOMiningOperation} gets cancelled + */ + @Override + public void onCancel(@Nonnull BlockPosition position) { + ResourceManager resourceManager = Slimefun.getGPSNetwork().getResourceManager(); + OptionalInt supplies = resourceManager.getSupplies(resource, position.getWorld(), position.getChunkX(), position.getChunkZ()); + supplies.ifPresent(s -> resourceManager.setSupplies(resource, position.getWorld(), position.getChunkX(), position.getChunkZ(), s + 1)); + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/MiningOperation.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/MiningOperation.java index 2a1201855f..e7f94c98bf 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/MiningOperation.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/operations/MiningOperation.java @@ -5,17 +5,13 @@ import org.apache.commons.lang.Validate; import org.bukkit.inventory.ItemStack; -import io.github.thebusybiscuit.slimefun4.api.geo.GEOResource; import io.github.thebusybiscuit.slimefun4.core.machines.MachineOperation; -import io.github.thebusybiscuit.slimefun4.implementation.items.geo.GEOMiner; /** - * This {@link MachineOperation} represents a {@link GEOMiner} - * mining a {@link GEOResource}. + * This {@link MachineOperation} represents an operation + * with no inputs, only a result. * * @author TheBusyBiscuit - * - * @see GEOMiner * */ public class MiningOperation implements MachineOperation { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java index c2b40ae1ed..29ac850805 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/armor/RadiationTask.java @@ -85,6 +85,8 @@ protected void onPlayerTick(Player p, PlayerProfile profile) { BaseComponent[] components = new ComponentBuilder().append(ChatColors.color(msg)).create(); p.spigot().sendMessage(ChatMessageType.ACTION_BAR, components); } + } else { + RadiationUtils.removeExposure(p, 1); } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java b/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java index 579e5bfb69..5063884c56 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/integrations/IntegrationsManager.java @@ -6,6 +6,7 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import dev.lone.itemsadder.api.CustomBlock; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Server; @@ -236,7 +237,7 @@ public boolean isEventFaked(@Nonnull Event event) { public boolean isCustomBlock(@Nonnull Block block) { if (isItemsAdderInstalled) { try { - return ItemsAdder.isCustomBlock(block); + return CustomBlock.byAlreadyPlaced(block) != null; } catch (Exception | LinkageError x) { logError("ItemsAdder", x); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/Storage.java b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/Storage.java new file mode 100644 index 0000000000..037db2afc3 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/Storage.java @@ -0,0 +1,26 @@ +package io.github.thebusybiscuit.slimefun4.storage; + +import io.github.thebusybiscuit.slimefun4.storage.data.PlayerData; + +import javax.annotation.concurrent.ThreadSafe; + +import com.google.common.annotations.Beta; + +import java.util.UUID; + +/** + * The {@link Storage} interface is the abstract layer on top of our storage backends. + * Every backend has to implement this interface and has to implement it in a thread-safe way. + * There will be no expectation of running functions in here within the main thread. + * + *

+ * This API is still experimental, it may change without notice. + */ +@Beta +@ThreadSafe +public interface Storage { + + PlayerData loadPlayerData(UUID uuid); + + void savePlayerData(UUID uuid, PlayerData data); +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java new file mode 100644 index 0000000000..d7981a5466 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/backend/legacy/LegacyStorage.java @@ -0,0 +1,127 @@ +package io.github.thebusybiscuit.slimefun4.storage.backend.legacy; + +import io.github.bakedlibs.dough.config.Config; +import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint; +import io.github.thebusybiscuit.slimefun4.api.player.PlayerBackpack; +import io.github.thebusybiscuit.slimefun4.api.researches.Research; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.storage.Storage; +import io.github.thebusybiscuit.slimefun4.storage.data.PlayerData; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; + +import com.google.common.annotations.Beta; + +import javax.annotation.Nonnull; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; + +@Beta +public class LegacyStorage implements Storage { + + @Override + public PlayerData loadPlayerData(@Nonnull UUID uuid) { + Config playerFile = new Config("data-storage/Slimefun/Players/" + uuid + ".yml"); + // Not too sure why this is its own file + Config waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid + ".yml"); + + // Load research + Set researches = new HashSet<>(); + for (Research research : Slimefun.getRegistry().getResearches()) { + if (playerFile.contains("researches." + research.getID())) { + researches.add(research); + } + } + + // Load backpacks + HashMap backpacks = new HashMap<>(); + for (String key : playerFile.getKeys("backpacks")) { + try { + int id = Integer.parseInt(key); + int size = playerFile.getInt("backpacks." + key + ".size"); + + HashMap items = new HashMap<>(); + for (int i = 0; i < size; i++) { + items.put(i, playerFile.getItem("backpacks." + key + ".contents." + i)); + } + + PlayerBackpack backpack = PlayerBackpack.load(uuid, id, size, items); + + backpacks.put(id, backpack); + } catch (Exception x) { + Slimefun.logger().log(Level.WARNING, x, () -> "Could not load Backpack \"" + key + "\" for Player \"" + uuid + '"'); + } + } + + // Load waypoints + Set waypoints = new HashSet<>(); + for (String key : waypointsFile.getKeys()) { + try { + if (waypointsFile.contains(key + ".world") && Bukkit.getWorld(waypointsFile.getString(key + ".world")) != null) { + String waypointName = waypointsFile.getString(key + ".name"); + Location loc = waypointsFile.getLocation(key); + waypoints.add(new Waypoint(uuid, key, loc, waypointName)); + } + } catch (Exception x) { + Slimefun.logger().log(Level.WARNING, x, () -> "Could not load Waypoint \"" + key + "\" for Player \"" + uuid + '"'); + } + } + + return new PlayerData(researches, backpacks, waypoints); + } + + // The current design of saving all at once isn't great, this will be refined. + @Override + public void savePlayerData(@Nonnull UUID uuid, @Nonnull PlayerData data) { + Config playerFile = new Config("data-storage/Slimefun/Players/" + uuid + ".yml"); + // Not too sure why this is its own file + Config waypointsFile = new Config("data-storage/Slimefun/waypoints/" + uuid + ".yml"); + + // Save research + playerFile.setValue("rearches", null); + for (Research research : Slimefun.getRegistry().getResearches()) { + // Save the research if it's researched + if (data.getResearches().contains(research)) { + playerFile.setValue("researches." + research.getID(), true); + + // Remove the research if it's no longer researched + } else if (playerFile.contains("researches." + research.getID())) { + playerFile.setValue("researches." + research.getID(), null); + } + } + + // Save backpacks + for (PlayerBackpack backpack : data.getBackpacks().values()) { + playerFile.setValue("backpacks." + backpack.getId() + ".size", backpack.getSize()); + + for (int i = 0; i < backpack.getSize(); i++) { + ItemStack item = backpack.getInventory().getItem(i); + if (item != null) { + playerFile.setValue("backpacks." + backpack.getId() + ".contents." + i, item); + + // Remove the item if it's no longer in the inventory + } else if (playerFile.contains("backpacks." + backpack.getId() + ".contents." + i)) { + playerFile.setValue("backpacks." + backpack.getId() + ".contents." + i, null); + } + } + } + + // Save waypoints + waypointsFile.clear(); + for (Waypoint waypoint : data.getWaypoints()) { + // Legacy data uses IDs + waypointsFile.setValue(waypoint.getId(), waypoint.getLocation()); + waypointsFile.setValue(waypoint.getId() + ".name", waypoint.getName()); + } + + // Save files + playerFile.save(); + waypointsFile.save(); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/storage/data/PlayerData.java b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/data/PlayerData.java new file mode 100644 index 0000000000..8615b6ee5f --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/storage/data/PlayerData.java @@ -0,0 +1,96 @@ +package io.github.thebusybiscuit.slimefun4.storage.data; + +import com.google.common.annotations.Beta; + +import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint; +import io.github.thebusybiscuit.slimefun4.api.player.PlayerBackpack; +import io.github.thebusybiscuit.slimefun4.api.researches.Research; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; + +import org.apache.commons.lang.Validate; + +/** + * The data which backs {@link io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile} + * + * This API is still experimental, it may change without notice. + */ +// TODO: Should we keep this in PlayerProfile? +@Beta +public class PlayerData { + + private final Set researches = new HashSet<>(); + private final Map backpacks = new HashMap<>(); + private final Set waypoints = new HashSet<>(); + + public PlayerData(Set researches, Map backpacks, Set waypoints) { + this.researches.addAll(researches); + this.backpacks.putAll(backpacks); + this.waypoints.addAll(waypoints); + } + + public Set getResearches() { + return researches; + } + + public void addResearch(@Nonnull Research research) { + Validate.notNull(research, "Cannot add a 'null' research!"); + researches.add(research); + } + + public void removeResearch(@Nonnull Research research) { + Validate.notNull(research, "Cannot remove a 'null' research!"); + researches.remove(research); + } + + @Nonnull + public Map getBackpacks() { + return backpacks; + } + + @Nonnull + public PlayerBackpack getBackpack(int id) { + return backpacks.get(id); + } + + public void addBackpack(@Nonnull PlayerBackpack backpack) { + Validate.notNull(backpack, "Cannot add a 'null' backpack!"); + backpacks.put(backpack.getId(), backpack); + } + + public void removeBackpack(@Nonnull PlayerBackpack backpack) { + Validate.notNull(backpack, "Cannot remove a 'null' backpack!"); + backpacks.remove(backpack.getId()); + } + + public Set getWaypoints() { + return waypoints; + } + + public void addWaypoint(@Nonnull Waypoint waypoint) { + Validate.notNull(waypoint, "Cannot add a 'null' waypoint!"); + + for (Waypoint wp : waypoints) { + if (wp.getId().equals(waypoint.getId())) { + throw new IllegalArgumentException("A Waypoint with that id already exists for this Player"); + } + } + + // Limited to 21 due to limited UI space and no pagination + if (waypoints.size() >= 21) { + return; // not sure why this doesn't throw but the one above does... + } + + waypoints.add(waypoint); + } + + public void removeWaypoint(@Nonnull Waypoint waypoint) { + Validate.notNull(waypoint, "Cannot remove a 'null' waypoint!"); + waypoints.remove(waypoint); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChatUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChatUtils.java index e3d7aac4b9..829e5891c3 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChatUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ChatUtils.java @@ -80,4 +80,26 @@ public static void awaitInput(@Nonnull Player p, @Nonnull Consumer callb return builder.toString(); } + /** + * This method adds an s to a string if the supplied integer is not 1. + * + * @param string + * The string to potentially pluralize + * @param count + * The amount of things + * @return + * {@code string} if {@code count} is 1 else {@code string + "s"} + * @throws IllegalArgumentException + * if count is less than 0 + */ + public static @Nonnull String checkPlurality(@Nonnull String string, int count) { + if (count < 0) { + throw new IllegalArgumentException("Argument count cannot be negative."); + } + if (count == 1) { + return string; + } + return string + "s"; + } + } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java index f57aaf4ac7..5fc3bda47f 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/SlimefunUtils.java @@ -589,12 +589,14 @@ public static boolean canPlayerUseItem(@Nonnull Player p, @Nullable ItemStack it * The {@link ItemSpawnReason} why the item is being dropped * @param addRandomOffset * Whether a random offset should be added (see {@link World#dropItemNaturally(Location, ItemStack)}) + * @param player + * The player that caused this {@link SlimefunItemSpawnEvent} * * @return The dropped {@link Item} (or null if the {@link SlimefunItemSpawnEvent} was cancelled) */ @ParametersAreNonnullByDefault - public static @Nullable Item spawnItem(Location loc, ItemStack item, ItemSpawnReason reason, boolean addRandomOffset) { - SlimefunItemSpawnEvent event = new SlimefunItemSpawnEvent(loc, item, reason); + public static @Nullable Item spawnItem(Location loc, ItemStack item, ItemSpawnReason reason, boolean addRandomOffset, @Nullable Player player) { + SlimefunItemSpawnEvent event = new SlimefunItemSpawnEvent(player, loc, item, reason); Slimefun.instance().getServer().getPluginManager().callEvent(event); if (!event.isCancelled()) { @@ -610,6 +612,27 @@ public static boolean canPlayerUseItem(@Nonnull Player p, @Nullable ItemStack it } } + /** + * Helper method to spawn an {@link ItemStack}. + * This method automatically calls a {@link SlimefunItemSpawnEvent} to allow + * other plugins to catch the item being dropped. + * + * @param loc + * The {@link Location} where to drop the item + * @param item + * The {@link ItemStack} to drop + * @param reason + * The {@link ItemSpawnReason} why the item is being dropped + * @param addRandomOffset + * Whether a random offset should be added (see {@link World#dropItemNaturally(Location, ItemStack)}) + * + * @return The dropped {@link Item} (or null if the {@link SlimefunItemSpawnEvent} was cancelled) + */ + @ParametersAreNonnullByDefault + public static @Nullable Item spawnItem(Location loc, ItemStack item, ItemSpawnReason reason, boolean addRandomOffset) { + return spawnItem(loc, item, reason, addRandomOffset, null); + } + /** * Helper method to spawn an {@link ItemStack}. * This method automatically calls a {@link SlimefunItemSpawnEvent} to allow diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f906fd3754..cb133170e4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -8,6 +8,7 @@ options: chat-prefix: '&a&lSlimefun 4&7> ' armor-update-interval: 10 enable-armor-effects: true + enable-radiation: true radiation-update-interval: 1 radiation-grace-period: 15 rainbow-armor-update-interval: 3 @@ -21,6 +22,7 @@ options: burn-players-when-radioactive: true drop-excess-sf-give-items: false backup-data: true + drop-block-creative: true guide: show-vanilla-recipes: true @@ -87,4 +89,4 @@ research-ranks: - Excellent Master - Great Master - Proficient Master -- The chosen One +- The Chosen One diff --git a/src/main/resources/languages/en/messages.yml b/src/main/resources/languages/en/messages.yml index f9c32a8115..81d5115c30 100644 --- a/src/main/resources/languages/en/messages.yml +++ b/src/main/resources/languages/en/messages.yml @@ -172,6 +172,7 @@ messages: bee-suit-slow-fall: '&eYour Bee Wings will help you to get back to the ground safe and slow' deprecated-item: '&4This item has been deprecated and will be removed from Slimefun soon.' researching-is-disabled: '&cResearching has been disabled on this server. Everything is unlocked by default!' + await-deletion: '&cYou cannot place a Slimefun block so soon after breaking one. Try again shortly.' multi-tool: mode-change: '&b%device% mode changed to: &9%mode%' diff --git a/src/main/resources/tags/block_placer_ignored_materials.json b/src/main/resources/tags/block_placer_ignored_materials.json index 3f9417b711..bc32030c7f 100644 --- a/src/main/resources/tags/block_placer_ignored_materials.json +++ b/src/main/resources/tags/block_placer_ignored_materials.json @@ -27,7 +27,6 @@ "minecraft:lily_pad", "minecraft:dead_bush", "minecraft:fern", - "minecraft:grass", "minecraft:sea_pickle", "minecraft:nether_wart", "minecraft:seagrass", @@ -35,6 +34,10 @@ "minecraft:kelp", "minecraft:bell", "minecraft:lantern", + { + "id" : "minecraft:grass", + "required" : false + }, { "id" : "minecraft:soul_lantern", "required" : false @@ -58,6 +61,10 @@ { "id" : "minecraft:weeping_vines", "required" : false + }, + { + "id" : "minecraft:short_grass", + "required" : false } ] } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockBreakEvent.java b/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockBreakEvent.java index 2f94729d38..9eb9225213 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockBreakEvent.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockBreakEvent.java @@ -58,7 +58,7 @@ void testEventIsFired() { Player player = new PlayerMock(server, "SomePlayer"); World world = server.addSimpleWorld("my_world"); - Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, 1, 1, 1)); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, TestUtilities.randomInt(), 100, TestUtilities.randomInt())); Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); BlockStorage.addBlockInfo(block, "id", "FOOD_COMPOSTER"); @@ -75,7 +75,7 @@ void testGetters() { player.getInventory().setItemInMainHand(itemStack); World world = server.addSimpleWorld("my_world"); - Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, 1, 1, 1)); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, TestUtilities.randomInt(), 100, TestUtilities.randomInt())); Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); BlockStorage.addBlockInfo(block, "id", "FOOD_COMPOSTER"); @@ -106,7 +106,7 @@ public void onBlockBreak(SlimefunBlockBreakEvent event) { player.getInventory().setItemInMainHand(itemStack); World world = server.addSimpleWorld("my_world"); - Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, 1, 1, 1)); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, TestUtilities.randomInt(), 100, TestUtilities.randomInt())); Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); BlockStorage.addBlockInfo(block, "id", "FOOD_COMPOSTER"); @@ -119,4 +119,24 @@ public void onBlockBreak(SlimefunBlockBreakEvent event) { return true; }); } + + @Test + @DisplayName("Test that breaking a Slimefun block gets queued for deletion") + void testBlockBreaksGetQueuedForDeletion() { + Player player = new PlayerMock(server, "SomePlayer"); + ItemStack itemStack = new ItemStack(Material.IRON_PICKAXE); + player.getInventory().setItemInMainHand(itemStack); + + World world = server.addSimpleWorld("my_world"); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, TestUtilities.randomInt(), 100, TestUtilities.randomInt())); + + Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); + BlockStorage.addBlockInfo(block, "id", "FOOD_COMPOSTER"); + + BlockBreakEvent blockBreakEvent = new BlockBreakEvent(block, player); + server.getPluginManager().callEvent(blockBreakEvent); + server.getPluginManager().assertEventFired(SlimefunBlockBreakEvent.class, e -> true); + + Assertions.assertTrue(Slimefun.getTickerTask().isDeletedSoon(block.getLocation())); + } } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockPlaceEvent.java b/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockPlaceEvent.java index b599f13eb6..f6d8d2868d 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockPlaceEvent.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestSlimefunBlockPlaceEvent.java @@ -16,6 +16,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; @@ -61,9 +62,10 @@ void testEventIsFired() { player.getInventory().setItemInMainHand(itemStack); World world = server.addSimpleWorld("my_world"); - Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, 1, 1, 1)); - Block blockAgainst = new BlockMock(Material.GRASS, new Location(world, 1, 0, 1)); - BlockStorage.clearBlockInfo(block); + int x = TestUtilities.randomInt(); + int z = TestUtilities.randomInt(); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, x, 0, z)); + Block blockAgainst = new BlockMock(Material.GRASS, new Location(world, x, 1, z)); Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); @@ -83,9 +85,10 @@ void testGetters() { player.getInventory().setItemInMainHand(itemStack); World world = server.addSimpleWorld("my_world"); - Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, 1, 1, 1)); - Block blockAgainst = new BlockMock(Material.GRASS, new Location(world, 1, 0, 1)); - BlockStorage.clearBlockInfo(block); + int x = TestUtilities.randomInt(); + int z = TestUtilities.randomInt(); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, x, 0, z)); + Block blockAgainst = new BlockMock(Material.GRASS, new Location(world, x, 1, z)); Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); @@ -119,9 +122,10 @@ public void onBlockPlace(SlimefunBlockPlaceEvent event) { player.getInventory().setItemInMainHand(itemStack); World world = server.addSimpleWorld("my_world"); - Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, 1, 1, 1)); - Block blockAgainst = new BlockMock(Material.GRASS, new Location(world, 1, 0, 1)); - BlockStorage.clearBlockInfo(block); + int x = TestUtilities.randomInt(); + int z = TestUtilities.randomInt(); + Block block = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, x, 0, z)); + Block blockAgainst = new BlockMock(Material.GRASS, new Location(world, x, 1, z)); Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); @@ -136,4 +140,48 @@ public void onBlockPlace(SlimefunBlockPlaceEvent event) { return true; }); } + + @Test + @DisplayName("Test that you cannot place before a SlimefunBlock is fully cleared") + void testBlockPlacementBeforeFullDeletion() { + Player player = new PlayerMock(server, "SomePlayer"); + ItemStack itemStack = slimefunItem.getItem(); + player.getInventory().setItemInMainHand(itemStack); + + // Place first block + World world = server.addSimpleWorld("my_world"); + int x = TestUtilities.randomInt(); + int z = TestUtilities.randomInt(); + Block firstBlock = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, x, 0, z)); + Block firstBlockAgainst = new BlockMock(Material.GRASS, new Location(world, x, 1, z)); + + Slimefun.getRegistry().getWorlds().put("my_world", new BlockStorage(world)); + + BlockPlaceEvent firstBlockPlaceEvent = new BlockPlaceEvent( + firstBlock, firstBlock.getState(), firstBlockAgainst, itemStack, player, true, EquipmentSlot.HAND + ); + + server.getPluginManager().callEvent(firstBlockPlaceEvent); + server.getPluginManager().assertEventFired(SlimefunBlockPlaceEvent.class, e -> { + Assertions.assertFalse(e.isCancelled()); + return true; + }); + + // Break block + server.getPluginManager().callEvent(new BlockBreakEvent(firstBlock, player)); + server.getPluginManager().assertEventFired(SlimefunBlockBreakEvent.class, e -> true); + + // Assert that the block is not fully deleted + Assertions.assertTrue(Slimefun.getTickerTask().isDeletedSoon(firstBlock.getLocation())); + + // Place second block in the same location + Block secondBlock = new BlockMock(Material.GREEN_TERRACOTTA, new Location(world, x, 0, z)); + Block secondBlockAgainst = new BlockMock(Material.GRASS, new Location(world, x, 1, z)); + + BlockPlaceEvent secondBlockPlaceEvent = new BlockPlaceEvent( + secondBlock, secondBlock.getState(), secondBlockAgainst, itemStack, player, true, EquipmentSlot.HAND + ); + server.getPluginManager().callEvent(secondBlockPlaceEvent); + Assertions.assertTrue(secondBlockPlaceEvent.isCancelled()); + } } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestTalismanActivateEvent.java b/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestTalismanActivateEvent.java new file mode 100644 index 0000000000..83b49fa231 --- /dev/null +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/api/events/TestTalismanActivateEvent.java @@ -0,0 +1,177 @@ +package io.github.thebusybiscuit.slimefun4.api.events; + +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.implementation.SlimefunItems; +import io.github.thebusybiscuit.slimefun4.implementation.items.magical.talismans.Talisman; +import io.github.thebusybiscuit.slimefun4.implementation.listeners.TalismanListener; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerItemBreakEvent; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TestTalismanActivateEvent { + + private static ServerMock server; + private static Slimefun plugin; + private static Player player; + private static SlimefunItem talisman; + private static SlimefunItem enderTalisman; + + @BeforeAll + public static void load() { + server = MockBukkit.mock(); + plugin = MockBukkit.load(Slimefun.class); + + new TalismanListener(plugin); + + talisman = new Talisman(SlimefunItems.TALISMAN_ANVIL, new ItemStack[] {}, true, false, "anvil"); + talisman.register(plugin); + + enderTalisman = SlimefunItem.getById("ENDER_" + talisman.getId()); + + player = server.addPlayer(); + } + + @AfterAll + public static void unload() { + MockBukkit.unmock(); + } + + void activateAnvilTalisman(boolean enderVariant, boolean inEnderChest) { + player.getInventory().clear(); + player.getEnderChest().clear(); + + ItemStack talismanItem = enderVariant ? enderTalisman.getItem() : talisman.getItem(); + ItemStack breakableItem = new ItemStack(Material.IRON_PICKAXE); + + if (inEnderChest) { + player.getEnderChest().addItem(talismanItem); + } else { + player.getInventory().addItem(talismanItem); + } + + player.getInventory().setItemInMainHand(breakableItem); + + PlayerItemBreakEvent event = new PlayerItemBreakEvent(player, breakableItem); + server.getPluginManager().callEvent(event); + } + + @Test + @DisplayName("Test that TalismanActivateEvent is fired when an anvil talisman activates") + void testEventIsFired() { + // Assert the talisman activates in the inventory + activateAnvilTalisman(false, false); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, ignored -> true); + server.getPluginManager().clearEvents(); + + // Assert the talisman activates in the ender chest + activateAnvilTalisman(true, true); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, ignored -> true); + server.getPluginManager().clearEvents(); + // Assert the normal talisman does not activate in the ender chest + activateAnvilTalisman(false, true); + try { + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, ignored -> true); + } catch (AssertionError ignored) { + return; // This is expected; the event should not have fired + } + server.getPluginManager().clearEvents(); + + // Assert the ender talisman does not activate in the inventory + try { + activateAnvilTalisman(true, false); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, ignored -> true); + } catch (AssertionError ignored) { + return; // This is expected; the event should not have fired + } + server.getPluginManager().clearEvents(); + } + + @Test + @DisplayName("Test that the TalismanActivateEvent has the correct fields") + void testEventFields() { + // Assert the talisman activates in the inventory + activateAnvilTalisman(false, false); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, event -> { + Assertions.assertEquals(talisman, event.getTalisman()); + Assertions.assertEquals(talisman.getItem(), event.getTalismanItem()); + Assertions.assertEquals(player, event.getPlayer()); + return true; + }); + server.getPluginManager().clearEvents(); + + // Assert the talisman activates in the ender chest + activateAnvilTalisman(true, true); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, event -> { + Assertions.assertEquals(enderTalisman, event.getTalisman()); + Assertions.assertEquals(enderTalisman.getItem(), event.getTalismanItem()); + Assertions.assertEquals(player, event.getPlayer()); + return true; + }); + server.getPluginManager().clearEvents(); + } + + @Test + @DisplayName("Test that the TalismanActivateEvent can be cancelled") + void testEventCanBeCancelled() { + server.getPluginManager().registerEvents(new Listener() { + @EventHandler + public void onTalismanActivate(TalismanActivateEvent event) { + event.setCancelled(true); + } + }, plugin); + + // Assert the talisman activates in the inventory + activateAnvilTalisman(false, false); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, event -> { + Assertions.assertTrue(event.isCancelled()); + return true; + }); + server.getPluginManager().clearEvents(); + + // Assert the talisman activates in the ender chest + activateAnvilTalisman(true, true); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, event -> { + Assertions.assertTrue(event.isCancelled()); + return true; + }); + server.getPluginManager().clearEvents(); + } + + @Test + @DisplayName("Test that the TalismanActivateEvent can prevent consumption") + void testEventCanPreventConsumption() { + server.getPluginManager().registerEvents(new Listener() { + @EventHandler + public void onTalismanActivate(TalismanActivateEvent event) { + event.setPreventConsumption(true); + } + }, plugin); + + // Assert the talisman activates in the inventory + activateAnvilTalisman(false, false); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, event -> { + Assertions.assertTrue(event.preventsConsumption()); + return true; + }); + server.getPluginManager().clearEvents(); + + // Assert the talisman activates in the ender chest + activateAnvilTalisman(true, true); + server.getPluginManager().assertEventFired(TalismanActivateEvent.class, event -> { + Assertions.assertTrue(event.preventsConsumption()); + return true; + }); + server.getPluginManager().clearEvents(); + } +} diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/api/gps/TestWaypoints.java b/src/test/java/io/github/thebusybiscuit/slimefun4/api/gps/TestWaypoints.java index d115135ba7..a0de64b14f 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/api/gps/TestWaypoints.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/api/gps/TestWaypoints.java @@ -1,5 +1,9 @@ package io.github.thebusybiscuit.slimefun4.api.gps; +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; import org.bukkit.entity.Player; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -19,16 +23,20 @@ class TestWaypoints { private static ServerMock server; private static Slimefun plugin; + private static File dataFolder; @BeforeAll public static void load() { server = MockBukkit.mock(); plugin = MockBukkit.load(Slimefun.class); + dataFolder = new File("data-storage/Slimefun/waypoints"); + dataFolder.mkdirs(); } @AfterAll - public static void unload() { + public static void unload() throws IOException { MockBukkit.unmock(); + FileUtils.deleteDirectory(dataFolder); } @Test @@ -38,9 +46,8 @@ void testAddWaypointToProfile() throws InterruptedException { PlayerProfile profile = TestUtilities.awaitProfile(player); Assertions.assertTrue(profile.getWaypoints().isEmpty()); - Waypoint waypoint = new Waypoint(profile, "hello", player.getLocation(), "HELLO"); + Waypoint waypoint = new Waypoint(player.getUniqueId(), "hello", player.getLocation(), "HELLO"); profile.addWaypoint(waypoint); - Assertions.assertTrue(profile.isDirty()); Assertions.assertThrows(IllegalArgumentException.class, () -> profile.addWaypoint(null)); @@ -55,7 +62,7 @@ void testRemoveWaypointFromProfile() throws InterruptedException { Player player = server.addPlayer(); PlayerProfile profile = TestUtilities.awaitProfile(player); - Waypoint waypoint = new Waypoint(profile, "hello", player.getLocation(), "HELLO"); + Waypoint waypoint = new Waypoint(player.getUniqueId(), "hello", player.getLocation(), "HELLO"); profile.addWaypoint(waypoint); Assertions.assertEquals(1, profile.getWaypoints().size()); @@ -76,7 +83,7 @@ void testWaypointAlreadyExisting() throws InterruptedException { Player player = server.addPlayer(); PlayerProfile profile = TestUtilities.awaitProfile(player); - Waypoint waypoint = new Waypoint(profile, "test", player.getLocation(), "Testing"); + Waypoint waypoint = new Waypoint(player.getUniqueId(), "test", player.getLocation(), "Testing"); profile.addWaypoint(waypoint); Assertions.assertEquals(1, profile.getWaypoints().size()); @@ -91,7 +98,7 @@ void testTooManyWaypoints() throws InterruptedException { PlayerProfile profile = TestUtilities.awaitProfile(player); for (int i = 0; i < 99; i++) { - Waypoint waypoint = new Waypoint(profile, String.valueOf(i), player.getLocation(), "Test"); + Waypoint waypoint = new Waypoint(player.getUniqueId(), String.valueOf(i), player.getLocation(), "Test"); profile.addWaypoint(waypoint); } @@ -114,11 +121,10 @@ void testWaypointEvent() throws InterruptedException { @DisplayName("Test equal Waypoints being equal") void testWaypointComparison() throws InterruptedException { Player player = server.addPlayer(); - PlayerProfile profile = TestUtilities.awaitProfile(player); - Waypoint waypoint = new Waypoint(profile, "waypoint", player.getLocation(), "Test"); - Waypoint same = new Waypoint(profile, "waypoint", player.getLocation(), "Test"); - Waypoint different = new Waypoint(profile, "waypoint_nope", player.getLocation(), "Test2"); + Waypoint waypoint = new Waypoint(player.getUniqueId(), "waypoint", player.getLocation(), "Test"); + Waypoint same = new Waypoint(player.getUniqueId(), "waypoint", player.getLocation(), "Test"); + Waypoint different = new Waypoint(player.getUniqueId(), "waypoint_nope", player.getLocation(), "Test2"); Assertions.assertEquals(waypoint, same); Assertions.assertEquals(waypoint.hashCode(), same.hashCode()); @@ -131,10 +137,9 @@ void testWaypointComparison() throws InterruptedException { @DisplayName("Test Deathpoints being recognized as Deathpoints") void testIsDeathpoint() throws InterruptedException { Player player = server.addPlayer(); - PlayerProfile profile = TestUtilities.awaitProfile(player); - Waypoint waypoint = new Waypoint(profile, "waypoint", player.getLocation(), "Some Waypoint"); - Waypoint deathpoint = new Waypoint(profile, "deathpoint", player.getLocation(), "player:death I died"); + Waypoint waypoint = new Waypoint(player.getUniqueId(), "waypoint", player.getLocation(), "Some Waypoint"); + Waypoint deathpoint = new Waypoint(player.getUniqueId(), "deathpoint", player.getLocation(), "player:death I died"); Assertions.assertFalse(waypoint.isDeathpoint()); Assertions.assertTrue(deathpoint.isDeathpoint()); diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/api/profiles/TestPlayerBackpacks.java b/src/test/java/io/github/thebusybiscuit/slimefun4/api/profiles/TestPlayerBackpacks.java index 3c1ae7175c..07f69761e0 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/api/profiles/TestPlayerBackpacks.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/api/profiles/TestPlayerBackpacks.java @@ -2,9 +2,7 @@ import java.util.Optional; -import org.bukkit.Material; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -39,16 +37,12 @@ public static void unload() { void testCreateBackpack() throws InterruptedException { Player player = server.addPlayer(); PlayerProfile profile = TestUtilities.awaitProfile(player); - Assertions.assertFalse(profile.isDirty()); PlayerBackpack backpack = profile.createBackpack(18); Assertions.assertNotNull(backpack); - // Creating a backpack should mark profiles as dirty - Assertions.assertTrue(profile.isDirty()); - - Assertions.assertEquals(profile, backpack.getOwner()); + Assertions.assertEquals(player.getUniqueId(), backpack.getOwnerId()); Assertions.assertEquals(18, backpack.getSize()); Assertions.assertEquals(18, backpack.getInventory().getSize()); } @@ -71,7 +65,6 @@ void testChangeSize() throws InterruptedException { backpack.setSize(27); Assertions.assertEquals(27, backpack.getSize()); - Assertions.assertTrue(profile.isDirty()); } @Test @@ -90,33 +83,4 @@ void testGetBackpackById() throws InterruptedException { Assertions.assertFalse(profile.getBackpack(500).isPresent()); } - - @Test - @DisplayName("Test loading a backpack from file") - void testLoadBackpackFromFile() throws InterruptedException { - Player player = server.addPlayer(); - PlayerProfile profile = TestUtilities.awaitProfile(player); - - profile.getConfig().setValue("backpacks.50.size", 27); - - for (int i = 0; i < 27; i++) { - profile.getConfig().setValue("backpacks.50.contents." + i, new ItemStack(Material.DIAMOND)); - } - - Optional optional = profile.getBackpack(50); - Assertions.assertTrue(optional.isPresent()); - - PlayerBackpack backpack = optional.get(); - Assertions.assertEquals(50, backpack.getId()); - Assertions.assertEquals(27, backpack.getSize()); - Assertions.assertEquals(-1, backpack.getInventory().firstEmpty()); - - backpack.getInventory().setItem(1, new ItemStack(Material.NETHER_STAR)); - - Assertions.assertEquals(new ItemStack(Material.DIAMOND), profile.getConfig().getItem("backpacks.50.contents.1")); - - // Saving should write it to the Config file - backpack.save(); - Assertions.assertEquals(new ItemStack(Material.NETHER_STAR), profile.getConfig().getItem("backpacks.50.contents.1")); - } } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/core/guide/TestGuideOpening.java b/src/test/java/io/github/thebusybiscuit/slimefun4/core/guide/TestGuideOpening.java index 8ed5d838b9..a8685b9736 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/core/guide/TestGuideOpening.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/core/guide/TestGuideOpening.java @@ -5,11 +5,14 @@ import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; +import io.github.thebusybiscuit.slimefun4.implementation.guide.SurvivalSlimefunGuide; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -89,14 +92,36 @@ void testOpenItemStack() throws InterruptedException { Mockito.verify(guide).displayItem(profile, item, 1, false); } + @Test + @DisplayName("Test if the Slimefun Search works with normal and colored terms") + void testOpenSearch_withColoredSearchTerm() throws InterruptedException { + String normalTerm = "iron"; + String coloredTerm = ChatColor.DARK_PURPLE + "iron"; + + SlimefunItem testItem = TestUtilities.mockSlimefunItem(plugin, "IRON_ITEM", new CustomItemStack(Material.IRON_INGOT, "iron item")); + testItem.register(plugin); + + Player player = server.addPlayer(); + PlayerProfile profile = TestUtilities.awaitProfile(player); + SlimefunGuideImplementation guide = new SurvivalSlimefunGuide(false, false); + + guide.openSearch(profile, normalTerm, false); + // Assert we can open with a non-coloured search term + Assertions.assertTrue(player.getOpenInventory().getTopInventory().contains(testItem.getItem()), "Failed on normal query"); + + guide.openSearch(profile, coloredTerm, false); + // Assert we can open with a coloured search term + Assertions.assertTrue(player.getOpenInventory().getTopInventory().contains(testItem.getItem()), "Failed on colored query"); + } + @Test @DisplayName("Test if the Slimefun Search can be opened from the History") - void testOpenSearch() throws InterruptedException { - String query = "electric"; + void testOpenSearchHistory() throws InterruptedException { + String term = "electric"; SlimefunGuideImplementation guide = Mockito.mock(SlimefunGuideImplementation.class); - PlayerProfile profile = prepare(guide, history -> history.add(query)); - Mockito.verify(guide).openSearch(profile, query, false); + PlayerProfile profile = prepare(guide, history -> history.add(term)); + Mockito.verify(guide).openSearch(profile, term, false); } @Test diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/core/services/TestUpdaterService.java b/src/test/java/io/github/thebusybiscuit/slimefun4/core/services/TestUpdaterService.java index 9f76c65d42..4de8e01e62 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/core/services/TestUpdaterService.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/core/services/TestUpdaterService.java @@ -33,7 +33,7 @@ public static void unload() { @Test @DisplayName("Test if the development branch is recognized correctly") void testDevelopmentBuilds() { - UpdaterService service = new UpdaterService(plugin, "DEV - 131 (git 123456)", file); + UpdaterService service = new UpdaterService(plugin, "Dev - 131 (git 123456)", file); Assertions.assertEquals(SlimefunBranch.DEVELOPMENT, service.getBranch()); Assertions.assertTrue(service.getBranch().isOfficial()); // Cannot currently be tested... yay diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestBackpackListener.java b/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestBackpackListener.java index 01e83b9e71..443d47c3c7 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestBackpackListener.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestBackpackListener.java @@ -13,7 +13,6 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryType.SlotType; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.inventory.Inventory; @@ -119,7 +118,7 @@ void testSetId() throws InterruptedException { Assertions.assertEquals(ChatColor.GRAY + "ID: " + player.getUniqueId() + "#" + id, item.getItemMeta().getLore().get(2)); PlayerBackpack backpack = awaitBackpack(item); - Assertions.assertEquals(player.getUniqueId(), backpack.getOwner().getUUID()); + Assertions.assertEquals(player.getUniqueId(), backpack.getOwnerId()); Assertions.assertEquals(id, backpack.getId()); } @@ -132,16 +131,6 @@ void testOpenBackpack() throws InterruptedException { Assertions.assertEquals(backpack.getInventory(), view.getTopInventory()); } - @Test - @DisplayName("Test backpacks being marked dirty on close") - void testCloseBackpack() throws InterruptedException { - Player player = server.addPlayer(); - PlayerBackpack backpack = openMockBackpack(player, "TEST_CLOSE_BACKPACK", 27); - listener.onClose(new InventoryCloseEvent(player.getOpenInventory())); - - Assertions.assertTrue(backpack.getOwner().isDirty()); - } - @Test @DisplayName("Test backpacks not disturbing normal item dropping") void testBackpackDropNormalItem() throws InterruptedException { diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestSmithingTableListener.java b/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestSmithingTableListener.java index 472a90c9a1..3c57ed5060 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestSmithingTableListener.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/implementation/listeners/TestSmithingTableListener.java @@ -1,21 +1,26 @@ package io.github.thebusybiscuit.slimefun4.implementation.listeners; +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; + +import org.apache.commons.lang3.mutable.MutableObject; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.Event.Result; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType.SlotType; -import org.bukkit.inventory.Inventory; +import org.bukkit.event.inventory.PrepareSmithingEvent; +import org.bukkit.event.inventory.SmithItemEvent; import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.SmithingInventory; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import io.github.bakedlibs.dough.items.CustomItemStack; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; @@ -24,15 +29,13 @@ import io.github.thebusybiscuit.slimefun4.implementation.listeners.crafting.SmithingTableListener; import io.github.thebusybiscuit.slimefun4.test.TestUtilities; -import be.seeseemelk.mockbukkit.MockBukkit; -import be.seeseemelk.mockbukkit.ServerMock; - class TestSmithingTableListener { private static SmithingTableListener listener; private static ServerMock server; private static SlimefunItem slimefunIngot; + private static SlimefunItem usableSlimefunIngot; private static SlimefunItem slimefunTool; private static VanillaItem vanillaIngot; private static VanillaItem vanillaTool; @@ -45,11 +48,15 @@ public static void load() { slimefunTool = TestUtilities.mockSlimefunItem(plugin, "MOCK_DIAMOND_SWORD", new CustomItemStack(Material.DIAMOND_SWORD, "&6Mock")); slimefunIngot = TestUtilities.mockSlimefunItem(plugin, "MOCK_NETHERITE_INGOT", new CustomItemStack(Material.NETHERITE_INGOT, "&6Mock")); + usableSlimefunIngot = TestUtilities.mockSlimefunItem(plugin, "MOCK_NETHERITE_INGOT_USABLE", new CustomItemStack(Material.NETHERITE_INGOT, "&6Mock")); + usableSlimefunIngot.setUseableInWorkbench(true); + vanillaTool = TestUtilities.mockVanillaItem(plugin, Material.DIAMOND_SWORD, true); vanillaIngot = TestUtilities.mockVanillaItem(plugin, Material.NETHERITE_INGOT, true); slimefunTool.register(plugin); slimefunIngot.register(plugin); + usableSlimefunIngot.register(plugin); vanillaTool.register(plugin); vanillaIngot.register(plugin); } @@ -59,77 +66,182 @@ public static void unload() { MockBukkit.unmock(); } - private InventoryClickEvent mockSmithingEvent(ItemStack tool, ItemStack material) { + private SmithItemEvent mockSmithingEvent(ItemStack tool, ItemStack material) { Player player = server.addPlayer(); - Inventory inv = TestUtilities.mockInventory(InventoryType.SMITHING, tool, material, null); + + SmithingInventory inv = Mockito.mock(SmithingInventory.class); + // MinecraftVersion#isAtLeast always returns true during unit test, so we use the 1.20 layout here. + Mockito.when(inv.getContents()).thenReturn(new ItemStack[] { new ItemStack(Material.NETHERITE_UPGRADE_SMITHING_TEMPLATE), tool, material, null }); + InventoryView view = player.openInventory(inv); - InventoryClickEvent event = new InventoryClickEvent(view, SlotType.CONTAINER, 2, ClickType.LEFT, InventoryAction.PICKUP_ONE); + SmithItemEvent event = new SmithItemEvent(view, SlotType.RESULT, 3, ClickType.LEFT, InventoryAction.PICKUP_ONE); listener.onSmith(event); return event; } + private PrepareSmithingEvent mockPrepareSmithingEvent(ItemStack tool, ItemStack material) { + Player player = server.addPlayer(); + + SmithingInventory inv = Mockito.mock(SmithingInventory.class); + MutableObject result = new MutableObject<>(new ItemStack(Material.NETHERITE_PICKAXE)); + + Mockito.doAnswer(invocation -> { + ItemStack argument = invocation.getArgument(0); + result.setValue(argument); + return null; + }).when(inv).setResult(Mockito.any()); + + Mockito.when(inv.getResult()).thenAnswer(invocation -> result.getValue()); + // MinecraftVersion#isAtLeast always returns true during unit test, so we use the 1.20 layout here. + Mockito.when(inv.getContents()).thenReturn(new ItemStack[] { new ItemStack(Material.NETHERITE_UPGRADE_SMITHING_TEMPLATE), tool, material, null }); + + InventoryView view = player.openInventory(inv); + PrepareSmithingEvent event = new PrepareSmithingEvent(view, result.getValue()); + + listener.onPrepareSmith(event); + return event; + } + @Test @DisplayName("Test that vanilla is unchanged (ItemStack tool x ItemStack material)") void testSmithingTableWithItemStacks() { - InventoryClickEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), new ItemStack(Material.NETHERITE_INGOT)); + SmithItemEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), new ItemStack(Material.NETHERITE_INGOT)); Assertions.assertEquals(Result.DEFAULT, event.getResult()); } @Test @DisplayName("Test that SlimefunItem material doesn't work (ItemStack tool x SlimefunItem material)") void testSmithingTableWithItemStackAndSlimefunItem() { - InventoryClickEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), slimefunIngot.getItem()); + SmithItemEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), slimefunIngot.getItem()); Assertions.assertEquals(Result.DENY, event.getResult()); } @Test @DisplayName("Test that VanillaItem material works (ItemStack tool x VanillaItem material)") void testSmithingTableWithItemStackAndVanillaItem() { - InventoryClickEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), vanillaIngot.getItem()); + SmithItemEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), vanillaIngot.getItem()); Assertions.assertEquals(Result.DEFAULT, event.getResult()); } @Test @DisplayName("Test that SlimefunItems can upgrade with vanilla (SlimefunItem tool x ItemStack material)") void testSmithingTableWithSlimefunItemAndItemStack() { - InventoryClickEvent event = mockSmithingEvent(slimefunTool.getItem(), new ItemStack(Material.NETHERITE_INGOT)); + SmithItemEvent event = mockSmithingEvent(slimefunTool.getItem(), new ItemStack(Material.NETHERITE_INGOT)); Assertions.assertEquals(Result.DEFAULT, event.getResult()); } @Test @DisplayName("Test that SlimefunItems can't upgrade with SlimefunItem materials (SlimefunItem tool x SlimefunItem material)") void testSmithingTableWithSlimefunItems() { - InventoryClickEvent event = mockSmithingEvent(slimefunTool.getItem(), slimefunIngot.getItem()); + SmithItemEvent event = mockSmithingEvent(slimefunTool.getItem(), slimefunIngot.getItem()); Assertions.assertEquals(Result.DENY, event.getResult()); } @Test @DisplayName("Test that SlimefunItems can upgrade with VanillaItems (SlimefunItem tool x VanillaItem material)") void testSmithingTableWithSlimefunItemAndVanillaItem() { - InventoryClickEvent event = mockSmithingEvent(slimefunTool.getItem(), vanillaIngot.getItem()); + SmithItemEvent event = mockSmithingEvent(slimefunTool.getItem(), vanillaIngot.getItem()); Assertions.assertEquals(Result.DEFAULT, event.getResult()); } @Test - @DisplayName("Test that SlimefunItems can upgrade with vanilla (SlimefunItem tool x ItemStack material)") + @DisplayName("Test that VanillaItems can upgrade with vanilla (VanillaItem tool x ItemStack material)") void testSmithingTableWithVanillaItemAndItemStack() { - InventoryClickEvent event = mockSmithingEvent(vanillaTool.getItem(), new ItemStack(Material.NETHERITE_INGOT)); + SmithItemEvent event = mockSmithingEvent(vanillaTool.getItem(), new ItemStack(Material.NETHERITE_INGOT)); Assertions.assertEquals(Result.DEFAULT, event.getResult()); } @Test - @DisplayName("Test that SlimefunItems can't upgrade with SlimefunItem materials (SlimefunItem tool x SlimefunItem material)") + @DisplayName("Test that VanillaItems can't upgrade with SlimefunItem materials (VanillaItem tool x SlimefunItem material)") void testSmithingTableWithVanillaItemAndSlimefunItem() { - InventoryClickEvent event = mockSmithingEvent(vanillaTool.getItem(), slimefunIngot.getItem()); + SmithItemEvent event = mockSmithingEvent(vanillaTool.getItem(), slimefunIngot.getItem()); Assertions.assertEquals(Result.DENY, event.getResult()); } @Test - @DisplayName("Test that SlimefunItems can upgrade with VanillaItems (SlimefunItem tool x VanillaItem material)") + @DisplayName("Test that VanillaItems can upgrade with VanillaItems (VanillaItem tool x VanillaItem material)") void testSmithingTableWithVanillaItemAndVanillaItem() { - InventoryClickEvent event = mockSmithingEvent(vanillaTool.getItem(), vanillaIngot.getItem()); + SmithItemEvent event = mockSmithingEvent(vanillaTool.getItem(), vanillaIngot.getItem()); Assertions.assertEquals(Result.DEFAULT, event.getResult()); } + @Test + @DisplayName("Test that ItemStacks can be upgraded with SlimefunItem can-be-used-in-workbenches: true") + void testCanBeUsedInWorkbenchTrue() { + Assertions.assertTrue(usableSlimefunIngot.isUseableInWorkbench()); + SmithItemEvent event = mockSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), usableSlimefunIngot.getItem()); + Assertions.assertEquals(Result.DEFAULT, event.getResult()); + } + + @Test + @DisplayName("Test that vanilla is unchanged (ItemStack tool x ItemStack material)") + void testPrepareSmithingTableWithItemStacks() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), new ItemStack(Material.NETHERITE_INGOT)); + Assertions.assertNotNull(event.getResult()); + } + + @Test + @DisplayName("Test that SlimefunItem material doesn't work (ItemStack tool x SlimefunItem material)") + void testPrepareSmithingTableWithItemStackAndSlimefunItem() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), slimefunIngot.getItem()); + Assertions.assertNull(event.getResult()); + } + + @Test + @DisplayName("Test that VanillaItem material works (ItemStack tool x VanillaItem material)") + void testPrepareSmithingTableWithItemStackAndVanillaItem() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), vanillaIngot.getItem()); + Assertions.assertNotNull(event.getResult()); + } + + @Test + @DisplayName("Test that SlimefunItems can upgrade with vanilla (SlimefunItem tool x ItemStack material)") + void testPrepareSmithingTableWithSlimefunItemAndItemStack() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(slimefunTool.getItem(), new ItemStack(Material.NETHERITE_INGOT)); + Assertions.assertNotNull(event.getResult()); + } + + @Test + @DisplayName("Test that SlimefunItems can't upgrade with SlimefunItem materials (SlimefunItem tool x SlimefunItem material)") + void testPrepareSmithingTableWithSlimefunItems() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(slimefunTool.getItem(), slimefunIngot.getItem()); + Assertions.assertNull(event.getResult()); + } + + @Test + @DisplayName("Test that SlimefunItems can upgrade with VanillaItems (SlimefunItem tool x VanillaItem material)") + void testPrepareSmithingTableWithSlimefunItemAndVanillaItem() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(slimefunTool.getItem(), vanillaIngot.getItem()); + Assertions.assertNotNull(event.getResult()); + } + + @Test + @DisplayName("Test that VanillaItems can upgrade with vanilla (VanillaItem tool x ItemStack material)") + void testPrepareSmithingTableWithVanillaItemAndItemStack() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(vanillaTool.getItem(), new ItemStack(Material.NETHERITE_INGOT)); + Assertions.assertNotNull(event.getResult()); + } + + @Test + @DisplayName("Test that VanillaItems can't upgrade with SlimefunItem materials (VanillaItem tool x SlimefunItem material)") + void testPrepareSmithingTableWithVanillaItemAndSlimefunItem() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(vanillaTool.getItem(), slimefunIngot.getItem()); + Assertions.assertNull(event.getResult()); + } + + @Test + @DisplayName("Test that VanillaItems can upgrade with VanillaItems (VanillaItem tool x VanillaItem material)") + void testPrepareSmithingTableWithVanillaItemAndVanillaItem() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(vanillaTool.getItem(), vanillaIngot.getItem()); + Assertions.assertNotNull(event.getResult()); + } + + @Test + @DisplayName("Test that ItemStacks can be upgraded with SlimefunItem can-be-used-in-workbenches: true") + void testPrepareCanBeUsedInWorkbenchTrue() { + PrepareSmithingEvent event = mockPrepareSmithingEvent(new ItemStack(Material.DIAMOND_SWORD), usableSlimefunIngot.getItem()); + Assertions.assertNotNull(event.getResult()); + } + } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java b/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java new file mode 100644 index 0000000000..1859659999 --- /dev/null +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/storage/backend/TestLegacyBackend.java @@ -0,0 +1,383 @@ +package io.github.thebusybiscuit.slimefun4.storage.backend; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.UUID; + +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.World.Environment; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; +import io.github.thebusybiscuit.slimefun4.api.gps.Waypoint; +import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; +import io.github.thebusybiscuit.slimefun4.api.researches.Research; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.storage.backend.legacy.LegacyStorage; +import io.github.thebusybiscuit.slimefun4.storage.data.PlayerData; +import io.github.thebusybiscuit.slimefun4.test.TestUtilities; +import net.md_5.bungee.api.ChatColor; + +class TestLegacyBackend { + + private static ServerMock server; + private static Slimefun plugin; + + @BeforeAll + public static void load() { + server = MockBukkit.mock(); + plugin = MockBukkit.load(Slimefun.class); + + File playerFolder = new File("data-storage/Slimefun/Players"); + playerFolder.mkdirs(); + File waypointFolder = new File("data-storage/Slimefun/waypoints"); + waypointFolder.mkdirs(); + + // Not too sure why this is needed, we don't use it elsewhere, it should just use the ItemStack serialization + // My guess is MockBukkit isn't loading the ConfigurationSerialization class therefore the static block + // within the class isn't being fired (where ItemStack and other classes are registered) + ConfigurationSerialization.registerClass(ItemStack.class); + ConfigurationSerialization.registerClass(ItemMeta.class); + + setupResearches(); + } + + @AfterAll + public static void unload() throws IOException { + MockBukkit.unmock(); + FileUtils.deleteDirectory(new File("data-storage")); + } + + // Test simple loading and saving of player data + @Test + void testLoadingResearches() throws IOException { + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + Files.writeString(playerFile.toPath(), """ + researches: + '0': true + '1': true + '2': true + '3': true + '4': true + '5': true + '6': true + '7': true + '8': true + '9': true + """); + + // Load the player data + LegacyStorage storage = new LegacyStorage(); + PlayerData data = storage.loadPlayerData(uuid); + + // Check if the data is correct + Assertions.assertEquals(10, data.getResearches().size()); + for (int i = 0; i < 10; i++) { + Assertions.assertTrue(data.getResearches().contains(Slimefun.getRegistry().getResearches().get(i))); + } + } + + // There's some issues with deserializing items in tests, I spent quite a while debugging this + // and didn't really get anywhere. So commenting this out for now. + /* + @Test + void testLoadingBackpacks() throws IOException { + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + Files.writeString(playerFile.toPath(), """ + backpacks: + '0': + size: 9 + contents: + '0': + ==: org.bukkit.inventory.ItemStack + v: 1 + type: IRON_BLOCK + meta: + ==: org.bukkit.inventory.meta.ItemMeta + enchants: {} + damage: 0 + persistentDataContainer: + slimefun:slimefun_item: TEST + displayName: §6Test block + itemFlags: !!set {} + unbreakable: false + repairCost: 0 + """); + + // Load the player data + LegacyStorage storage = new LegacyStorage(); + PlayerData data = storage.loadPlayerData(uuid); + + // Check if the data is correct + Assertions.assertEquals(1, data.getBackpacks().size()); + Assertions.assertEquals(9, data.getBackpacks().get(0).getSize()); + + // Validate item deserialization + System.out.println( + Arrays.stream(data.getBackpack(0).getInventory().getContents()) + .map((item) -> item == null ? "null" : item.getType().name()) + .collect(Collectors.joining(", ")) + ); + ItemStack stack = data.getBackpack(0).getInventory().getItem(0); + Assertions.assertNotNull(stack); + Assertions.assertEquals("IRON_BLOCK", stack.getType().name()); + Assertions.assertEquals(1, stack.getAmount()); + Assertions.assertEquals(ChatColor.GREEN + "Test block", stack.getItemMeta().getDisplayName()); + } + */ + + @Test + void testLoadingWaypoints() throws IOException { + // Create mock world + server.createWorld(WorldCreator.name("world").environment(Environment.NORMAL)); + + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File waypointFile = new File("data-storage/Slimefun/waypoints/" + uuid + ".yml"); + Files.writeString(waypointFile.toPath(), """ + TEST: + x: -173.0 + y: 75.0 + z: -11.0 + pitch: 0.0 + yaw: 178.0 + world: world + name: test + """); + + // Load the player data + LegacyStorage storage = new LegacyStorage(); + PlayerData data = storage.loadPlayerData(uuid); + + // Check if the data is correct + Assertions.assertEquals(1, data.getWaypoints().size()); + + // Validate waypoint deserialization + Waypoint waypoint = data.getWaypoints().iterator().next(); + + Assertions.assertEquals("test", waypoint.getName()); + Assertions.assertEquals(-173.0, waypoint.getLocation().getX()); + Assertions.assertEquals(75.0, waypoint.getLocation().getY()); + Assertions.assertEquals(-11.0, waypoint.getLocation().getZ()); + Assertions.assertEquals(178.0, waypoint.getLocation().getYaw()); + Assertions.assertEquals(0.0, waypoint.getLocation().getPitch()); + Assertions.assertEquals("world", waypoint.getLocation().getWorld().getName()); + } + + @Test + void testSavingResearches() throws InterruptedException { + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + + PlayerProfile profile = TestUtilities.awaitProfile(player); + + for (Research research : Slimefun.getRegistry().getResearches()) { + profile.setResearched(research, true); + } + + // Save the player data + LegacyStorage storage = new LegacyStorage(); + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + PlayerData assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(10, assertion.getResearches().size()); + for (int i = 0; i < 10; i++) { + Assertions.assertTrue(assertion.getResearches().contains(Slimefun.getRegistry().getResearches().get(i))); + } + } + + // There's some issues with deserializing items in tests, I spent quite a while debugging this + // and didn't really get anywhere. So commenting this out for now. + /* + @Test + void testSavingBackpacks() throws InterruptedException { + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + + PlayerProfile profile = TestUtilities.awaitProfile(player); + + PlayerBackpack backpack = profile.createBackpack(9); + backpack.getInventory().addItem(SlimefunItems.AIR_RUNE); + + // Save the player data + LegacyStorage storage = new LegacyStorage(); + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + PlayerData assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(1, assertion.getBackpacks().size()); + } + */ + + @Test + void testSavingWaypoints() throws InterruptedException { + // Create mock world + World world = server.createWorld(WorldCreator.name("world").environment(Environment.NORMAL)); + + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + PlayerProfile profile = TestUtilities.awaitProfile(player); + + profile.addWaypoint(new Waypoint( + player.getUniqueId(), + "test", + new Location(world, 1, 2, 3, 4, 5), + ChatColor.GREEN + "Test waypoint") + ); + + // Save the player data + LegacyStorage storage = new LegacyStorage(); + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + PlayerData assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(1, assertion.getWaypoints().size()); + + // Validate waypoint deserialization + Waypoint waypoint = assertion.getWaypoints().iterator().next(); + + Assertions.assertEquals(ChatColor.GREEN + "Test waypoint", waypoint.getName()); + Assertions.assertEquals(1, waypoint.getLocation().getX()); + Assertions.assertEquals(2, waypoint.getLocation().getY()); + Assertions.assertEquals(3, waypoint.getLocation().getZ()); + Assertions.assertEquals(4, waypoint.getLocation().getYaw()); + Assertions.assertEquals(5, waypoint.getLocation().getPitch()); + Assertions.assertEquals("world", waypoint.getLocation().getWorld().getName()); + } + + // Test realistic situations + @Test + void testResearchChanges() throws InterruptedException { + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + PlayerProfile profile = TestUtilities.awaitProfile(player); + + // Unlock all researches + for (Research research : Slimefun.getRegistry().getResearches()) { + profile.setResearched(research, true); + } + + // Save the player data + LegacyStorage storage = new LegacyStorage(); + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + PlayerData assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(10, assertion.getResearches().size()); + for (int i = 0; i < 10; i++) { + Assertions.assertTrue(assertion.getResearches().contains(Slimefun.getRegistry().getResearches().get(i))); + } + + // Now let's change the data and save it again + profile.setResearched(Slimefun.getRegistry().getResearches().get(3), false); + + // Save the player data + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + System.out.println("update assertion"); + assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(9, assertion.getResearches().size()); + for (int i = 0; i < 10; i++) { + if (i != 3) { + Assertions.assertTrue(assertion.getResearches().contains(Slimefun.getRegistry().getResearches().get(i))); + } + } + } + + // Test realistic situations - when we fix the serialization issue + // @Test + // void testBackpackChanges() throws InterruptedException {} + + @Test + void testWaypointChanges() throws InterruptedException { + // Create mock world + World world = server.createWorld(WorldCreator.name("world").environment(Environment.NORMAL)); + + // Create a player file which we can load + UUID uuid = UUID.randomUUID(); + File playerFile = new File("data-storage/Slimefun/Players/" + uuid + ".yml"); + + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + PlayerProfile profile = TestUtilities.awaitProfile(player); + + profile.addWaypoint(new Waypoint( + player.getUniqueId(), + "test", + new Location(world, 1, 2, 3, 4, 5), + ChatColor.GREEN + "Test waypoint" + )); + + Waypoint test2 = new Waypoint( + player.getUniqueId(), + "test2", + new Location(world, 10, 20, 30, 40, 50), + ChatColor.GREEN + "Test 2 waypoint" + ); + profile.addWaypoint(test2); + + // Save the player data + LegacyStorage storage = new LegacyStorage(); + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + PlayerData assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(2, assertion.getWaypoints().size()); + + // Remove one + profile.removeWaypoint(test2); + + // Save the player data + storage.savePlayerData(uuid, profile.getPlayerData()); + + // Assert the file exists and data is correct + Assertions.assertTrue(playerFile.exists()); + assertion = storage.loadPlayerData(uuid); + Assertions.assertEquals(1, assertion.getWaypoints().size()); + } + + // Utils + private static void setupResearches() { + for (int i = 0; i < 10; i++) { + NamespacedKey key = new NamespacedKey(plugin, "test_" + i); + Research research = new Research(key, i, "Test " + i, 100); + research.register(); + } + } +} diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/test/TestUtilities.java b/src/test/java/io/github/thebusybiscuit/slimefun4/test/TestUtilities.java index 0f520306aa..56f517da20 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/test/TestUtilities.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/test/TestUtilities.java @@ -2,6 +2,7 @@ import static org.mockito.Mockito.when; +import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -30,6 +31,8 @@ public final class TestUtilities { + private static final Random random = new Random(); + private TestUtilities() {} @ParametersAreNonnullByDefault @@ -76,4 +79,14 @@ private TestUtilities() {} latch.await(2, TimeUnit.SECONDS); return ref.get(); } + + @ParametersAreNonnullByDefault + public static @Nonnull int randomInt() { + return random.nextInt(Integer.MAX_VALUE); + } + + @ParametersAreNonnullByDefault + public static @Nonnull int randomInt(int upperBound) { + return random.nextInt(upperBound); + } } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/test/mocks/MockProfile.java b/src/test/java/io/github/thebusybiscuit/slimefun4/test/mocks/MockProfile.java index 8afad75e12..9a7837d00a 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/test/mocks/MockProfile.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/test/mocks/MockProfile.java @@ -1,15 +1,22 @@ package io.github.thebusybiscuit.slimefun4.test.mocks; +import java.util.HashMap; +import java.util.Set; + import javax.annotation.Nonnull; import org.bukkit.OfflinePlayer; import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; +import io.github.thebusybiscuit.slimefun4.storage.data.PlayerData; public class MockProfile extends PlayerProfile { public MockProfile(@Nonnull OfflinePlayer p) { - super(p); + this(p, new PlayerData(Set.of(), new HashMap<>(), Set.of())); } + public MockProfile(@Nonnull OfflinePlayer p, @Nonnull PlayerData data) { + super(p, data); + } } diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestChatUtils.java b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestChatUtils.java index 80fa61aa83..794b02e4b5 100644 --- a/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestChatUtils.java +++ b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/TestChatUtils.java @@ -33,4 +33,14 @@ void testColorCodeRemoval() { Assertions.assertEquals(expected, ChatUtils.removeColorCodes(ChatColor.GREEN + "Hello " + ChatColor.RED + "world")); } + @Test + @DisplayName("Test ChatUtils.checkPlurality(...)") + void testPluralization() { + String input = "Banana"; + Assertions.assertThrows(IllegalArgumentException.class, () -> ChatUtils.checkPlurality(input, -1)); + Assertions.assertEquals("Bananas", ChatUtils.checkPlurality(input, 0)); + Assertions.assertEquals("Banana", ChatUtils.checkPlurality(input, 1)); + Assertions.assertEquals("Bananas", ChatUtils.checkPlurality(input, 2)); + } + }