diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..8fcf21cbc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,61 @@ +name: Report a bug +description: You found a bug? Let us know! +title: "[Bug]: " +labels: [ "bug", "needs triage" ] +projects: ["minestom/6"] +body: + - type: markdown + attributes: + value: | + Thank you for reporting a bug to Minestom! + Please fill out the information below to help us understand the issue. + - type: markdown + attributes: + value: | + Before filling in the form fields, please consider the following: + - Ensure that you are using the latest version of Minestom. + - Search for existing issues in the [issue tracker](https://github.com/Minestom/Minestom/issues) + - type: textarea + attributes: + label: Affected Version + description: | + The unique version of the used dependency. + It can be found out via the Git class or in the dependency. + validations: + required: true + - type: textarea + attributes: + label: Describe the bug + description: | + A clear and concise description of what the bug is. + If you have a screenshot of the bug, please attach it below. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce the bug + description: Tell us exactly how to reproduce the bug you are experiencing + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: false + - type: textarea + attributes: + label: Code sample + description: | + Please create a reproducible sample to show us the bug in action and attach it below between the lines with the backticks. + This helps us to verify that the bug is valid and prevents us from having to ask you for a sample later. + + Without this we will be unlikely to be able to progress on the issue, so + we'll regretfully have to close it. + + **Note**: Please do not upload screenshots of text. Instead, use code blocks + or the above mentioned ways to upload your code sample. + value: | + ```java + [Paste your code here] + ``` + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..4c6c0a05c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: Minestom Discord + url: https://discord.gg/minestom/ + about: Please join our Discord server if you have any questions or concerns. + icon: https://github.com/simple-icons/simple-icons/blob/develop/icons/discord.svg \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 000000000..5efef7995 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,34 @@ +name: Feature Request +description: Suggest an idea for this project +title: "[Feature]: " +labels: [ "enhancement" ] +projects: ["minestom/6"] +body: + - type: markdown + attributes: + value: | + Thank you for suggesting an idea to make Minestom better! + Please fill out the information below to help us understand your idea. + - type: textarea + attributes: + label: Is your feature request related to a problem? + description: Please give some context for this request. Why do you want it added? + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: Give us a clear description of what you want + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: markdown + attributes: + value: | + Before submitting your feature request, please make sure you have done the following: + - [ ] Searched for existing feature requests \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/performance.yml b/.github/ISSUE_TEMPLATE/performance.yml new file mode 100644 index 000000000..622d8ad02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance.yml @@ -0,0 +1,41 @@ +name: Performance Problem +description: Report a performance problem which is related to Minestom +labels: [ "needs triage", "performance" ] +projects: ["minestom/6"] +body: + - type: markdown + attributes: + value: | + Before creating an issue regarding to the performance, please reach out for support on our [Discord](https://discord.gg/minestom/) + or in the [Discussions](https://github.com/Minestom/Minestom/discussions)! + + **Please be aware: Performance issues can sometimes depend on your specific implementation and not on Minestom itself. If the situation is clear and it's not a problem with the project, we will close the issue without any comment.** + - type: input + attributes: + label: Used Minestom version + description: Which version of Minestom are you using? + placeholder: 1.0.0 + validations: + required: true + - type: textarea + attributes: + label: Describe the performance problem + description: If applicable, please describe your issue. + validations: + required: false + - type: textarea + attributes: + label: Other + description: | + Please include other helpful links below. + The more information we receive, the quicker and more effective we can be at finding the solution to the issue. + validations: + required: false + - type: markdown + attributes: + value: | + Before submitting your issue, please make sure you have done the following: + + 1. You are running the latest version of Minestom from [Release page](https://github.com/Minestom/Minestom) + 2. You searched for and ensured there isn't already an open issue regarding this + 3. Your version of Minecraft is supported by Minestom \ No newline at end of file diff --git a/.github/old-workflows/check-pr-style.yml b/.github/old-workflows/check-pr-style.yml deleted file mode 100644 index 9e0568f90..000000000 --- a/.github/old-workflows/check-pr-style.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Check PR code style - -on: - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 21 - uses: actions/setup-java@v1 - with: - java-version: 21 - - name: Run java checkstyle - uses: nikitasavinov/checkstyle-action@0.3.1 - with: - # Report level for reviewdog [info,warning,error] - level: info - # Reporter of reviewdog command [github-pr-check,github-pr-review] - reporter: github-pr-check - # Filtering for the reviewdog command [added,diff_context,file,nofilter]. - filter_mode: added - # Exit code for reviewdog when errors are found [true,false]. - fail_on_error: false - # Checkstyle config file - checkstyle_config: minestom_checks.xml - checkstyle_version: "8.42" diff --git a/.github/old-workflows/codeql-analysis.yml b/.github/old-workflows/codeql-analysis.yml deleted file mode 100644 index 2325a1cc2..000000000 --- a/.github/old-workflows/codeql-analysis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '27 19 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..c7d2916ba --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ +## Proposed changes + +Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. +If it fixes a bug or resolves a feature request, be sure to link to that issue. + +## Types of changes + +What types of changes does your code introduce to this project? +_Put an `x` in the boxes that apply_ + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation Update (if none of the other choices apply) + +## Checklist + +_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of +them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before +merging your code._ + +- [ ] I have read the [CONTRIBUTING.md](CONTRIBUTING.md) +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added necessary documentation (if appropriate) + +## Further comments + +If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you +did and what alternatives you considered, etc... \ No newline at end of file diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 000000000..d70d891b5 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,43 @@ +name: Build PR +on: [pull_request] +jobs: + build_pr: + name: Build PR + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v3 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build on ${{ matrix.os }} + run: ./gradlew test + publish: + name: Publish PR + if: contains(github.event.pull_request.labels.*.name, 'Publish Pull Request') && github.repository_owner == 'Minestom' + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - id: vars + run: echo "short_commit_hash=${GITHUB_SHA::10}" >> $GITHUB_OUTPUT + - name: Publish Artifacts + env: + MINESTOM_VERSION: ${{ github.head_ref }}-${{ steps.vars.outputs.short_commit_hash }} + MINESTOM_CHANNEL: snapshot + run: | + ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository + echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..0cea8b337 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build +on: + push: + branches: + - master +jobs: + Build: + name: Build and Publish + # Run on all label events (won't be duplicated) or all push events or on PR syncs not from the same repo + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v3 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + cache: gradle + java-version: 21 + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - id: vars + run: echo "short_commit_hash=${GITHUB_SHA::10}" >> $GITHUB_OUTPUT + - name: Publish to Central via Build + if: github.repository_owner == 'Minestom' + run: | + ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository + echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY + env: + MINESTOM_VERSION: ${{ steps.vars.outputs.short_commit_hash }} + MINESTOM_CHANNEL: release + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/close_invalid_prs.yml b/.github/workflows/close_invalid_prs.yml index 44c763b6d..bf9fa73d5 100644 --- a/.github/workflows/close_invalid_prs.yml +++ b/.github/workflows/close_invalid_prs.yml @@ -7,6 +7,7 @@ on: jobs: run: + name: Close invalid PRs if: | github.repository != github.event.pull_request.head.repo.full_name && ( diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml index 92d41fb98..0508616b1 100644 --- a/.github/workflows/javadoc.yml +++ b/.github/workflows/javadoc.yml @@ -6,9 +6,8 @@ on: jobs: build: - runs-on: ubuntu-latest - + name: Build and deploy Javadoc steps: - uses: actions/checkout@v2 - name: Set up JDK 21 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index 548cb3921..000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Build and test Minestom - -on: - pull_request: - branches: [ master ] - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 21 - uses: actions/setup-java@v2 - with: - distribution: 'zulu' - java-version: 21 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Setup gradle cache - uses: burrunan/gradle-cache-action@v1 - with: - save-generated-gradle-jars: false - save-local-build-cache: false - save-gradle-dependencies-cache: true - save-maven-dependencies-cache: true - # Ignore some of the paths when caching Maven Local repository - maven-local-ignore-paths: | - net/minestom/ - - name: Build Minestom - run: ./gradlew classes testClasses javadoc - - name: Run Minestom tests - run: ./gradlew test - publish: - runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.full_name == github.repository - env: - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }} - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 21 - uses: actions/setup-java@v2 - with: - java-version: '21' - distribution: 'temurin' - - name: Set outputs - id: vars - run: | - export ACTUAL=${{ github.event.pull_request.head.sha }} - echo "short_commit_hash=${ACTUAL::10}" >> $GITHUB_OUTPUT - - name: Publish to Sonatype - env: - MINESTOM_VERSION: ${{ github.head_ref }}-${{ steps.vars.outputs.short_commit_hash }} - MINESTOM_CHANNEL: snapshot - run: | - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository - echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/snapshot-deploy.yaml b/.github/workflows/snapshot-deploy.yaml deleted file mode 100644 index cf032b8da..000000000 --- a/.github/workflows/snapshot-deploy.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Gradle Publish to Maven Central - -on: - push: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - env: - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }} - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 21 - uses: actions/setup-java@v2 - with: - java-version: '21' - distribution: 'temurin' - - name: Set outputs - id: vars - run: | - echo "short_commit_hash=${GITHUB_SHA::10}" >> $GITHUB_OUTPUT - - name: Publish to Sonatype - env: - MINESTOM_VERSION: ${{ steps.vars.outputs.short_commit_hash }} - MINESTOM_CHANNEL: release - run: | - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository - echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/trigger-jitpack-build.yml b/.github/workflows/trigger-jitpack-build.yml deleted file mode 100644 index 8366c4f6f..000000000 --- a/.github/workflows/trigger-jitpack-build.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Trigger Jitpack Build -on: - push: - branches: [ master ] - - workflow_dispatch: -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Trigger Jitpack Build - run: curl "https://jitpack.io/com/github/Minestom/Minestom/${GITHUB_SHA:0:10}/build.log" diff --git a/code-generators/src/main/java/net/minestom/codegen/Generators.java b/code-generators/src/main/java/net/minestom/codegen/Generators.java index 3ac9a26e5..d251b5dec 100644 --- a/code-generators/src/main/java/net/minestom/codegen/Generators.java +++ b/code-generators/src/main/java/net/minestom/codegen/Generators.java @@ -52,6 +52,7 @@ public class Generators { generator.generate(resource("attributes.json"), "net.minestom.server.entity.attribute", "Attribute", "AttributeImpl", "Attributes"); generator.generate(resource("feature_flags.json"), "net.minestom.server", "FeatureFlag", "FeatureFlagImpl", "FeatureFlags"); generator.generate(resource("villager_professions.json"), "net.minestom.server.entity", "VillagerProfession", "VillagerProfessionImpl", "VillagerProfessions"); + generator.generate(resource("game_events.json"), "net.minestom.server.game", "GameEvent", "GameEventImpl", "GameEvents"); // Dynamic registries diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index 6fd80f58d..71e1623c4 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -141,7 +141,6 @@ public class PlayerInit { inventory.addItemStack(getFoodItem(10000)); inventory.addItemStack(getFoodItem(Integer.MAX_VALUE)); - if (event.isFirstSpawn()) { event.getPlayer().sendNotification(new Notification( Component.text("Welcome!"), diff --git a/src/autogenerated/java/net/minestom/server/game/GameEvents.java b/src/autogenerated/java/net/minestom/server/game/GameEvents.java new file mode 100644 index 000000000..44e09b862 --- /dev/null +++ b/src/autogenerated/java/net/minestom/server/game/GameEvents.java @@ -0,0 +1,127 @@ +package net.minestom.server.game; + +/** + * Code autogenerated, do not edit! + */ +@SuppressWarnings("unused") +interface GameEvents { + GameEvent BLOCK_ACTIVATE = GameEventImpl.get("minecraft:block_activate"); + + GameEvent BLOCK_ATTACH = GameEventImpl.get("minecraft:block_attach"); + + GameEvent BLOCK_CHANGE = GameEventImpl.get("minecraft:block_change"); + + GameEvent BLOCK_CLOSE = GameEventImpl.get("minecraft:block_close"); + + GameEvent BLOCK_DEACTIVATE = GameEventImpl.get("minecraft:block_deactivate"); + + GameEvent BLOCK_DESTROY = GameEventImpl.get("minecraft:block_destroy"); + + GameEvent BLOCK_DETACH = GameEventImpl.get("minecraft:block_detach"); + + GameEvent BLOCK_OPEN = GameEventImpl.get("minecraft:block_open"); + + GameEvent BLOCK_PLACE = GameEventImpl.get("minecraft:block_place"); + + GameEvent CONTAINER_CLOSE = GameEventImpl.get("minecraft:container_close"); + + GameEvent CONTAINER_OPEN = GameEventImpl.get("minecraft:container_open"); + + GameEvent DRINK = GameEventImpl.get("minecraft:drink"); + + GameEvent EAT = GameEventImpl.get("minecraft:eat"); + + GameEvent ELYTRA_GLIDE = GameEventImpl.get("minecraft:elytra_glide"); + + GameEvent ENTITY_DAMAGE = GameEventImpl.get("minecraft:entity_damage"); + + GameEvent ENTITY_DIE = GameEventImpl.get("minecraft:entity_die"); + + GameEvent ENTITY_DISMOUNT = GameEventImpl.get("minecraft:entity_dismount"); + + GameEvent ENTITY_INTERACT = GameEventImpl.get("minecraft:entity_interact"); + + GameEvent ENTITY_MOUNT = GameEventImpl.get("minecraft:entity_mount"); + + GameEvent ENTITY_PLACE = GameEventImpl.get("minecraft:entity_place"); + + GameEvent ENTITY_ACTION = GameEventImpl.get("minecraft:entity_action"); + + GameEvent EQUIP = GameEventImpl.get("minecraft:equip"); + + GameEvent EXPLODE = GameEventImpl.get("minecraft:explode"); + + GameEvent FLAP = GameEventImpl.get("minecraft:flap"); + + GameEvent FLUID_PICKUP = GameEventImpl.get("minecraft:fluid_pickup"); + + GameEvent FLUID_PLACE = GameEventImpl.get("minecraft:fluid_place"); + + GameEvent HIT_GROUND = GameEventImpl.get("minecraft:hit_ground"); + + GameEvent INSTRUMENT_PLAY = GameEventImpl.get("minecraft:instrument_play"); + + GameEvent ITEM_INTERACT_FINISH = GameEventImpl.get("minecraft:item_interact_finish"); + + GameEvent ITEM_INTERACT_START = GameEventImpl.get("minecraft:item_interact_start"); + + GameEvent JUKEBOX_PLAY = GameEventImpl.get("minecraft:jukebox_play"); + + GameEvent JUKEBOX_STOP_PLAY = GameEventImpl.get("minecraft:jukebox_stop_play"); + + GameEvent LIGHTNING_STRIKE = GameEventImpl.get("minecraft:lightning_strike"); + + GameEvent NOTE_BLOCK_PLAY = GameEventImpl.get("minecraft:note_block_play"); + + GameEvent PRIME_FUSE = GameEventImpl.get("minecraft:prime_fuse"); + + GameEvent PROJECTILE_LAND = GameEventImpl.get("minecraft:projectile_land"); + + GameEvent PROJECTILE_SHOOT = GameEventImpl.get("minecraft:projectile_shoot"); + + GameEvent SCULK_SENSOR_TENDRILS_CLICKING = GameEventImpl.get("minecraft:sculk_sensor_tendrils_clicking"); + + GameEvent SHEAR = GameEventImpl.get("minecraft:shear"); + + GameEvent SHRIEK = GameEventImpl.get("minecraft:shriek"); + + GameEvent SPLASH = GameEventImpl.get("minecraft:splash"); + + GameEvent STEP = GameEventImpl.get("minecraft:step"); + + GameEvent SWIM = GameEventImpl.get("minecraft:swim"); + + GameEvent TELEPORT = GameEventImpl.get("minecraft:teleport"); + + GameEvent UNEQUIP = GameEventImpl.get("minecraft:unequip"); + + GameEvent RESONATE_1 = GameEventImpl.get("minecraft:resonate_1"); + + GameEvent RESONATE_2 = GameEventImpl.get("minecraft:resonate_2"); + + GameEvent RESONATE_3 = GameEventImpl.get("minecraft:resonate_3"); + + GameEvent RESONATE_4 = GameEventImpl.get("minecraft:resonate_4"); + + GameEvent RESONATE_5 = GameEventImpl.get("minecraft:resonate_5"); + + GameEvent RESONATE_6 = GameEventImpl.get("minecraft:resonate_6"); + + GameEvent RESONATE_7 = GameEventImpl.get("minecraft:resonate_7"); + + GameEvent RESONATE_8 = GameEventImpl.get("minecraft:resonate_8"); + + GameEvent RESONATE_9 = GameEventImpl.get("minecraft:resonate_9"); + + GameEvent RESONATE_10 = GameEventImpl.get("minecraft:resonate_10"); + + GameEvent RESONATE_11 = GameEventImpl.get("minecraft:resonate_11"); + + GameEvent RESONATE_12 = GameEventImpl.get("minecraft:resonate_12"); + + GameEvent RESONATE_13 = GameEventImpl.get("minecraft:resonate_13"); + + GameEvent RESONATE_14 = GameEventImpl.get("minecraft:resonate_14"); + + GameEvent RESONATE_15 = GameEventImpl.get("minecraft:resonate_15"); +} diff --git a/src/main/java/net/minestom/server/ServerFlag.java b/src/main/java/net/minestom/server/ServerFlag.java index 5dcd1dcb2..653c0e09c 100644 --- a/src/main/java/net/minestom/server/ServerFlag.java +++ b/src/main/java/net/minestom/server/ServerFlag.java @@ -27,6 +27,7 @@ public final class ServerFlag { public static final int PLAYER_PACKET_QUEUE_SIZE = intProperty("minestom.packet-queue-size", 1000); public static final long KEEP_ALIVE_DELAY = longProperty("minestom.keep-alive-delay", 10_000); public static final long KEEP_ALIVE_KICK = longProperty("minestom.keep-alive-kick", 15_000); + public static final int PLAYER_CHUNK_UPDATE_LIMITER_HISTORY_SIZE = intProperty("minestom.player.chunk-update-limiter-history-size", 5, 0, Integer.MAX_VALUE); // Network buffers public static final int MAX_PACKET_SIZE = intProperty("minestom.max-packet-size", 2_097_151); // 3 bytes var-int @@ -98,8 +99,19 @@ public final class ServerFlag { return System.getProperty(name); } + private static int intProperty(String name, int defaultValue, int minValue, int maxValue) { + int value = Integer.getInteger(name, defaultValue); + if (value < minValue || value > maxValue) { + throw new IllegalArgumentException(String.format( + "Property '%s' value must be in range [%d..%d] but was %d", + name, minValue, maxValue, value + )); + } + return value; + } + private static int intProperty(String name, int defaultValue) { - return Integer.getInteger(name, defaultValue); + return intProperty(name, defaultValue, Integer.MIN_VALUE, Integer.MAX_VALUE); } private static long longProperty(String name, long defaultValue) { diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index b68383401..27a408143 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -5,6 +5,7 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.EntityPose; import net.minestom.server.instance.block.BlockFace; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,11 +22,11 @@ public record BoundingBox(Vec relativeStart, Vec relativeEnd) implements Shape { final static BoundingBox ZERO = new BoundingBox(Vec.ZERO, Vec.ZERO); public BoundingBox(double width, double height, double depth, Point offset) { - this(new Vec(-width / 2.0, 0.0, -depth / 2.0).add(offset), new Vec(width / 2.0, height, depth / 2.0).add(offset)); + this(Vec.fromPoint(offset), new Vec(width, height, depth).add(offset)); } public BoundingBox(double width, double height, double depth) { - this(width, height, depth, Vec.ZERO); + this(width, height, depth, new Vec(-width / 2, 0, -depth / 2)); } @Override @@ -94,6 +95,41 @@ public record BoundingBox(Vec relativeStart, Vec relativeEnd) implements Shape { return new BoundingBox(width(), height(), depth(), offset); } + /** + * Creates a new {@link BoundingBox} with an expanded size from its center in every plane. + *

+ * Equivalent to an expansion and an offset where the point is the three-axis offset. + * Particularly useful when you already use centered and aligned minY=0 position. + * + * @param x the X offset, this will be applied on both sides + * @param y the Y offset, this will be applied on both sides + * @param z the Z offset, this will be applied on both sides + * @return a new {@link BoundingBox} expanded and centered from the original minY + */ + @Contract(pure = true) + public @NotNull BoundingBox grow(double x, double y, double z) { + final double newWidth = width() + x, newDepth = depth() + z; + final Vec centerOffset = new Vec(-newWidth / 2, minY() - y / 2, -newDepth / 2); + return new BoundingBox(newWidth, height() + y, newDepth, centerOffset); + } + + /** + * Creates a new {@link BoundingBox} with an expanded size from its center in every plane. + *

+ * Equivalent to a double expansion and an offset where the point is the three-axis offset. + * Particularly useful when you already use centered and aligned minY=0 position. + * + * @param x the X offset, this will be applied on both sides + * @param y the Y offset, this will be applied on both sides + * @param z the Z offset, this will be applied on both sides + * @return a new {@link BoundingBox} expanded and centered from the original minY + */ + @Contract(pure = true) + public @NotNull BoundingBox growSymmetrically(double x, double y, double z) { + // Double all amounts to make it symmetric conformance to xyz + return grow(x * 2, y * 2, z * 2); + } + public double width() { return relativeEnd.x() - relativeStart.x(); } diff --git a/src/main/java/net/minestom/server/command/CommandParserImpl.java b/src/main/java/net/minestom/server/command/CommandParserImpl.java index 07c3125f4..7f93b2115 100644 --- a/src/main/java/net/minestom/server/command/CommandParserImpl.java +++ b/src/main/java/net/minestom/server/command/CommandParserImpl.java @@ -132,8 +132,8 @@ final class CommandParserImpl implements CommandParser { int start = reader.cursor(); if (reader.hasRemaining()) { - ArgumentResult result = parseArgument(sender, argument, reader); SuggestionCallback suggestionCallback = argument.getSuggestionCallback(); + ArgumentResult result = parseArgument(sender, argument, reader); NodeResult nodeResult = new NodeResult(node, chain, (ArgumentResult) result, suggestionCallback); chain.append(nodeResult); if (suggestionCallback != null) chain.suggestionCallback = suggestionCallback; @@ -200,12 +200,27 @@ final class CommandParserImpl implements CommandParser { if (reader.hasRemaining()) { // Trailing data is a syntax error - return new NodeResult( - node, + // Can get to here if there's a default executor even if the user is still typing the command + // So let's supply the next argument's suggestion callback if it exists + Node returnNode = node; + SuggestionCallback suggestionCallback = argument.getSuggestionCallback(); + List nextNodes = node.next(); + if (!nextNodes.isEmpty()) { + returnNode = nextNodes.getFirst(); + suggestionCallback = returnNode.argument().getSuggestionCallback(); + } + NodeResult nodeResult = new NodeResult( + returnNode, chain, new ArgumentResult.SyntaxError<>("Command has trailing data", "", -1), - argument.getSuggestionCallback() + suggestionCallback ); + chain.suggestionCallback = suggestionCallback; + // prevent duplicates from being added (Fixes CommandParseTest#singleCommandWithMultipleSyntax() failure) + if (chain.getArgs().stream().noneMatch(arg -> arg.getId().equals(argument.getId()))) { + chain.append(nodeResult); + } + return nodeResult; } // Command was successful! diff --git a/src/main/java/net/minestom/server/coordinate/BlockVec.java b/src/main/java/net/minestom/server/coordinate/BlockVec.java index f8df8214b..e8be63043 100644 --- a/src/main/java/net/minestom/server/coordinate/BlockVec.java +++ b/src/main/java/net/minestom/server/coordinate/BlockVec.java @@ -28,6 +28,21 @@ public record BlockVec(double x, double y, double z) implements Point { this(point.x(), point.y(), point.z()); } + @Override + public int blockX() { + return (int) x; + } + + @Override + public int blockY() { + return (int) y; + } + + @Override + public int blockZ() { + return (int) z; + } + @Override public @NotNull Point withX(@NotNull DoubleUnaryOperator operator) { return new Vec(operator.applyAsDouble(x), y, z); diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 665562783..0dbf8fcaa 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -46,6 +46,7 @@ import net.minestom.server.timer.Schedulable; import net.minestom.server.timer.Scheduler; import net.minestom.server.timer.TaskSchedule; import net.minestom.server.utils.ArrayUtils; +import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.PacketViewableUtils; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.block.BlockIterator; @@ -79,6 +80,10 @@ import java.util.function.UnaryOperator; */ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler, Taggable, HoverEventSource, Sound.Emitter, Shape, AcquirableSource { + // This is somewhat arbitrary, but we don't want to hit the max int ever because it is very easy to + // overflow while working with a position at the max int (for example, looping over a bounding box) + private static final int MAX_COORDINATE = 2_000_000_000; + private static final AtomicInteger LAST_ENTITY_ID = new AtomicInteger(); // Certain entities should only have their position packets sent during synchronization @@ -96,7 +101,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev protected Instance instance; protected Chunk currentChunk; - protected Pos position; + protected Pos position; // Should be updated by setPositionInternal only. protected Pos previousPosition; protected Pos lastSyncedPosition; protected boolean onGround; @@ -202,6 +207,19 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev this(entityType, UUID.randomUUID()); } + protected void setPositionInternal(@NotNull Pos newPosition) { + if (newPosition.x() >= MAX_COORDINATE || newPosition.x() <= -MAX_COORDINATE || + newPosition.y() >= MAX_COORDINATE || newPosition.y() <= -MAX_COORDINATE || + newPosition.z() >= MAX_COORDINATE || newPosition.z() <= -MAX_COORDINATE) { + newPosition = newPosition.withCoord( + MathUtils.clamp(newPosition.x(), -MAX_COORDINATE, MAX_COORDINATE), + MathUtils.clamp(newPosition.y(), -MAX_COORDINATE, MAX_COORDINATE), + MathUtils.clamp(newPosition.z(), -MAX_COORDINATE, MAX_COORDINATE) + ); + } + this.position = newPosition; + } + /** * Schedules a task to be run during the next entity tick. * @@ -325,7 +343,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev final Runnable endCallback = () -> { this.previousPosition = this.position; - this.position = globalPosition; + setPositionInternal(globalPosition); this.velocity = globalVelocity; refreshCoordinate(globalPosition); if (this instanceof Player player) player.synchronizePositionAfterTeleport(position, velocity, flags, shouldConfirm); @@ -356,7 +374,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev public void setView(float yaw, float pitch) { final Pos currentPosition = this.position; if (currentPosition.sameView(yaw, pitch)) return; - this.position = currentPosition.withView(yaw, pitch); + setPositionInternal(currentPosition.withView(yaw, pitch)); synchronizeView(); } @@ -784,7 +802,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev if (previousInstance != null) removeFromInstance(previousInstance); this.isActive = true; - this.position = spawnPosition; + setPositionInternal(spawnPosition); this.previousPosition = spawnPosition; this.lastSyncedPosition = spawnPosition; this.previousPhysicsResult = null; @@ -1236,7 +1254,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev final var previousPosition = this.position; final Pos position = ignoreView ? previousPosition.withCoord(newPosition) : newPosition; if (position.equals(lastSyncedPosition)) return; - this.position = position; + setPositionInternal(position); this.previousPosition = previousPosition; if (!position.samePoint(previousPosition)) refreshCoordinate(position); if (nextSynchronizationTick <= ticks + 1 || !sendPackets) { @@ -1297,7 +1315,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev final Pos newPassengerPos = oldPassengerPos.withCoord(newPosition.x(), newPosition.y() + EntityUtils.getPassengerHeightOffset(this, passenger), newPosition.z()); - passenger.position = newPassengerPos; + passenger.setPositionInternal(newPassengerPos); passenger.previousPosition = oldPassengerPos; passenger.refreshCoordinate(newPassengerPos); } @@ -1474,7 +1492,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev this.removed = true; if (!permanent) { // Reset some state to be ready for re-use - this.position = Pos.ZERO; + setPositionInternal(Pos.ZERO); this.previousPosition = Pos.ZERO; this.lastSyncedPosition = Pos.ZERO; } diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index eb2f9d3ed..c23c1ea8f 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -191,7 +191,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou // Game state (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Game_Event) private boolean enableRespawnScreen; - private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(6); + private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(ServerFlag.PLAYER_CHUNK_UPDATE_LIMITER_HISTORY_SIZE); // Experience orb pickup protected Cooldown experiencePickupCooldown = new Cooldown(Duration.of(10, TimeUnit.SERVER_TICK)); diff --git a/src/main/java/net/minestom/server/entity/metadata/animal/tameable/WolfMeta.java b/src/main/java/net/minestom/server/entity/metadata/animal/tameable/WolfMeta.java index f6f91f205..3934bfe44 100644 --- a/src/main/java/net/minestom/server/entity/metadata/animal/tameable/WolfMeta.java +++ b/src/main/java/net/minestom/server/entity/metadata/animal/tameable/WolfMeta.java @@ -58,7 +58,7 @@ public class WolfMeta extends TameableAnimalMeta { } public sealed interface Variant extends ProtocolObject, WolfVariants permits VariantImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::wolfVariant); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::wolfVariant, true); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::wolfVariant); static @NotNull Variant create( diff --git a/src/main/java/net/minestom/server/entity/metadata/other/PaintingMeta.java b/src/main/java/net/minestom/server/entity/metadata/other/PaintingMeta.java index a26b84ffc..1d47aae41 100644 --- a/src/main/java/net/minestom/server/entity/metadata/other/PaintingMeta.java +++ b/src/main/java/net/minestom/server/entity/metadata/other/PaintingMeta.java @@ -79,7 +79,7 @@ public class PaintingMeta extends EntityMeta implements ObjectDataProvider { } public sealed interface Variant extends ProtocolObject, PaintingVariants permits VariantImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::paintingVariant); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::paintingVariant, true); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::paintingVariant); static @NotNull Variant create( diff --git a/src/main/java/net/minestom/server/entity/pathfinding/PNode.java b/src/main/java/net/minestom/server/entity/pathfinding/PNode.java index 919a274ad..606928931 100644 --- a/src/main/java/net/minestom/server/entity/pathfinding/PNode.java +++ b/src/main/java/net/minestom/server/entity/pathfinding/PNode.java @@ -95,7 +95,7 @@ public class PNode { } @ApiStatus.Internal - @NotNull Type getType() { + public @NotNull Type getType() { return type; } diff --git a/src/main/java/net/minestom/server/event/EventNodeImpl.java b/src/main/java/net/minestom/server/event/EventNodeImpl.java index e6710669f..c7af4fec8 100644 --- a/src/main/java/net/minestom/server/event/EventNodeImpl.java +++ b/src/main/java/net/minestom/server/event/EventNodeImpl.java @@ -292,10 +292,21 @@ non-sealed class EventNodeImpl implements EventNode { aClass -> new Handle<>((Class) aClass)); handle.invalidate(); }); + invalidateRecursiveSuperclasses(eventClass); final EventNodeImpl parent = this.parent; if (parent != null) parent.invalidateEvent(eventClass); } + private void invalidateRecursiveSuperclasses(@NotNull Class eventClass) { + if (RecursiveEvent.class.isAssignableFrom(eventClass)) { + for (var cls : this.handleMap.keySet()) { + if (eventClass.isAssignableFrom(cls)) { + this.handleMap.get(cls).invalidate(); + } + } + } + } + private ListenerEntry getEntry(Class type) { return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>()); } diff --git a/src/main/java/net/minestom/server/game/GameEvent.java b/src/main/java/net/minestom/server/game/GameEvent.java new file mode 100644 index 000000000..a2853834a --- /dev/null +++ b/src/main/java/net/minestom/server/game/GameEvent.java @@ -0,0 +1,66 @@ +package net.minestom.server.game; + + +import net.minestom.server.registry.Registry; +import net.minestom.server.registry.StaticProtocolObject; +import net.minestom.server.utils.NamespaceID; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * Represents a game event. + * Used for a wide variety of events, from weather to bed use to game mode to demo messages. + */ +public sealed interface GameEvent extends StaticProtocolObject permits GameEventImpl { + + /** + * Returns the game event registry. + * + * @return the game event registry or null if not found + */ + @Contract(pure = true) + @Nullable + Registry.GameEventEntry registry(); + + /** + * Gets the namespace ID of this game event. + * + * @return the namespace ID + */ + @Override + @NotNull + NamespaceID namespace(); + + /** + * Gets the game events from the registry. + * + * @return the game events + */ + static @NotNull Collection<@NotNull GameEvent> values() { + return GameEventImpl.values(); + } + + /** + * Gets a game event by its namespace ID. + * + * @param namespaceID the namespace ID + * @return the game event or null if not found + */ + static @Nullable GameEvent fromNamespaceId(@NotNull String namespaceID) { + return GameEventImpl.getSafe(namespaceID); + } + + /** + * Gets a game event by its namespace ID. + * + * @param namespaceID the namespace ID + * @return the game event or null if not found + */ + static @Nullable GameEvent fromNamespaceId(@NotNull NamespaceID namespaceID) { + return fromNamespaceId(namespaceID.asString()); + } + +} diff --git a/src/main/java/net/minestom/server/game/GameEventImpl.java b/src/main/java/net/minestom/server/game/GameEventImpl.java new file mode 100644 index 000000000..d07ab20d0 --- /dev/null +++ b/src/main/java/net/minestom/server/game/GameEventImpl.java @@ -0,0 +1,65 @@ +package net.minestom.server.game; + + +import net.minestom.server.registry.Registry; +import net.minestom.server.utils.NamespaceID; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Represents a game event implementation. + * Used for a wide variety of events, from weather to bed use to game mode to demo messages. + */ +record GameEventImpl(Registry.GameEventEntry registry, NamespaceID namespace, int id) implements GameEvent { + private static final Registry.Container CONTAINER = Registry.createStaticContainer(Registry.Resource.GAME_EVENTS, GameEventImpl::createImpl); + + /** + * Creates a new {@link GameEventImpl} with the given namespace and properties. + * + * @param namespace the namespace + * @param properties the properties + * @return a new {@link GameEventImpl} + */ + private static GameEventImpl createImpl(String namespace, Registry.Properties properties) { + return new GameEventImpl(Registry.gameEventEntry(namespace, properties)); + } + + /** + * Creates a new {@link GameEventImpl} with the given registry. + * + * @param registry the registry + */ + private GameEventImpl(Registry.GameEventEntry registry) { + this(registry, registry.namespace(), registry.main().getInt("id")); + } + + /** + * Gets the game events from the registry. + * + * @return the game events + */ + static Collection values() { + return CONTAINER.values(); + } + + /** + * Gets a game event by its namespace ID. + * + * @param namespace the namespace ID + * @return the game event or null if not found + */ + public static GameEvent get(@NotNull String namespace) { + return CONTAINER.get(namespace); + } + + /** + * Gets a game event by its namespace ID. + * + * @param namespace the namespace ID + * @return the game event or null if not found + */ + static GameEvent getSafe(@NotNull String namespace) { + return CONTAINER.getSafe(namespace); + } +} \ No newline at end of file diff --git a/src/main/java/net/minestom/server/gamedata/tags/Tag.java b/src/main/java/net/minestom/server/gamedata/tags/Tag.java index a6c321232..7783fef32 100644 --- a/src/main/java/net/minestom/server/gamedata/tags/Tag.java +++ b/src/main/java/net/minestom/server/gamedata/tags/Tag.java @@ -2,8 +2,8 @@ package net.minestom.server.gamedata.tags; import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; -import net.minestom.server.MinecraftServer; import net.minestom.server.entity.EntityType; +import net.minestom.server.game.GameEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.item.Material; import net.minestom.server.registry.*; @@ -14,10 +14,9 @@ import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.HashSet; -import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; -import java.util.function.Function; /** * Represents a group of items, blocks, fluids, entity types or function. @@ -92,32 +91,35 @@ public final class Tag implements ProtocolObject, Keyed { public enum BasicType { BLOCKS("minecraft:block", Registry.Resource.BLOCK_TAGS, - (name, registries) -> Objects.requireNonNull(Block.fromNamespaceId(name)).id()), + (blockName, registries) -> Optional.ofNullable(Block.fromNamespaceId(blockName)).map(Block::id)), ITEMS("minecraft:item", Registry.Resource.ITEM_TAGS, - (name, registries) -> Objects.requireNonNull(Material.fromNamespaceId(name)).id()), + (itemName, registries) -> Optional.ofNullable(Material.fromNamespaceId(itemName)).map(Material::id)), FLUIDS("minecraft:fluid", Registry.Resource.FLUID_TAGS, - (name, registries) -> FluidRegistries.getFluid(name).ordinal()), + (name, registries) -> Optional.of(name).map(FluidRegistries::getFluid).map(Enum::ordinal)), ENTITY_TYPES("minecraft:entity_type", Registry.Resource.ENTITY_TYPE_TAGS, - (name, registries) -> Objects.requireNonNull(EntityType.fromNamespaceId(name)).id()), + (entityName, registries) -> Optional.ofNullable(EntityType.fromNamespaceId(entityName)).map(EntityType::id)), GAME_EVENTS("minecraft:game_event", Registry.Resource.GAMEPLAY_TAGS, - (name, registries) -> FluidRegistries.getFluid(name).ordinal()), + (eventName, registries) -> Optional.ofNullable(GameEvent.fromNamespaceId(eventName)).map(GameEvent::id)), SOUND_EVENTS("minecraft:sound_event", null, null), // Seems not to be included in server data POTION_EFFECTS("minecraft:potion_effect", null, null), // Seems not to be included in server data - //todo this is cursed. it does not update as the registry changes. Fix later. ENCHANTMENTS("minecraft:enchantment", Registry.Resource.ENCHANTMENT_TAGS, - (name, registries) -> registries.enchantment().getId(DynamicRegistry.Key.of(name))), + (name, registries) -> Optional.of(DynamicRegistry.Key.of(name)) + .map(DynamicRegistry.Key::namespace) + .map(registries.enchantment()::getId)), BIOMES("minecraft:worldgen/biome", Registry.Resource.BIOME_TAGS, - (name, registries) -> registries.biome().getId(DynamicRegistry.Key.of(name))); + (name, registries) -> Optional.of(DynamicRegistry.Key.of(name)) + .map(DynamicRegistry.Key::namespace) + .map(registries.biome()::getId)); - private final static BasicType[] VALUES = values(); + private static final BasicType[] VALUES = values(); private final String identifier; private final Registry.Resource resource; - private final BiFunction function; + private final BiFunction> function; BasicType(@NotNull String identifier, @Nullable Registry.Resource resource, - @Nullable BiFunction function) { + @Nullable BiFunction> function) { this.identifier = identifier; this.resource = resource; this.function = function; @@ -131,7 +133,7 @@ public final class Tag implements ProtocolObject, Keyed { return resource; } - public BiFunction getFunction() { + public BiFunction> getFunction() { return function; } diff --git a/src/main/java/net/minestom/server/gamedata/tags/TagManager.java b/src/main/java/net/minestom/server/gamedata/tags/TagManager.java index 81b0ba01c..f5f9025db 100644 --- a/src/main/java/net/minestom/server/gamedata/tags/TagManager.java +++ b/src/main/java/net/minestom/server/gamedata/tags/TagManager.java @@ -30,8 +30,8 @@ public final class TagManager { public @Nullable Tag getTag(Tag.BasicType type, String namespace) { final var tags = tagMap.get(type); - for (var tag : tags) { - if (tag.getName().asString().equals(namespace)) + for (final var tag : tags) { + if (tag.name().equals(namespace)) return tag; } return null; @@ -46,10 +46,10 @@ public final class TagManager { for (Map.Entry> entry : tagMap.entrySet()) { final Tag.BasicType type = entry.getKey(); final String registry = type.getIdentifier(); - List tags = new ArrayList<>(); - for (Tag tag : entry.getValue()) { - final String identifier = tag.getName().asString(); - final int[] values = tag.getValues().stream().mapToInt(value -> type.getFunction().apply(value.asString(), registries)).toArray(); + final List tags = new ArrayList<>(); + for (final Tag tag : entry.getValue()) { + final String identifier = tag.name(); + final int[] values = tag.getValues().stream().mapToInt(value -> type.getFunction().apply(value.asString(), registries).orElse(null)).filter(Objects::nonNull).toArray(); tags.add(new TagsPacket.Tag(identifier, values)); } registryList.add(new TagsPacket.Registry(registry, tags)); diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index dc89d31e3..75eb2db8a 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -211,7 +211,7 @@ public class DynamicChunk extends Chunk { final Section section = getSectionAt(y); final int blockStateId = section.blockPalette() .get(globalToSectionRelative(x), globalToSectionRelative(y), globalToSectionRelative(z)); - return Objects.requireNonNullElse(Block.fromStateId((short) blockStateId), Block.AIR); + return Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR); } @Override diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index df2ae7b68..0358848fe 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -103,7 +103,7 @@ public abstract class Instance implements Block.Getter, Block.Setter, private final ChunkCache blockRetriever = new ChunkCache(this, null, null); // the uuid of this instance - protected UUID uniqueId; + protected UUID uuid; // instance custom data protected TagHandler tagHandler = TagHandler.newHandler(); @@ -119,31 +119,31 @@ public abstract class Instance implements Block.Getter, Block.Setter, /** * Creates a new instance. * - * @param uniqueId the {@link UUID} of the instance + * @param uuid the {@link UUID} of the instance * @param dimensionType the {@link DimensionType} of the instance */ - public Instance(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType) { - this(uniqueId, dimensionType, dimensionType.namespace()); + public Instance(@NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType) { + this(uuid, dimensionType, dimensionType.namespace()); } /** * Creates a new instance. * - * @param uniqueId the {@link UUID} of the instance + * @param uuid the {@link UUID} of the instance * @param dimensionType the {@link DimensionType} of the instance */ - public Instance(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType, @NotNull NamespaceID dimensionName) { - this(MinecraftServer.getDimensionTypeRegistry(), uniqueId, dimensionType, dimensionName); + public Instance(@NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType, @NotNull NamespaceID dimensionName) { + this(MinecraftServer.getDimensionTypeRegistry(), uuid, dimensionType, dimensionName); } /** * Creates a new instance. * - * @param uniqueId the {@link UUID} of the instance + * @param uuid the {@link UUID} of the instance * @param dimensionType the {@link DimensionType} of the instance */ - public Instance(@NotNull DynamicRegistry dimensionTypeRegistry, @NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType, @NotNull NamespaceID dimensionName) { - this.uniqueId = uniqueId; + public Instance(@NotNull DynamicRegistry dimensionTypeRegistry, @NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType, @NotNull NamespaceID dimensionName) { + this.uuid = uuid; this.dimensionType = dimensionType; this.cachedDimensionType = dimensionTypeRegistry.get(dimensionType); Check.argCondition(cachedDimensionType == null, "The dimension " + dimensionType + " is not registered! Please add it to the registry (`MinecraftServer.getDimensionTypeRegistry().registry(dimensionType)`)."); @@ -153,7 +153,7 @@ public abstract class Instance implements Block.Getter, Block.Setter, targetBorderDiameter = this.worldBorder.diameter(); this.pointers = Pointers.builder() - .withDynamic(Identity.UUID, this::getUniqueId) + .withDynamic(Identity.UUID, this::getUuid) .build(); final ServerProcess process = MinecraftServer.process(); @@ -750,8 +750,19 @@ public abstract class Instance implements Block.Getter, Block.Setter, * * @return the instance unique id */ + public @NotNull UUID getUuid() { + return uuid; + } + + /** + * Gets the instance unique id. + * + * @return the instance unique id + * @deprecated Replace with {@link Instance#getUuid()} + */ + @Deprecated(forRemoval = true) public @NotNull UUID getUniqueId() { - return uniqueId; + return uuid; } /** diff --git a/src/main/java/net/minestom/server/instance/InstanceContainer.java b/src/main/java/net/minestom/server/instance/InstanceContainer.java index a1ebbf950..84b2a6af8 100644 --- a/src/main/java/net/minestom/server/instance/InstanceContainer.java +++ b/src/main/java/net/minestom/server/instance/InstanceContainer.java @@ -89,30 +89,30 @@ public class InstanceContainer extends Instance { protected InstanceContainer srcInstance; // only present if this instance has been created using a copy private long lastBlockChangeTime; // Time at which the last block change happened (#setBlock) - public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType) { - this(uniqueId, dimensionType, null, dimensionType.namespace()); + public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType) { + this(uuid, dimensionType, null, dimensionType.namespace()); } - public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType, @NotNull NamespaceID dimensionName) { - this(uniqueId, dimensionType, null, dimensionName); + public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType, @NotNull NamespaceID dimensionName) { + this(uuid, dimensionType, null, dimensionName); } - public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType, @Nullable IChunkLoader loader) { - this(uniqueId, dimensionType, loader, dimensionType.namespace()); + public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType, @Nullable IChunkLoader loader) { + this(uuid, dimensionType, loader, dimensionType.namespace()); } - public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key dimensionType, @Nullable IChunkLoader loader, @NotNull NamespaceID dimensionName) { - this(MinecraftServer.getDimensionTypeRegistry(), uniqueId, dimensionType, loader, dimensionName); + public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType, @Nullable IChunkLoader loader, @NotNull NamespaceID dimensionName) { + this(MinecraftServer.getDimensionTypeRegistry(), uuid, dimensionType, loader, dimensionName); } public InstanceContainer( @NotNull DynamicRegistry dimensionTypeRegistry, - @NotNull UUID uniqueId, + @NotNull UUID uuid, @NotNull DynamicRegistry.Key dimensionType, @Nullable IChunkLoader loader, @NotNull NamespaceID dimensionName ) { - super(dimensionTypeRegistry, uniqueId, dimensionType, dimensionName); + super(dimensionTypeRegistry, uuid, dimensionType, dimensionName); setChunkSupplier(DynamicChunk::new); setChunkLoader(Objects.requireNonNullElse(loader, DEFAULT_LOADER)); this.chunkLoader.loadInstance(this); diff --git a/src/main/java/net/minestom/server/instance/InstanceManager.java b/src/main/java/net/minestom/server/instance/InstanceManager.java index 6b4bc2d12..8fed6b6d9 100644 --- a/src/main/java/net/minestom/server/instance/InstanceManager.java +++ b/src/main/java/net/minestom/server/instance/InstanceManager.java @@ -151,7 +151,7 @@ public final class InstanceManager { public @Nullable Instance getInstance(@NotNull UUID uuid) { Optional instance = getInstances() .stream() - .filter(someInstance -> someInstance.getUniqueId().equals(uuid)) + .filter(someInstance -> someInstance.getUuid().equals(uuid)) .findFirst(); return instance.orElse(null); } diff --git a/src/main/java/net/minestom/server/instance/LightingChunk.java b/src/main/java/net/minestom/server/instance/LightingChunk.java index c8872e363..99126db98 100644 --- a/src/main/java/net/minestom/server/instance/LightingChunk.java +++ b/src/main/java/net/minestom/server/instance/LightingChunk.java @@ -241,97 +241,98 @@ public class LightingChunk extends DynamicChunk { @Override protected LightData createLightData(boolean requiredFullChunk) { packetGenerationLock.lock(); - if (requiredFullChunk) { - if (fullLightData != null) { - packetGenerationLock.unlock(); - return fullLightData; - } - } else { - if (partialLightData != null) { - packetGenerationLock.unlock(); - return partialLightData; - } - } - - BitSet skyMask = new BitSet(); - BitSet blockMask = new BitSet(); - BitSet emptySkyMask = new BitSet(); - BitSet emptyBlockMask = new BitSet(); - List skyLights = new ArrayList<>(); - List blockLights = new ArrayList<>(); - - int chunkMin = instance.getCachedDimensionType().minY(); - int highestNeighborBlock = instance.getCachedDimensionType().minY(); - for (int i = -1; i <= 1; i++) { - for (int j = -1; j <= 1; j++) { - Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j); - if (neighborChunk == null) continue; - - if (neighborChunk instanceof LightingChunk light) { - light.getOcclusionMap(); - highestNeighborBlock = Math.max(highestNeighborBlock, light.highestBlock); + try { + if (requiredFullChunk) { + if (fullLightData != null) { + return fullLightData; } - } - } - - int index = 0; - for (Section section : sections) { - boolean wasUpdatedBlock = false; - boolean wasUpdatedSky = false; - - if (section.blockLight().requiresUpdate()) { - relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.BLOCK); - wasUpdatedBlock = true; - } else if (requiredFullChunk || section.blockLight().requiresSend()) { - wasUpdatedBlock = true; - } - - if (section.skyLight().requiresUpdate()) { - relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.SKY); - wasUpdatedSky = true; - } else if (requiredFullChunk || section.skyLight().requiresSend()) { - wasUpdatedSky = true; - } - - final int sectionMinY = index * 16 + chunkMin; - index++; - - if ((wasUpdatedSky) && this.instance.getCachedDimensionType().hasSkylight() && sectionMinY <= (highestNeighborBlock + 16)) { - final byte[] skyLight = section.skyLight().array(); - - if (skyLight.length != 0 && skyLight != EMPTY_CONTENT) { - skyLights.add(skyLight); - skyMask.set(index); - } else { - emptySkyMask.set(index); + } else { + if (partialLightData != null) { + return partialLightData; } } - if (wasUpdatedBlock) { - final byte[] blockLight = section.blockLight().array(); + BitSet skyMask = new BitSet(); + BitSet blockMask = new BitSet(); + BitSet emptySkyMask = new BitSet(); + BitSet emptyBlockMask = new BitSet(); + List skyLights = new ArrayList<>(); + List blockLights = new ArrayList<>(); - if (blockLight.length != 0 && blockLight != EMPTY_CONTENT) { - blockLights.add(blockLight); - blockMask.set(index); - } else { - emptyBlockMask.set(index); + int chunkMin = instance.getCachedDimensionType().minY(); + int highestNeighborBlock = instance.getCachedDimensionType().minY(); + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j); + if (neighborChunk == null) continue; + + if (neighborChunk instanceof LightingChunk light) { + light.getOcclusionMap(); + highestNeighborBlock = Math.max(highestNeighborBlock, light.highestBlock); + } } } + + int index = 0; + for (Section section : sections) { + boolean wasUpdatedBlock = false; + boolean wasUpdatedSky = false; + + if (section.blockLight().requiresUpdate()) { + relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.BLOCK); + wasUpdatedBlock = true; + } else if (requiredFullChunk || section.blockLight().requiresSend()) { + wasUpdatedBlock = true; + } + + if (section.skyLight().requiresUpdate()) { + relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.SKY); + wasUpdatedSky = true; + } else if (requiredFullChunk || section.skyLight().requiresSend()) { + wasUpdatedSky = true; + } + + final int sectionMinY = index * 16 + chunkMin; + index++; + + if ((wasUpdatedSky) && this.instance.getCachedDimensionType().hasSkylight() && sectionMinY <= (highestNeighborBlock + 16)) { + final byte[] skyLight = section.skyLight().array(); + + if (skyLight.length != 0 && skyLight != EMPTY_CONTENT) { + skyLights.add(skyLight); + skyMask.set(index); + } else { + emptySkyMask.set(index); + } + } + + if (wasUpdatedBlock) { + final byte[] blockLight = section.blockLight().array(); + + if (blockLight.length != 0 && blockLight != EMPTY_CONTENT) { + blockLights.add(blockLight); + blockMask.set(index); + } else { + emptyBlockMask.set(index); + } + } + } + + LightData lightData = new LightData(skyMask, blockMask, + emptySkyMask, emptyBlockMask, + skyLights, blockLights); + + if (requiredFullChunk) { + this.fullLightData = lightData; + } else { + this.partialLightData = lightData; + } + + + return lightData; + } finally { + packetGenerationLock.unlock(); } - - LightData lightData = new LightData(skyMask, blockMask, - emptySkyMask, emptyBlockMask, - skyLights, blockLights); - - if (requiredFullChunk) { - this.fullLightData = lightData; - } else { - this.partialLightData = lightData; - } - - packetGenerationLock.unlock(); - - return lightData; } @Override diff --git a/src/main/java/net/minestom/server/instance/SharedInstance.java b/src/main/java/net/minestom/server/instance/SharedInstance.java index c01540fa8..10a1cd217 100644 --- a/src/main/java/net/minestom/server/instance/SharedInstance.java +++ b/src/main/java/net/minestom/server/instance/SharedInstance.java @@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture; public class SharedInstance extends Instance { private final InstanceContainer instanceContainer; - public SharedInstance(@NotNull UUID uniqueId, @NotNull InstanceContainer instanceContainer) { - super(uniqueId, instanceContainer.getDimensionType()); + public SharedInstance(@NotNull UUID uuid, @NotNull InstanceContainer instanceContainer) { + super(uuid, instanceContainer.getDimensionType()); this.instanceContainer = instanceContainer; } diff --git a/src/main/java/net/minestom/server/instance/batch/ChunkBatch.java b/src/main/java/net/minestom/server/instance/batch/ChunkBatch.java index f6ac7795d..81dda9ecc 100644 --- a/src/main/java/net/minestom/server/instance/batch/ChunkBatch.java +++ b/src/main/java/net/minestom/server/instance/batch/ChunkBatch.java @@ -106,7 +106,7 @@ public class ChunkBatch implements Batch { final Chunk chunk = instance.getChunk(chunkX, chunkZ); if (chunk == null) { LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.", - chunkX, chunkZ, instance.getUniqueId()); + chunkX, chunkZ, instance.getUuid()); return null; } return apply(instance, chunk, callback); @@ -165,7 +165,7 @@ public class ChunkBatch implements Batch { try { if (!chunk.isLoaded()) { LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.", - chunk.getChunkX(), chunk.getChunkZ(), instance.getUniqueId()); + chunk.getChunkX(), chunk.getChunkZ(), instance.getUuid()); return; } diff --git a/src/main/java/net/minestom/server/instance/block/banner/BannerPattern.java b/src/main/java/net/minestom/server/instance/block/banner/BannerPattern.java index 5b7774ba1..7fbf7307a 100644 --- a/src/main/java/net/minestom/server/instance/block/banner/BannerPattern.java +++ b/src/main/java/net/minestom/server/instance/block/banner/BannerPattern.java @@ -13,7 +13,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public sealed interface BannerPattern extends ProtocolObject, BannerPatterns permits BannerPatternImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::bannerPattern); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::bannerPattern, true); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::bannerPattern); static @NotNull BannerPattern create( diff --git a/src/main/java/net/minestom/server/instance/block/jukebox/JukeboxSong.java b/src/main/java/net/minestom/server/instance/block/jukebox/JukeboxSong.java index 51dce5e39..f82e14fc5 100644 --- a/src/main/java/net/minestom/server/instance/block/jukebox/JukeboxSong.java +++ b/src/main/java/net/minestom/server/instance/block/jukebox/JukeboxSong.java @@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable; public sealed interface JukeboxSong extends ProtocolObject, JukeboxSongs permits JukeboxSongImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::jukeboxSong); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::jukeboxSong, false); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::jukeboxSong); static @NotNull JukeboxSong create( diff --git a/src/main/java/net/minestom/server/instance/generator/UnitModifier.java b/src/main/java/net/minestom/server/instance/generator/UnitModifier.java index dfd297232..406a59bdb 100644 --- a/src/main/java/net/minestom/server/instance/generator/UnitModifier.java +++ b/src/main/java/net/minestom/server/instance/generator/UnitModifier.java @@ -48,7 +48,7 @@ public interface UnitModifier extends Block.Setter, Biome.Setter { void fill(@NotNull Point start, @NotNull Point end, @NotNull Block block); /** - * Fills the 3d rectangular area with the given biome. + * Fills the 3d rectangular area with the given block. * * @param minHeight the minimum height of the area * @param maxHeight the maximum height of the area diff --git a/src/main/java/net/minestom/server/instance/light/BlockLight.java b/src/main/java/net/minestom/server/instance/light/BlockLight.java index 244940fc4..751c461c1 100644 --- a/src/main/java/net/minestom/server/instance/light/BlockLight.java +++ b/src/main/java/net/minestom/server/instance/light/BlockLight.java @@ -33,7 +33,7 @@ final class BlockLight implements Light { ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); // Apply section light blockPalette.getAllPresent((x, y, z, stateId) -> { - final Block block = Block.fromStateId((short) stateId); + final Block block = Block.fromStateId(stateId); assert block != null; final byte lightEmission = (byte) block.registry().lightEmission(); diff --git a/src/main/java/net/minestom/server/instance/light/LightCompute.java b/src/main/java/net/minestom/server/instance/light/LightCompute.java index 591e5d21c..43d92abd7 100644 --- a/src/main/java/net/minestom/server/instance/light/LightCompute.java +++ b/src/main/java/net/minestom/server/instance/light/LightCompute.java @@ -112,7 +112,7 @@ public final class LightCompute { } public static Block getBlock(Palette palette, int x, int y, int z) { - return Block.fromStateId((short) palette.get(x, y, z)); + return Block.fromStateId(palette.get(x, y, z)); } public static byte[] bake(byte[] content1, byte[] content2) { diff --git a/src/main/java/net/minestom/server/instance/palette/Palette.java b/src/main/java/net/minestom/server/instance/palette/Palette.java index 755d8c309..9acd0b24f 100644 --- a/src/main/java/net/minestom/server/instance/palette/Palette.java +++ b/src/main/java/net/minestom/server/instance/palette/Palette.java @@ -115,6 +115,7 @@ public interface Palette { if (bitsPerEntry == 0) { // Single valued 0-0 final int value = buffer.read(VAR_INT); + buffer.read(VAR_INT); // Skip size return new PaletteSingle((byte) dimension, value); } else if (bitsPerEntry >= minIndirect && bitsPerEntry <= maxIndirect) { // Indirect palette diff --git a/src/main/java/net/minestom/server/instance/palette/PaletteIndirect.java b/src/main/java/net/minestom/server/instance/palette/PaletteIndirect.java index 3cd3aa2e2..eff381467 100644 --- a/src/main/java/net/minestom/server/instance/palette/PaletteIndirect.java +++ b/src/main/java/net/minestom/server/instance/palette/PaletteIndirect.java @@ -49,8 +49,6 @@ final class PaletteIndirect implements SpecializedPalette, Cloneable { this.paletteToValueList.add(palette[i]); this.valueToPaletteMap.put(palette[i], i); } - - this.values = new long[arrayLength(dimension(), bitsPerEntry)]; } PaletteIndirect(int dimension, int maxBitsPerEntry, byte bitsPerEntry) { diff --git a/src/main/java/net/minestom/server/item/ItemComponent.java b/src/main/java/net/minestom/server/item/ItemComponent.java index 849c8a0cc..8de53eff8 100644 --- a/src/main/java/net/minestom/server/item/ItemComponent.java +++ b/src/main/java/net/minestom/server/item/ItemComponent.java @@ -77,7 +77,7 @@ public final class ItemComponent { public static final DataComponent WRITABLE_BOOK_CONTENT = register("writable_book_content", WritableBookContent.NETWORK_TYPE, WritableBookContent.NBT_TYPE); public static final DataComponent WRITTEN_BOOK_CONTENT = register("written_book_content", WrittenBookContent.NETWORK_TYPE, WrittenBookContent.NBT_TYPE); public static final DataComponent TRIM = register("trim", ArmorTrim.NETWORK_TYPE, ArmorTrim.NBT_TYPE); - public static final DataComponent DEBUG_STICK_STATE = register("debug_stick_state", null, DebugStickState.NBT_TYPE); + public static final DataComponent DEBUG_STICK_STATE = register("debug_stick_state", DebugStickState.NETWORK_TYPE, DebugStickState.NBT_TYPE); public static final DataComponent ENTITY_DATA = register("entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); public static final DataComponent BUCKET_ENTITY_DATA = register("bucket_entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); public static final DataComponent BLOCK_ENTITY_DATA = register("block_entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); diff --git a/src/main/java/net/minestom/server/item/armor/TrimMaterial.java b/src/main/java/net/minestom/server/item/armor/TrimMaterial.java index 610b2e59c..e215f1174 100644 --- a/src/main/java/net/minestom/server/item/armor/TrimMaterial.java +++ b/src/main/java/net/minestom/server/item/armor/TrimMaterial.java @@ -17,7 +17,7 @@ import java.util.HashMap; import java.util.Map; public sealed interface TrimMaterial extends ProtocolObject permits TrimMaterialImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimMaterial); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimMaterial, true); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::trimMaterial); static @NotNull TrimMaterial create( diff --git a/src/main/java/net/minestom/server/item/armor/TrimPattern.java b/src/main/java/net/minestom/server/item/armor/TrimPattern.java index bfe2b76a1..9c6988fda 100644 --- a/src/main/java/net/minestom/server/item/armor/TrimPattern.java +++ b/src/main/java/net/minestom/server/item/armor/TrimPattern.java @@ -15,7 +15,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public sealed interface TrimPattern extends ProtocolObject permits TrimPatternImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimPattern); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimPattern, true); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::trimPattern); static @NotNull TrimPattern create( diff --git a/src/main/java/net/minestom/server/item/component/DebugStickState.java b/src/main/java/net/minestom/server/item/component/DebugStickState.java index 8c2116955..1af3d4229 100644 --- a/src/main/java/net/minestom/server/item/component/DebugStickState.java +++ b/src/main/java/net/minestom/server/item/component/DebugStickState.java @@ -3,6 +3,7 @@ package net.minestom.server.item.component; import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.StringBinaryTag; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.nbt.BinaryTagSerializer; import org.jetbrains.annotations.NotNull; @@ -30,6 +31,7 @@ public record DebugStickState(@NotNull Map state) { return builder.build(); } ); + public static final NetworkBuffer.Type NETWORK_TYPE = NetworkBuffer.TypedNBT(NBT_TYPE); public DebugStickState { state = Map.copyOf(state); diff --git a/src/main/java/net/minestom/server/item/enchant/Enchantment.java b/src/main/java/net/minestom/server/item/enchant/Enchantment.java index 91b345b06..020010d13 100644 --- a/src/main/java/net/minestom/server/item/enchant/Enchantment.java +++ b/src/main/java/net/minestom/server/item/enchant/Enchantment.java @@ -16,7 +16,7 @@ import org.jetbrains.annotations.Nullable; import java.util.List; public sealed interface Enchantment extends ProtocolObject, Enchantments permits EnchantmentImpl { - @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::enchantment); + @NotNull NetworkBuffer.Type> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::enchantment, false); @NotNull BinaryTagSerializer> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::enchantment); static @NotNull Builder builder() { diff --git a/src/main/java/net/minestom/server/listener/SpectateListener.java b/src/main/java/net/minestom/server/listener/SpectateListener.java index f528fd26e..7c4c044ef 100644 --- a/src/main/java/net/minestom/server/listener/SpectateListener.java +++ b/src/main/java/net/minestom/server/listener/SpectateListener.java @@ -35,7 +35,7 @@ public class SpectateListener { // Ignore if they're not in the same instance. Vanilla actually allows for // cross-instance spectating, but it's not really a good idea for Minestom. - if (targetInstance.getUniqueId() != playerInstance.getUniqueId()) { + if (targetInstance.getUuid() != playerInstance.getUuid()) { return; } diff --git a/src/main/java/net/minestom/server/listener/UseEntityListener.java b/src/main/java/net/minestom/server/listener/UseEntityListener.java index 246844963..819c32ca2 100644 --- a/src/main/java/net/minestom/server/listener/UseEntityListener.java +++ b/src/main/java/net/minestom/server/listener/UseEntityListener.java @@ -1,7 +1,9 @@ package net.minestom.server.listener; import net.minestom.server.ServerFlag; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; @@ -20,8 +22,11 @@ public class UseEntityListener { return; if (ServerFlag.ENFORCE_INTERACTION_LIMIT) { - double range = Math.pow(player.getAttributeValue(Attribute.ENTITY_INTERACTION_RANGE) + 1, 2); // Add 1 additional block for people with less than stellar ping - if (player.getDistanceSquared(entity) > range) { + final double maxDistanceSquared = Math.pow(player.getAttributeValue(Attribute.ENTITY_INTERACTION_RANGE) + 1, 2); + + final double distSquared = getDistSquared(player, entity); + + if (distSquared > maxDistanceSquared) { return; } } @@ -36,4 +41,33 @@ public class UseEntityListener { EventDispatcher.call(new PlayerEntityInteractEvent(player, entity, interactAt.hand(), interactPosition)); } } -} + + private static double getDistSquared(Player player, Entity entity) { + final Pos playerPos = player.getPosition(); + final double eyeHeight = player.getEyeHeight(); + final double px = playerPos.x(); + final double py = playerPos.y() + eyeHeight; + final double pz = playerPos.z(); + + final BoundingBox box = entity.getBoundingBox(); + final double halfWidth = box.width() / 2; + final double height = box.height(); + final Pos entityPos = entity.getPosition(); + + final double minX = entityPos.x() - halfWidth; + final double maxX = entityPos.x() + halfWidth; + final double minY = entityPos.y(); + final double maxY = entityPos.y() + height; + final double minZ = entityPos.z() - halfWidth; + final double maxZ = entityPos.z() + halfWidth; + + final double clampX = Math.max(minX, Math.min(px, maxX)); + final double clampY = Math.max(minY, Math.min(py, maxY)); + final double clampZ = Math.max(minZ, Math.min(pz, maxZ)); + + final double dx = px - clampX; + final double dy = py - clampY; + final double dz = pz - clampZ; + return dx * dx + dy * dy + dz * dz; + } +} \ No newline at end of file diff --git a/src/main/java/net/minestom/server/network/ComponentNetworkBufferTypeImpl.java b/src/main/java/net/minestom/server/network/ComponentNetworkBufferTypeImpl.java index 3fcacd3fb..9b2ec3ff6 100644 --- a/src/main/java/net/minestom/server/network/ComponentNetworkBufferTypeImpl.java +++ b/src/main/java/net/minestom/server/network/ComponentNetworkBufferTypeImpl.java @@ -45,33 +45,33 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl { - writeUtf(buffer, "text"); + buffer.write(STRING_IO_UTF8, "text"); buffer.write(BYTE, TAG_STRING); // Start "text" tag - writeUtf(buffer, "text"); - writeUtf(buffer, text.content()); + buffer.write(STRING_IO_UTF8, "text"); + buffer.write(STRING_IO_UTF8, text.content()); } case TranslatableComponent translatable -> { - writeUtf(buffer, "translatable"); + buffer.write(STRING_IO_UTF8, "translatable"); buffer.write(BYTE, TAG_STRING); // Start "translate" tag - writeUtf(buffer, "translate"); - writeUtf(buffer, translatable.key()); + buffer.write(STRING_IO_UTF8, "translate"); + buffer.write(STRING_IO_UTF8, translatable.key()); final String fallback = translatable.fallback(); if (fallback != null) { buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "fallback"); - writeUtf(buffer, fallback); + buffer.write(STRING_IO_UTF8, "fallback"); + buffer.write(STRING_IO_UTF8, fallback); } final List args = translatable.arguments(); if (!args.isEmpty()) { buffer.write(BYTE, TAG_LIST); - writeUtf(buffer, "with"); + buffer.write(STRING_IO_UTF8, "with"); buffer.write(BYTE, TAG_COMPOUND); // List type buffer.write(INT, args.size()); for (final TranslationArgument arg : args) @@ -79,42 +79,42 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl { - writeUtf(buffer, "score"); + buffer.write(STRING_IO_UTF8, "score"); buffer.write(BYTE, TAG_COMPOUND); // Start "score" tag - writeUtf(buffer, "score"); + buffer.write(STRING_IO_UTF8, "score"); { buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "name"); - writeUtf(buffer, score.name()); + buffer.write(STRING_IO_UTF8, "name"); + buffer.write(STRING_IO_UTF8, score.name()); buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "objective"); - writeUtf(buffer, score.objective()); + buffer.write(STRING_IO_UTF8, "objective"); + buffer.write(STRING_IO_UTF8, score.objective()); } buffer.write(BYTE, TAG_END); // End "score" tag } case SelectorComponent selector -> { - writeUtf(buffer, "selector"); + buffer.write(STRING_IO_UTF8, "selector"); buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "selector"); - writeUtf(buffer, selector.pattern()); + buffer.write(STRING_IO_UTF8, "selector"); + buffer.write(STRING_IO_UTF8, selector.pattern()); final Component separator = selector.separator(); if (separator != null) { buffer.write(BYTE, TAG_COMPOUND); - writeUtf(buffer, "separator"); + buffer.write(STRING_IO_UTF8, "separator"); writeInnerComponent(buffer, separator); } } case KeybindComponent keybind -> { - writeUtf(buffer, "keybind"); + buffer.write(STRING_IO_UTF8, "keybind"); buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "keybind"); - writeUtf(buffer, keybind.keybind()); + buffer.write(STRING_IO_UTF8, "keybind"); + buffer.write(STRING_IO_UTF8, keybind.keybind()); } case NBTComponent nbt -> { //todo @@ -126,7 +126,7 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl hoverEvent) { buffer.write(BYTE, TAG_COMPOUND); - writeUtf(buffer, "hoverEvent"); + buffer.write(STRING_IO_UTF8, "hoverEvent"); buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "action"); - writeUtf(buffer, hoverEvent.action().toString().toLowerCase(Locale.ROOT)); + buffer.write(STRING_IO_UTF8, "action"); + buffer.write(STRING_IO_UTF8, hoverEvent.action().toString().toLowerCase(Locale.ROOT)); buffer.write(BYTE, TAG_COMPOUND); // Start contents tag - writeUtf(buffer, "contents"); + buffer.write(STRING_IO_UTF8, "contents"); if (hoverEvent.action() == HoverEvent.Action.SHOW_TEXT) { writeInnerComponent(buffer, (Component) hoverEvent.value()); } else if (hoverEvent.action() == HoverEvent.Action.SHOW_ITEM) { var value = ((HoverEvent) hoverEvent).value(); buffer.write(BYTE, TAG_STRING); - writeUtf(buffer, "id"); - writeUtf(buffer, value.item().asString()); + buffer.write(STRING_IO_UTF8, "id"); + buffer.write(STRING_IO_UTF8, value.item().asString()); buffer.write(BYTE, TAG_INT); - writeUtf(buffer, "count"); + buffer.write(STRING_IO_UTF8, "count"); buffer.write(INT, value.count()); buffer.write(BYTE, TAG_COMPOUND); - writeUtf(buffer, "components"); + buffer.write(STRING_IO_UTF8, "components"); //todo item components buffer.write(BYTE, TAG_END); @@ -257,17 +257,17 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl= 0x80 || c == 0) - utflen += (c >= 0x800) ? 2 : 1; - } - - if (utflen > 65535 || /* overflow */ utflen < strlen) - throw new RuntimeException("UTF-8 string too long"); - - buffer.write(SHORT, (short) utflen); - buffer.ensureWritable(utflen); - var impl = (NetworkBufferImpl) buffer; - int i; - for (i = 0; i < strlen; i++) { // optimized for initial run of ASCII - int c = str.charAt(i); - if (c >= 0x80 || c == 0) break; - impl._putByte(buffer.writeIndex(), (byte) c); - impl.advanceWrite(1); - } - - for (; i < strlen; i++) { - int c = str.charAt(i); - if (c < 0x80 && c != 0) { - impl._putByte(buffer.writeIndex(), (byte) c); - impl.advanceWrite(1); - } else if (c >= 0x800) { - impl._putByte(buffer.writeIndex(), (byte) (0xE0 | ((c >> 12) & 0x0F))); - impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 6) & 0x3F))); - impl._putByte(buffer.writeIndex() + 2, (byte) (0x80 | ((c >> 0) & 0x3F))); - impl.advanceWrite(3); - } else { - impl._putByte(buffer.writeIndex(), (byte) (0xC0 | ((c >> 6) & 0x1F))); - impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 0) & 0x3F))); - impl.advanceWrite(2); - } - } - } } diff --git a/src/main/java/net/minestom/server/network/ConnectionManager.java b/src/main/java/net/minestom/server/network/ConnectionManager.java index ec2cd9cb7..597fc061f 100644 --- a/src/main/java/net/minestom/server/network/ConnectionManager.java +++ b/src/main/java/net/minestom/server/network/ConnectionManager.java @@ -333,6 +333,7 @@ public final class ConnectionManager { public void updateWaitingPlayers() { this.waitingPlayers.drain(player -> { if (!player.isOnline()) return; // Player disconnected while in queued to join + configurationPlayers.remove(player); playPlayers.add(player); keepAlivePlayers.add(player); diff --git a/src/main/java/net/minestom/server/network/NetworkBuffer.java b/src/main/java/net/minestom/server/network/NetworkBuffer.java index 068e34b36..bb07e2dc8 100644 --- a/src/main/java/net/minestom/server/network/NetworkBuffer.java +++ b/src/main/java/net/minestom/server/network/NetworkBuffer.java @@ -12,6 +12,7 @@ import net.minestom.server.registry.Registries; import net.minestom.server.utils.Direction; import net.minestom.server.utils.Unit; import net.minestom.server.utils.crypto.KeyUtils; +import net.minestom.server.utils.nbt.BinaryTagSerializer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; @@ -45,6 +46,7 @@ public sealed interface NetworkBuffer permits NetworkBufferImpl { Type RAW_BYTES = new NetworkBufferTypeImpl.RawBytesType(-1); Type STRING = new NetworkBufferTypeImpl.StringType(); Type STRING_TERMINATED = new NetworkBufferTypeImpl.StringTerminatedType(); + Type STRING_IO_UTF8 = new NetworkBufferTypeImpl.IOUTF8StringType(); Type NBT = new NetworkBufferTypeImpl.NbtType(); @SuppressWarnings({"unchecked", "rawtypes"}) Type NBT_COMPOUND = (Type) new NetworkBufferTypeImpl.NbtType(); @@ -63,8 +65,8 @@ public sealed interface NetworkBuffer permits NetworkBufferImpl { Type INSTANT_MS = LONG.transform(Instant::ofEpochMilli, Instant::toEpochMilli); Type PUBLIC_KEY = BYTE_ARRAY.transform(KeyUtils::publicRSAKeyFrom, PublicKey::getEncoded); - static @NotNull Type> RegistryKey(@NotNull Function> selector) { - return new NetworkBufferTypeImpl.RegistryTypeType<>(selector); + static @NotNull Type> RegistryKey(@NotNull Function> selector, boolean holder) { + return new NetworkBufferTypeImpl.RegistryTypeType<>(selector, holder); } // METADATA @@ -104,6 +106,10 @@ public sealed interface NetworkBuffer permits NetworkBufferImpl { return new NetworkBufferTypeImpl.LazyType<>(supplier); } + static @NotNull Type TypedNBT(@NotNull BinaryTagSerializer serializer) { + return new NetworkBufferTypeImpl.TypedNbtType<>(serializer); + } + void write(@NotNull Type type, @UnknownNullability T value) throws IndexOutOfBoundsException; @UnknownNullability T read(@NotNull Type type) throws IndexOutOfBoundsException; diff --git a/src/main/java/net/minestom/server/network/NetworkBufferTypeImpl.java b/src/main/java/net/minestom/server/network/NetworkBufferTypeImpl.java index b7c2efd8d..4da1b1c86 100644 --- a/src/main/java/net/minestom/server/network/NetworkBufferTypeImpl.java +++ b/src/main/java/net/minestom/server/network/NetworkBufferTypeImpl.java @@ -13,6 +13,7 @@ import net.minestom.server.registry.ProtocolObject; import net.minestom.server.registry.Registries; import net.minestom.server.utils.Unit; import net.minestom.server.utils.nbt.BinaryTagReader; +import net.minestom.server.utils.nbt.BinaryTagSerializer; import net.minestom.server.utils.nbt.BinaryTagWriter; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; @@ -708,6 +709,23 @@ interface NetworkBufferTypeImpl extends NetworkBuffer.Type { } } + record TypedNbtType(@NotNull BinaryTagSerializer nbtType) implements NetworkBufferTypeImpl { + @Override + public void write(@NotNull NetworkBuffer buffer, T value) { + final Registries registries = impl(buffer).registries; + Check.stateCondition(registries == null, "Buffer does not have registries"); + buffer.write(NBT, nbtType.write(new BinaryTagSerializer.ContextWithRegistries(registries), value)); + } + + @Override + public T read(@NotNull NetworkBuffer buffer) { + final Registries registries = impl(buffer).registries; + Check.stateCondition(registries == null, "Buffer does not have registries"); + final BinaryTag tag = buffer.read(NBT); + return nbtType.read(new BinaryTagSerializer.ContextWithRegistries(registries), tag); + } + } + record TransformType(@NotNull Type parent, @NotNull Function to, @NotNull Function from) implements NetworkBufferTypeImpl { @Override @@ -794,15 +812,18 @@ interface NetworkBufferTypeImpl extends NetworkBuffer.Type { } record RegistryTypeType( - @NotNull Function> selector) implements NetworkBufferTypeImpl> { + @NotNull Function> selector, + boolean holder + ) implements NetworkBufferTypeImpl> { @Override public void write(@NotNull NetworkBuffer buffer, DynamicRegistry.Key value) { final Registries registries = impl(buffer).registries; Check.stateCondition(registries == null, "Buffer does not have registries"); final DynamicRegistry registry = selector.apply(registries); - // Painting variants may be sent in their entirety rather than a registry reference so the ID is offset by 1 to indicate this. + // "Holder" references can either be a registry entry or the entire object itself. The id is zero if the + // entire object follows, but we only support registry objects currently so always offset by 1. // FIXME: Support sending the entire registry object instead of an ID reference. - final int id = registry.id().equals("minecraft:painting_variant") ? registry.getId(value) + 1 : registry.getId(value); + final int id = registry.getId(value) + (holder ? 1 : 0); Check.argCondition(id == -1, "Key is not registered: {0} > {1}", registry, value); buffer.write(VAR_INT, id); } @@ -812,13 +833,131 @@ interface NetworkBufferTypeImpl extends NetworkBuffer.Type { final Registries registries = impl(buffer).registries; Check.stateCondition(registries == null, "Buffer does not have registries"); DynamicRegistry registry = selector.apply(registries); - final int id = buffer.read(VAR_INT); + // See note above about holder references. + final int id = buffer.read(VAR_INT) + (holder ? -1 : 0); final DynamicRegistry.Key key = registry.getKey(id); Check.argCondition(key == null, "No such ID in registry: {0} > {1}", registry, id); return key; } } + /** + * This is a very gross version of {@link java.io.DataOutputStream#writeUTF(String)} & ${@link DataInputStream#readUTF()}. We need the data in the java + * modified utf-8 format for Component, and I couldnt find a method without creating a new buffer for it. + */ + record IOUTF8StringType() implements NetworkBufferTypeImpl { + @Override + public void write(@NotNull NetworkBuffer buffer, String value) { + final int strlen = value.length(); + int utflen = strlen; // optimized for ASCII + + for (int i = 0; i < strlen; i++) { + int c = value.charAt(i); + if (c >= 0x80 || c == 0) + utflen += (c >= 0x800) ? 2 : 1; + } + + if (utflen > 65535 || /* overflow */ utflen < strlen) + throw new RuntimeException("UTF-8 string too long"); + + buffer.write(SHORT, (short) utflen); + buffer.ensureWritable(utflen); + var impl = (NetworkBufferImpl) buffer; + int i; + for (i = 0; i < strlen; i++) { // optimized for initial run of ASCII + int c = value.charAt(i); + if (c >= 0x80 || c == 0) break; + impl._putByte(buffer.writeIndex(), (byte) c); + impl.advanceWrite(1); + } + + for (; i < strlen; i++) { + int c = value.charAt(i); + if (c < 0x80 && c != 0) { + impl._putByte(buffer.writeIndex(), (byte) c); + impl.advanceWrite(1); + } else if (c >= 0x800) { + impl._putByte(buffer.writeIndex(), (byte) (0xE0 | ((c >> 12) & 0x0F))); + impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 6) & 0x3F))); + impl._putByte(buffer.writeIndex() + 2, (byte) (0x80 | ((c >> 0) & 0x3F))); + impl.advanceWrite(3); + } else { + impl._putByte(buffer.writeIndex(), (byte) (0xC0 | ((c >> 6) & 0x1F))); + impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 0) & 0x3F))); + impl.advanceWrite(2); + } + } + } + + @Override + public String read(@NotNull NetworkBuffer buffer) { + int utflen = buffer.read(UNSIGNED_SHORT); + if (buffer.readableBytes() < utflen) throw new IllegalArgumentException("Invalid String size."); + byte[] bytearr = buffer.read(RAW_BYTES); + final char[] chararr = new char[utflen]; + + int c, char2, char3; + int count = 0; + int chararr_count = 0; + + while (count < utflen) { + c = (int) bytearr[count] & 0xff; + if (c > 127) break; + count++; + chararr[chararr_count++] = (char) c; + } + + while (count < utflen) { + c = (int) bytearr[count] & 0xff; + try { // Surround in try catch to throw a runtime exception instead of a checked one + switch (c >> 4) { + case 0, 1, 2, 3, 4, 5, 6, 7 -> { + /* 0xxxxxxx*/ + count++; + chararr[chararr_count++] = (char) c; + } + case 12, 13 -> { + /* 110x xxxx 10xx xxxx*/ + count += 2; + if (count > utflen) + throw new UTFDataFormatException( + "malformed input: partial character at end"); + char2 = bytearr[count - 1]; + if ((char2 & 0xC0) != 0x80) + throw new UTFDataFormatException( + "malformed input around byte " + count); + chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | + (char2 & 0x3F)); + } + case 14 -> { + /* 1110 xxxx 10xx xxxx 10xx xxxx */ + count += 3; + if (count > utflen) + throw new UTFDataFormatException( + "malformed input: partial character at end"); + char2 = bytearr[count - 2]; + char3 = bytearr[count - 1]; + if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80)) + throw new UTFDataFormatException( + "malformed input around byte " + (count - 1)); + chararr[chararr_count++] = (char) (((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + } + default -> + /* 10xx xxxx, 1111 xxxx */ + throw new UTFDataFormatException( + "malformed input around byte " + count); + } + } catch (UTFDataFormatException e) { + throw new IllegalArgumentException(e); + } + } + // The number of chars produced may be less than utflen + return new String(chararr, 0, chararr_count); + } + } + static long sizeOf(Type type, T value, Registries registries) { NetworkBuffer buffer = NetworkBufferImpl.dummy(registries); type.write(buffer, value); diff --git a/src/main/java/net/minestom/server/network/packet/PacketWriting.java b/src/main/java/net/minestom/server/network/packet/PacketWriting.java index d93e38694..8736388d9 100644 --- a/src/main/java/net/minestom/server/network/packet/PacketWriting.java +++ b/src/main/java/net/minestom/server/network/packet/PacketWriting.java @@ -202,6 +202,7 @@ public final class PacketWriting { if (written < minWrite) { // Try again with a bigger buffer final long newSize = Math.min(buffer.capacity() * 2, ServerFlag.MAX_PACKET_SIZE); + if (newSize == buffer.capacity()) break; // We reached the maximum size buffer.resize(newSize); } else { // At least one packet has been written diff --git a/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java b/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java index 8c4256516..239ae76cc 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java @@ -10,16 +10,15 @@ import net.minestom.server.network.player.GameProfile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.EnumSet; -import java.util.List; -import java.util.UUID; +import java.util.*; +import java.util.function.UnaryOperator; import static net.minestom.server.network.NetworkBuffer.*; public record PlayerInfoUpdatePacket( @NotNull EnumSet<@NotNull Action> actions, @NotNull List<@NotNull Entry> entries -) implements ServerPacket.Play { +) implements ServerPacket.Play, ServerPacket.ComponentHolding { public static final int MAX_ENTRIES = 1024; public PlayerInfoUpdatePacket(@NotNull Action action, @NotNull Entry entry) { @@ -46,6 +45,33 @@ public record PlayerInfoUpdatePacket( } }; + @Override + public @NotNull Collection components() { + final List components = new ArrayList<>(); + for (final Entry entry : entries) { + if (entry.displayName() == null) continue; + components.add(entry.displayName()); + } + return components; + } + + @Override + public @NotNull ServerPacket copyWithOperator(@NotNull UnaryOperator operator) { + final List newEntries = new ArrayList<>(); + for (final Entry entry : entries) { + final Component displayName = entry.displayName(); + if (displayName != null) { + newEntries.add(new Entry(entry.uuid, entry.username, + entry.properties, entry.listed, entry.latency, + entry.gameMode, operator.apply(displayName), + entry.chatSession, entry.listOrder)); + } else { + newEntries.add(entry); + } + } + return new PlayerInfoUpdatePacket(actions, newEntries); + } + public record Entry(UUID uuid, String username, List properties, boolean listed, int latency, GameMode gameMode, @Nullable Component displayName, @Nullable ChatSession chatSession, diff --git a/src/main/java/net/minestom/server/network/player/PlayerConnection.java b/src/main/java/net/minestom/server/network/player/PlayerConnection.java index 1b3c2682a..f09a04027 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerConnection.java @@ -141,9 +141,9 @@ public abstract class PlayerConnection { */ public void disconnect() { this.online = false; - MinecraftServer.getConnectionManager().removePlayer(this); - final Player player = getPlayer(); + final Player player = MinecraftServer.getConnectionManager().getPlayer(this); if (player != null) { + MinecraftServer.getConnectionManager().removePlayer(this); if (connectionState == ConnectionState.PLAY && !player.isRemoved()) player.scheduleNextTick(Entity::remove); else { diff --git a/src/main/java/net/minestom/server/registry/Registry.java b/src/main/java/net/minestom/server/registry/Registry.java index f413861fa..431db24d1 100644 --- a/src/main/java/net/minestom/server/registry/Registry.java +++ b/src/main/java/net/minestom/server/registry/Registry.java @@ -137,6 +137,10 @@ public final class Registry { return new JukeboxSongEntry(namespace, main, null); } + public static GameEventEntry gameEventEntry(String namespace, Properties properties) { + return new GameEventEntry(namespace, properties, null); + } + public static @NotNull InputStream loadRegistryFile(@NotNull Resource resource) throws IOException { // 1. Try to load from jar resources InputStream resourceStream = Registry.class.getClassLoader().getResourceAsStream(resource.name); @@ -245,6 +249,7 @@ public final class Registry { ENTITY_TYPE_TAGS("tags/entity_type.json"), FLUID_TAGS("tags/fluid.json"), GAMEPLAY_TAGS("tags/game_event.json"), + GAME_EVENTS("game_events.json"), ITEM_TAGS("tags/item.json"), ENCHANTMENT_TAGS("tags/enchantment.json"), BIOME_TAGS("tags/biome.json"), @@ -270,6 +275,12 @@ public final class Registry { } } + public record GameEventEntry(NamespaceID namespace, Properties main, Properties custom) implements Entry { + public GameEventEntry(String namespace, Properties main, Properties custom) { + this(NamespaceID.from(namespace), main, custom); + } + } + public static final class BlockEntry implements Entry { private final NamespaceID namespace; private final int id; diff --git a/src/main/java/net/minestom/server/scoreboard/TeamManager.java b/src/main/java/net/minestom/server/scoreboard/TeamManager.java index ecca0be73..f1a9721d5 100644 --- a/src/main/java/net/minestom/server/scoreboard/TeamManager.java +++ b/src/main/java/net/minestom/server/scoreboard/TeamManager.java @@ -5,7 +5,7 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.Player; import net.minestom.server.utils.PacketSendingUtils; -import net.minestom.server.utils.UniqueIdUtils; +import net.minestom.server.utils.UUIDUtils; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -158,7 +158,7 @@ public final class TeamManager { public List getPlayers(Team team) { List players = new ArrayList<>(); for (String member : team.getMembers()) { - boolean match = UniqueIdUtils.isUniqueId(member); + boolean match = UUIDUtils.isUuid(member); if (!match) players.add(member); } @@ -176,7 +176,7 @@ public final class TeamManager { public List getEntities(Team team) { List entities = new ArrayList<>(); for (String member : team.getMembers()) { - boolean match = UniqueIdUtils.isUniqueId(member); + boolean match = UUIDUtils.isUuid(member); if (match) entities.add(member); } diff --git a/src/main/java/net/minestom/server/snapshot/SnapshotImpl.java b/src/main/java/net/minestom/server/snapshot/SnapshotImpl.java index 02e753b8a..a934813fc 100644 --- a/src/main/java/net/minestom/server/snapshot/SnapshotImpl.java +++ b/src/main/java/net/minestom/server/snapshot/SnapshotImpl.java @@ -97,7 +97,7 @@ public final class SnapshotImpl { final Section section = sections[globalToChunk(y) - minSection]; final int blockStateId = section.blockPalette() .get(globalToSectionRelative(x), globalToSectionRelative(y), globalToSectionRelative(z)); - return Objects.requireNonNullElse(Block.fromStateId((short) blockStateId), Block.AIR); + return Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR); } @Override diff --git a/src/main/java/net/minestom/server/tag/Serializers.java b/src/main/java/net/minestom/server/tag/Serializers.java index f26ff1155..9f18715c7 100644 --- a/src/main/java/net/minestom/server/tag/Serializers.java +++ b/src/main/java/net/minestom/server/tag/Serializers.java @@ -5,7 +5,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minestom.server.ServerFlag; import net.minestom.server.item.ItemStack; -import net.minestom.server.utils.UniqueIdUtils; +import net.minestom.server.utils.UUIDUtils; import java.util.function.Function; @@ -23,7 +23,7 @@ final class Serializers { static final Entry STRING = new Entry<>(BinaryTagTypes.STRING, StringBinaryTag::value, StringBinaryTag::stringBinaryTag); static final Entry NBT_ENTRY = new Entry<>(null, Function.identity(), Function.identity()); - static final Entry UUID = new Entry<>(BinaryTagTypes.INT_ARRAY, UniqueIdUtils::fromNbt, UniqueIdUtils::toNbt); + static final Entry UUID = new Entry<>(BinaryTagTypes.INT_ARRAY, UUIDUtils::fromNbt, UUIDUtils::toNbt); static final Entry ITEM = new Entry<>(BinaryTagTypes.COMPOUND, ItemStack::fromItemNBT, ItemStack::toItemNBT); static final Entry COMPONENT = new Entry<>(BinaryTagTypes.STRING, input -> GsonComponentSerializer.gson().deserialize(input.value()), component -> StringBinaryTag.stringBinaryTag(GsonComponentSerializer.gson().serialize(component))); diff --git a/src/main/java/net/minestom/server/utils/UniqueIdUtils.java b/src/main/java/net/minestom/server/utils/UUIDUtils.java similarity index 91% rename from src/main/java/net/minestom/server/utils/UniqueIdUtils.java rename to src/main/java/net/minestom/server/utils/UUIDUtils.java index 8d04807af..9d9717838 100644 --- a/src/main/java/net/minestom/server/utils/UniqueIdUtils.java +++ b/src/main/java/net/minestom/server/utils/UUIDUtils.java @@ -11,7 +11,7 @@ import java.util.regex.Pattern; * An utilities class for {@link UUID}. */ @ApiStatus.Internal -public final class UniqueIdUtils { +public final class UUIDUtils { public static final Pattern UNIQUE_ID_PATTERN = Pattern.compile("\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b"); /** @@ -20,8 +20,8 @@ public final class UniqueIdUtils { * @param input The input string to be checked * @return {@code true} if the input an unique identifier, otherwise {@code false} */ - public static boolean isUniqueId(String input) { - return input.matches(UNIQUE_ID_PATTERN.pattern()); + public static boolean isUuid(String input) { + return UNIQUE_ID_PATTERN.matcher(input).matches(); } public static @NotNull UUID fromNbt(@NotNull IntArrayBinaryTag tag) { diff --git a/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java b/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java index bff217aa6..e6eec0758 100644 --- a/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java +++ b/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java @@ -6,17 +6,28 @@ import org.jetbrains.annotations.ApiStatus; import java.util.Arrays; +/** + * Allows to limit operations with recently operated chunks + *

+ * {@link ChunkUpdateLimitChecker#historySize} defines how many last chunks will be remembered + * to skip operations with them via {@link ChunkUpdateLimitChecker#addToHistory(Chunk)} returning {@code false} + */ @ApiStatus.Internal public final class ChunkUpdateLimitChecker { + private final int historySize; private final long[] chunkHistory; public ChunkUpdateLimitChecker(int historySize) { - this.historySize = historySize; - this.chunkHistory = new long[historySize]; + this.historySize = Math.max(0, historySize); + this.chunkHistory = new long[this.historySize]; this.clearHistory(); } + public boolean isEnabled() { + return historySize > 0; + } + /** * Adds the chunk to the history * @@ -24,14 +35,19 @@ public final class ChunkUpdateLimitChecker { * @return {@code true} if it's a new chunk in the history */ public boolean addToHistory(Chunk chunk) { + if (!isEnabled()) { + return true; + } final long index = CoordConversion.chunkIndex(chunk.getChunkX(), chunk.getChunkZ()); boolean result = true; final int lastIndex = historySize - 1; - for (int i = 0; i < lastIndex; i++) { + for (int i = 0; i <= lastIndex; i++) { if (chunkHistory[i] == index) { result = false; } - chunkHistory[i] = chunkHistory[i + 1]; + if (i != lastIndex) { + chunkHistory[i] = chunkHistory[i + 1]; + } } chunkHistory[lastIndex] = index; return result; diff --git a/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java b/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java index b00918286..90f028fbd 100644 --- a/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java +++ b/src/main/java/net/minestom/server/utils/nbt/BinaryTagSerializer.java @@ -14,7 +14,7 @@ import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.registry.ProtocolObject; import net.minestom.server.registry.Registries; import net.minestom.server.utils.NamespaceID; -import net.minestom.server.utils.UniqueIdUtils; +import net.minestom.server.utils.UUIDUtils; import net.minestom.server.utils.Unit; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.ApiStatus; @@ -257,7 +257,7 @@ public interface BinaryTagSerializer { BinaryTagSerializer UUID = new BinaryTagSerializer<>() { @Override public @NotNull BinaryTag write(java.util.@NotNull UUID value) { - return UniqueIdUtils.toNbt(value); + return UUIDUtils.toNbt(value); } @Override @@ -265,7 +265,7 @@ public interface BinaryTagSerializer { if (!(tag instanceof IntArrayBinaryTag intArrayTag)) { throw new IllegalArgumentException("unexpected uuid type: " + tag.type()); } - return UniqueIdUtils.fromNbt(intArrayTag); + return UUIDUtils.fromNbt(intArrayTag); } }; diff --git a/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java b/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java index eaa513b9d..ef26d4dbb 100644 --- a/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java @@ -8,6 +8,11 @@ import net.minestom.testing.Env; import net.minestom.testing.EnvTest; import org.junit.jupiter.api.Test; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import static org.junit.jupiter.api.Assertions.assertEquals; @EnvTest @@ -99,4 +104,21 @@ public class EntityTeleportIntegrationTest { player.teleport(new Pos(5, 10, 2, 5, 5), null, RelativeFlags.VIEW).join(); assertEquals(player.getPosition(), new Pos(5, 10, 2, 95, 5)); } + + @Test + public void entityTeleportToInfinity(Env env) throws ExecutionException, InterruptedException, TimeoutException { + var instance = env.createFlatInstance(); + var entity = new Entity(EntityTypes.ZOMBIE); + entity.setInstance(instance, new Pos(0, 42, 0)).join(); + assertEquals(instance, entity.getInstance()); + assertEquals(new Pos(0, 42, 0), entity.getPosition()); + + entity.teleport(new Pos(Double.POSITIVE_INFINITY, 42, 52)).join(); + CompletableFuture.runAsync(() -> entity.tick(System.currentTimeMillis())) + .get(10, TimeUnit.SECONDS); + // This should not hang forever + + // The position should have been capped at 2 billion. + assertEquals(new Pos(2_000_000_000, 42, 52), entity.getPosition()); + } } diff --git a/src/test/java/net/minestom/server/event/EventNodeTest.java b/src/test/java/net/minestom/server/event/EventNodeTest.java index 114033a08..3fa1a782a 100644 --- a/src/test/java/net/minestom/server/event/EventNodeTest.java +++ b/src/test/java/net/minestom/server/event/EventNodeTest.java @@ -124,6 +124,33 @@ public class EventNodeTest { assertTrue(result1.get(), "Recursive1 should be called due to the RecursiveEvent interface"); } + @Test + public void testRecursiveChild() { + var called1 = new AtomicBoolean(false); + var called2 = new AtomicBoolean(false); + var child1 = EventNode.all("child1"); + var child2 = EventNode.all("child2"); + child1.addListener(Recursive1.class, event -> called1.set(true)); + child2.addListener(Recursive1.class, event -> called2.set(true)); + + var node = EventNode.all("main"); + node.addChild(child1); + + node.call(new Recursive2()); + + assertTrue(called1.get()); + assertFalse(called2.get()); + called1.set(false); + + node.removeChild(child1); + node.addChild(child2); + + node.call(new Recursive2()); + + assertFalse(called1.get()); + assertTrue(called2.get()); + } + // FIXME: nodes are currently unable to retrieve sub handles //@Test //public void recursiveSuper() { diff --git a/src/test/java/net/minestom/server/instance/InstanceUnregisterIntegrationTest.java b/src/test/java/net/minestom/server/instance/InstanceUnregisterIntegrationTest.java index 45e43c555..c397507bd 100644 --- a/src/test/java/net/minestom/server/instance/InstanceUnregisterIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/InstanceUnregisterIntegrationTest.java @@ -99,7 +99,7 @@ public class InstanceUnregisterIntegrationTest { private void tmp(InstanceContainer instanceContainer) { instanceContainer.eventNode().addListener(InstanceTickEvent.class, (e) -> { - var uuid = instanceContainer.getUniqueId(); + var uuid = instanceContainer.getUuid(); }); } } diff --git a/src/test/java/net/minestom/server/instance/palette/PaletteIndirectTest.java b/src/test/java/net/minestom/server/instance/palette/PaletteIndirectTest.java new file mode 100644 index 000000000..50d6e7571 --- /dev/null +++ b/src/test/java/net/minestom/server/instance/palette/PaletteIndirectTest.java @@ -0,0 +1,17 @@ +package net.minestom.server.instance.palette; + +import org.junit.jupiter.api.Test; + +public class PaletteIndirectTest { + + @Test + public void constructor() { + var palette = new PaletteIndirect(16, 8, (byte) 4); + palette.set(0, 0, 1, 1); + var otherPalette = new PaletteIndirect(palette.dimension(), palette.maxBitsPerEntry(), (byte) palette.bitsPerEntry(), palette.count(), palette.paletteToValueList.toIntArray(), palette.values); + + palette.getAll((x, y, z, value) -> { + assert value == otherPalette.get(x, y, z); + }); + } +} diff --git a/src/test/java/net/minestom/server/item/component/UnitTest.java b/src/test/java/net/minestom/server/item/component/UnitTest.java index e0a5f7e8c..78ac53e6c 100644 --- a/src/test/java/net/minestom/server/item/component/UnitTest.java +++ b/src/test/java/net/minestom/server/item/component/UnitTest.java @@ -1,5 +1,6 @@ package net.minestom.server.item.component; +import net.minestom.server.MinecraftServer; import net.minestom.server.component.DataComponent; import net.minestom.server.item.ItemComponent; import net.minestom.server.network.NetworkBuffer; @@ -25,6 +26,10 @@ public class UnitTest extends AbstractItemComponentTest { ItemComponent.GLIDER ); + static { + MinecraftServer.init(); + } + @Override protected @NotNull DataComponent component() { return UNIT_COMPONENTS.getFirst(); @@ -46,7 +51,7 @@ public class UnitTest extends AbstractItemComponentTest { // Try to write as a Unit and if it fails we can ignore that type try { //noinspection unchecked - ((DataComponent) component).write(NetworkBuffer.resizableBuffer(), Unit.INSTANCE); + ((DataComponent) component).write(NetworkBuffer.resizableBuffer(MinecraftServer.process()), Unit.INSTANCE); } catch (ClassCastException ignored) { continue; } diff --git a/src/test/java/net/minestom/server/listener/UseEntityListenerTest.java b/src/test/java/net/minestom/server/listener/UseEntityListenerTest.java new file mode 100644 index 000000000..8ddebdeb3 --- /dev/null +++ b/src/test/java/net/minestom/server/listener/UseEntityListenerTest.java @@ -0,0 +1,106 @@ +package net.minestom.server.listener; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.PlayerHand; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.event.player.PlayerEntityInteractEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.network.packet.client.play.ClientInteractEntityPacket; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@EnvTest +public class UseEntityListenerTest { + + private Player player; + private Entity targetEntity; + private boolean eventWasCalled; + + @BeforeEach + public void setup(Env env) { + Instance instance = env.createFlatInstance(); + + player = env.createPlayer(instance, new Pos(0, 0, 0)); + player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(5.0); + + targetEntity = new Entity(EntityType.SLIME); + targetEntity.setInstance(instance, new Pos(2, 0, 2)).join(); + + eventWasCalled = false; + + player.eventNode().addListener(PlayerEntityInteractEvent.class, event -> { + if (event.getPlayer().equals(player) && event.getTarget().equals(targetEntity)) { + eventWasCalled = true; + } + }); + } + + @Test + public void testInteractionWithinRange() { + ClientInteractEntityPacket packet = new ClientInteractEntityPacket( + targetEntity.getEntityId(), + new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN), + false + ); + + UseEntityListener.useEntityListener(packet, player); + assertTrue(eventWasCalled, "Expected PlayerEntityInteractEvent to be called for nearby target"); + } + + @Test + public void testInteractionOutOfRange() { + player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(1.0); + + targetEntity.teleport(new Pos(10, 0, 10)).join(); + ClientInteractEntityPacket packet = new ClientInteractEntityPacket( + targetEntity.getEntityId(), + new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN), + false + ); + + eventWasCalled = false; + UseEntityListener.useEntityListener(packet, player); + assertFalse(eventWasCalled, "Expected PlayerEntityInteractEvent NOT to be called for out-of-range target"); + } + + @Test + public void testInteractionConsideringHitboxAndEyePosition() { + player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(1.5); + + targetEntity.teleport(new Pos(1.6, 0, 0)).join(); + + ClientInteractEntityPacket packet = new ClientInteractEntityPacket( + targetEntity.getEntityId(), + new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN), + false + ); + + eventWasCalled = false; + UseEntityListener.useEntityListener(packet, player); + assertTrue(eventWasCalled, "Expected PlayerEntityInteractEvent to be called considering hitbox size and eye position"); + } + + + @Test + public void testInteractionConsideringEyeHeight() { + player.teleport(new Pos(0, 1.6, 0)).join(); + targetEntity.teleport(new Pos(0, 1.6, 2)).join(); + + ClientInteractEntityPacket packet = new ClientInteractEntityPacket( + targetEntity.getEntityId(), + new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN), + false + ); + + eventWasCalled = false; + UseEntityListener.useEntityListener(packet, player); + assertTrue(eventWasCalled, "Expected PlayerEntityInteractEvent to be called considering eye height"); + } +} diff --git a/src/test/java/net/minestom/server/network/NetworkBufferTest.java b/src/test/java/net/minestom/server/network/NetworkBufferTest.java index 8d782630e..72f2b5fdc 100644 --- a/src/test/java/net/minestom/server/network/NetworkBufferTest.java +++ b/src/test/java/net/minestom/server/network/NetworkBufferTest.java @@ -11,6 +11,8 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.io.UTFDataFormatException; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.Consumer; @@ -474,6 +476,62 @@ public class NetworkBufferTest { assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING)); // oom } + @Test + public void oomStringUtf8Regression() { + var buffer = NetworkBuffer.resizableBuffer(100); + buffer.write(UNSIGNED_SHORT, 65535); // String length + buffer.write(RAW_BYTES, "Hello".getBytes(StandardCharsets.UTF_8)); // String data + + assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING)); // oom + } + + @Test + public void testStringUtf8ModifiedWrite() throws IOException { + var stream = new java.io.ByteArrayOutputStream(); + java.io.DataOutputStream out = new java.io.DataOutputStream(stream); + out.writeUTF("Hello"); + + assertBufferType(STRING_IO_UTF8, "Hello", stream.toByteArray()); + } + + + @Test + public void testStringUtf8ModifiedRead() throws IOException { + var stream = new java.io.ByteArrayOutputStream(); + java.io.DataOutputStream out = new java.io.DataOutputStream(stream); + out.writeUTF("Hello"); + var buffer = NetworkBuffer.wrap(stream.toByteArray(), 0, stream.size()); + assertEquals("Hello", buffer.read(STRING_IO_UTF8)); + } + + @Test + public void oomStringUtf8ModfiedRegression() throws IOException { + var buffer = NetworkBuffer.resizableBuffer(100); + buffer.write(UNSIGNED_SHORT, 65535); // String length + // Write the raw bytes that are invalid + buffer.write(RAW_BYTES, new byte[]{(byte) 0xC0, (byte) 0x80}); // Invalid UTF-8 + + assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING_IO_UTF8)); // oom + buffer.clear(); + + var stream = new java.io.ByteArrayOutputStream(); + java.io.DataOutputStream out = new java.io.DataOutputStream(stream); + out.writeUTF("Hello"); + var byteArray = stream.toByteArray(); + + // Mess with the length to 0 + byteArray[0] = (byte) 0x00; + byteArray[1] = (byte) 0x00; + + assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING_IO_UTF8)); // oom + + buffer.clear(); + buffer.write(UNSIGNED_SHORT, 5); + buffer.write(RAW_BYTES, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); // Invalid utf8 + + assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING_IO_UTF8)); // oom + } + static void assertBufferType(NetworkBuffer.@NotNull Type type, @UnknownNullability T value, byte[] expected, @NotNull Action action) { var buffer = NetworkBuffer.resizableBuffer(MinecraftServer.process()); action.write(buffer, type, value); diff --git a/src/test/java/net/minestom/server/utils/UUIDUtilsTest.java b/src/test/java/net/minestom/server/utils/UUIDUtilsTest.java new file mode 100644 index 000000000..a249954e6 --- /dev/null +++ b/src/test/java/net/minestom/server/utils/UUIDUtilsTest.java @@ -0,0 +1,34 @@ +package net.minestom.server.utils; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class UUIDUtilsTest { + private static final UUID TEST_UUID = UUID.fromString("d2ac7139-76a6-435b-b659-7852d34dd7a3"); + private static final int[] TEST_INT_ARRAY = new int[]{ + 0xd2ac7139, + 0x76a6435b, + 0xb6597852, + 0xd34dd7a3 + }; + + @Test + void isUuid() { + assertTrue(UUIDUtils.isUuid("d2ac7139-76a6-435b-b659-7852d34dd7a3")); + assertFalse(UUIDUtils.isUuid("This is not a UUID")); + assertFalse(UUIDUtils.isUuid("d2acL139-76a6-435b-b659-7852d34dd7a3")); + } + + @Test + void uuidToIntArray() { + assertArrayEquals(TEST_INT_ARRAY, UUIDUtils.uuidToIntArray(TEST_UUID)); + } + + @Test + void intArrayToUuid() { + assertEquals(TEST_UUID, UUIDUtils.intArrayToUuid(TEST_INT_ARRAY)); + } +} \ No newline at end of file diff --git a/src/test/java/net/minestom/server/utils/chunk/ChunkUpdateLimitCheckerTest.java b/src/test/java/net/minestom/server/utils/chunk/ChunkUpdateLimitCheckerTest.java new file mode 100644 index 000000000..32266eea2 --- /dev/null +++ b/src/test/java/net/minestom/server/utils/chunk/ChunkUpdateLimitCheckerTest.java @@ -0,0 +1,54 @@ +package net.minestom.server.utils.chunk; + +import net.minestom.server.instance.DynamicChunk; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnvTest +public class ChunkUpdateLimitCheckerTest { + + @Test + public void testHistory(Env env) { + var instance = env.createFlatInstance(); + var limiter = new ChunkUpdateLimitChecker(3); + + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 2))); + // history : 0, 1, 2 + + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + // history : 1, 2, 0 + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + // history : 2, 0, 1 + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 2))); + // history : 0, 1, 2 + + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 2))); + // history : 1, 2, 2 + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + } + + @Test + public void testOneSlotHistory(Env env) { + var instance = env.createFlatInstance(); + var limiter = new ChunkUpdateLimitChecker(1); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + } + + @Test + public void testDisabling(Env env) { + var instance = env.createFlatInstance(); + var limiter = new ChunkUpdateLimitChecker(0); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + } +}