diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c32bad0c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +tab_width = 4 +max_line_length = off + +[*.java] +ij_java_class_count_to_use_import_on_demand = 9999 +ij_java_doc_align_exception_comments = false +ij_java_doc_align_param_comments = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 036d838ef..ecdb8deed 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,11 +1,12 @@ name: Bug report description: Create a report to help us improve +type: Bug body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report for Geyser! Fill out the following form to your best ability to help us fix the problem. - Only use this if you're absolutely sure that you found a bug and can reproduce it. For anything else, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or the [Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues). + Only use this if you're absolutely sure that you found a bug and can reproduce it. For anything else, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://geysermc.org/wiki/geyser/faq) or the [Common Issues](https://geysermc.org/wiki/geyser/common-issues). - type: textarea attributes: label: Describe the bug diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d0e42478b..872e30b8e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,12 +1,13 @@ name: Feature request description: Suggest an idea for this project labels: "Feature Request" +type: Feature body: - type: markdown attributes: value: | Thanks for taking the time to fill out this feature request for Geyser! Please fill out the following form to your best ability to help us understand your feature request and significantly improve the chance of getting added. - For anything else than a feature request, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or [the Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues). + For anything else than a feature request, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://geysermc.org/wiki/geyser/faq) or the [Common Issues](https://geysermc.org/wiki/geyser/common-issues). - type: textarea attributes: label: What feature do you want to see added? @@ -18,4 +19,4 @@ body: label: Are there any alternatives? description: List any alternatives you might have tried validations: - required: true \ No newline at end of file + required: true diff --git a/.github/workflows/build-remote.yml b/.github/workflows/build-remote.yml new file mode 100644 index 000000000..7cb89cc61 --- /dev/null +++ b/.github/workflows/build-remote.yml @@ -0,0 +1,47 @@ +name: Build Remote + +on: + workflow_call: + inputs: + repository: + required: true + description: 'The repo of the remote' + type: string + ref: + required: true + description: 'The ref of the remote' + type: string + +permissions: {} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Set Build Number + run: | + echo "BUILD_NUMBER=${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + + - name: Setup Gradle + uses: GeyserMC/actions/setup-gradle-composite@master + with: + checkout_repository: ${{ inputs.repository }} + checkout_ref: ${{ inputs.ref }} + setup-java_java-version: 21 + setup-gradle_cache-read-only: true + + - name: Build Geyser + run: ./gradlew build + + - name: Archive Artifacts + uses: GeyserMC/actions/upload-multi-artifact@master + if: success() + with: + artifacts: | + bootstrap/mod/fabric/build/libs/Geyser-Fabric.jar + bootstrap/mod/neoforge/build/libs/Geyser-NeoForge.jar + bootstrap/standalone/build/libs/Geyser-Standalone.jar + bootstrap/spigot/build/libs/Geyser-Spigot.jar + bootstrap/bungeecord/build/libs/Geyser-BungeeCord.jar + bootstrap/velocity/build/libs/Geyser-Velocity.jar + bootstrap/viaproxy/build/libs/Geyser-ViaProxy.jar \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d70d9948..59aa89086 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,149 +7,114 @@ on: - 'gh-readonly-queue/**' paths-ignore: - '.github/ISSUE_TEMPLATE/*.yml' - - '.github/actions/pullrequest.yml' + - '.github/workflows/build-remote.yml' + - '.github/workflows/preview.yml' + - '.github/workflows/pull-request.yml' - '.idea/copyright/*.xml' - '.gitignore' - 'CONTRIBUTING.md' - 'LICENSE' - 'Jenkinsfile ' - 'README.md' - - 'licenseheader.txt' jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout repository and submodules - # See https://github.com/actions/checkout/commits - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Get Release Info + id: release-info + uses: GeyserMC/actions/previous-release@master with: - submodules: recursive + data: ${{ vars.RELEASEACTION_PREVRELEASE }} - - name: Validate Gradle Wrapper - # See https://github.com/gradle/wrapper-validation-action/commits - uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # v2.1.1 + - name: Setup Gradle + uses: GeyserMC/actions/setup-gradle-composite@master + with: + setup-java_java-version: 21 - # See https://github.com/actions/setup-java/commits - - uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 - with: - java-version: 17 - distribution: temurin + - name: Build Geyser + run: ./gradlew build + env: + BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} - - name: Build - # See https://github.com/gradle/gradle-build-action/commits - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 from https://github.com/gradle/actions/commits - with: - arguments: build - gradle-home-cache-cleanup: true - - - name: Archive artifacts (Geyser Fabric) - # See https://github.com/actions/upload-artifact/commits - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + - name: Archive Artifacts + uses: GeyserMC/actions/upload-multi-artifact@master if: success() with: - name: Geyser Fabric - path: bootstrap/mod/fabric/build/libs/Geyser-Fabric.jar - if-no-files-found: error - - name: Archive artifacts (Geyser NeoForge) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser NeoForge - path: bootstrap/mod/neoforge/build/libs/Geyser-NeoForge.jar - if-no-files-found: error - - name: Archive artifacts (Geyser Standalone) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser Standalone - path: bootstrap/standalone/build/libs/Geyser-Standalone.jar - if-no-files-found: error - - name: Archive artifacts (Geyser Spigot) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser Spigot - path: bootstrap/spigot/build/libs/Geyser-Spigot.jar - if-no-files-found: error - - name: Archive artifacts (Geyser BungeeCord) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser BungeeCord - path: bootstrap/bungeecord/build/libs/Geyser-BungeeCord.jar - if-no-files-found: error - - name: Archive artifacts (Geyser Velocity) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser Velocity - path: bootstrap/velocity/build/libs/Geyser-Velocity.jar - if-no-files-found: error - - name: Archive artifacts (Geyser ViaProxy) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser ViaProxy - path: bootstrap/viaproxy/build/libs/Geyser-ViaProxy.jar - if-no-files-found: error + artifacts: | + bootstrap/mod/fabric/build/libs/Geyser-Fabric.jar + bootstrap/mod/neoforge/build/libs/Geyser-NeoForge.jar + bootstrap/standalone/build/libs/Geyser-Standalone.jar + bootstrap/spigot/build/libs/Geyser-Spigot.jar + bootstrap/bungeecord/build/libs/Geyser-BungeeCord.jar + bootstrap/velocity/build/libs/Geyser-Velocity.jar + bootstrap/viaproxy/build/libs/Geyser-ViaProxy.jar - name: Publish to Maven Repository if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + run: ./gradlew publish env: + BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} ORG_GRADLE_PROJECT_geysermcUsername: ${{ vars.DEPLOY_USER }} ORG_GRADLE_PROJECT_geysermcPassword: ${{ secrets.DEPLOY_PASS }} + + - name: Get Version + if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Geyser' }} + id: get-version + run: | + version=$(cat gradle.properties | grep -o "version=[0-9\\.]*" | cut -d"=" -f2) + echo "VERSION=${version}" >> $GITHUB_OUTPUT + + - name: Get Release Metadata + if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Geyser' }} + uses: GeyserMC/actions/release@master + id: metadata with: - arguments: publish + appID: ${{ secrets.RELEASE_APP_ID }} + appPrivateKey: ${{ secrets.RELEASE_APP_PK }} + files: | + bungeecord:bootstrap/bungeecord/build/libs/Geyser-BungeeCord.jar + fabric:bootstrap/mod/fabric/build/libs/Geyser-Fabric.jar + neoforge:bootstrap/mod/neoforge/build/libs/Geyser-NeoForge.jar + spigot:bootstrap/spigot/build/libs/Geyser-Spigot.jar + standalone:bootstrap/standalone/build/libs/Geyser-Standalone.jar + velocity:bootstrap/velocity/build/libs/Geyser-Velocity.jar + viaproxy:bootstrap/viaproxy/build/libs/Geyser-ViaProxy.jar + releaseEnabled: false + saveMetadata: true + releaseProject: 'geyser' + releaseVersion: ${{ steps.get-version.outputs.VERSION }} - name: Publish to Downloads API if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} - shell: bash - env: - DOWNLOADS_USERNAME: ${{ vars.DOWNLOADS_USERNAME }} - DOWNLOADS_PRIVATE_KEY: ${{ secrets.DOWNLOADS_PRIVATE_KEY }} - DOWNLOADS_SERVER_IP: ${{ secrets.DOWNLOADS_SERVER_IP }} - run: | - # Save the private key to a file - echo "$DOWNLOADS_PRIVATE_KEY" > id_ecdsa - chmod 600 id_ecdsa - # Set the project - project=geyser - # Get the version from gradle.properties - version=$(cat gradle.properties | grep -o "version=[0-9\\.]*" | cut -d"=" -f2) - # Create the build folder - ssh -o StrictHostKeyChecking=no -i id_ecdsa $DOWNLOADS_USERNAME@$DOWNLOADS_SERVER_IP mkdir -p "~/uploads/$project/$GITHUB_RUN_NUMBER/" - # Copy over artifacts - rsync -P -e "ssh -o StrictHostKeyChecking=no -i id_ecdsa" bootstrap/**/build/libs/Geyser-*.jar $DOWNLOADS_USERNAME@$DOWNLOADS_SERVER_IP:~/uploads/$project/$GITHUB_RUN_NUMBER/ - rsync -P -e "ssh -o StrictHostKeyChecking=no -i id_ecdsa" bootstrap/mod/**/build/libs/Geyser-*.jar $DOWNLOADS_USERNAME@$DOWNLOADS_SERVER_IP:~/uploads/$project/$GITHUB_RUN_NUMBER/ - # Run the build script - # Push the metadata - echo "{\"project\": \"$project\", \"version\": \"$version\", \"id\": $GITHUB_RUN_NUMBER, \"commit\": \"$GITHUB_SHA\"}" > metadata.json - rsync -P -e "ssh -o StrictHostKeyChecking=no -i id_ecdsa" metadata.json $DOWNLOADS_USERNAME@$DOWNLOADS_SERVER_IP:~/uploads/$project/$GITHUB_RUN_NUMBER/ + uses: GeyserMC/actions/upload-release@master + with: + username: ${{ vars.DOWNLOADS_USERNAME }} + privateKey: ${{ secrets.DOWNLOADS_PRIVATE_KEY }} + host: ${{ secrets.DOWNLOADS_SERVER_IP }} + files: | + bootstrap/bungeecord/build/libs/Geyser-BungeeCord.jar + bootstrap/mod/fabric/build/libs/Geyser-Fabric.jar + bootstrap/mod/neoforge/build/libs/Geyser-NeoForge.jar + bootstrap/spigot/build/libs/Geyser-Spigot.jar + bootstrap/standalone/build/libs/Geyser-Standalone.jar + bootstrap/velocity/build/libs/Geyser-Velocity.jar + bootstrap/viaproxy/build/libs/Geyser-ViaProxy.jar + changelog: ${{ steps.metadata.outputs.body }} - - name: Publish to Modrinth (Fabric) - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + - name: Publish to Modrinth if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} env: + CHANGELOG: ${{ steps.metadata.outputs.body }} + BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - with: - arguments: fabric:modrinth - gradle-home-cache-cleanup: true - - - name: Publish to Modrinth (NeoForge) - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - if: ${{ success() && github.repository == 'GeyserMC/Geyser' && github.ref_name == 'master' }} - env: - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - with: - arguments: neoforge:modrinth - gradle-home-cache-cleanup: true + run: ./gradlew modrinth - name: Notify Discord if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Geyser' }} - # See https://github.com/Tim203/actions-git-discord-webhook/commits - uses: Tim203/actions-git-discord-webhook@70f38ded3aca51635ec978ab4e1a58cd4cd0c2ff + uses: GeyserMC/actions/notify-discord@master with: - webhook_url: ${{ secrets.DISCORD_WEBHOOK }} + discordWebhook: ${{ secrets.DISCORD_WEBHOOK }} status: ${{ job.status }} + body: ${{ steps.metadata.outputs.body }} + includeDownloads: ${{ github.ref_name == 'master' }} diff --git a/.github/workflows/dispatch-preview.yml b/.github/workflows/dispatch-preview.yml new file mode 100644 index 000000000..83df08e37 --- /dev/null +++ b/.github/workflows/dispatch-preview.yml @@ -0,0 +1,33 @@ +name: Dispatch Preview + +on: + workflow_dispatch: + inputs: + runId: + required: true + description: 'ID of the action to pull artifacts from' + build: + required: true + description: 'Build number for the release' + version: + required: true + description: 'Version under which to upload to the Downloads API' + +jobs: + dispatch-preview: + # Allow access to secrets if we are uploading a preview + secrets: inherit + uses: GeyserMC/actions/.github/workflows/upload-preview.yml@master + with: + build: ${{ inputs.build }} + version: ${{ inputs.version }} + files: | + bungeecord:Geyser-BungeeCord.jar + fabric:Geyser-Fabric.jar + neoforge:Geyser-NeoForge.jar + spigot:Geyser-Spigot.jar + standalone:Geyser-Standalone.jar + velocity:Geyser-Velocity.jar + viaproxy:Geyser-ViaProxy.jar + project: geyserpreview + runId: ${{ inputs.runId }} \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 000000000..6167bb18e --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,34 @@ +name: Process Pull Request + +on: + pull_request_target: + +jobs: + build: + # Forbid access to secrets nor GH Token perms while building the PR + permissions: {} + secrets: {} + uses: GeyserMC/Geyser/.github/workflows/build-remote.yml@master + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + preview: + needs: [build] + if: >- + contains(github.event.pull_request.labels.*.name, 'PR: Needs Testing') + # Allow access to secrets if we are uploading a preview + secrets: inherit + uses: GeyserMC/actions/.github/workflows/upload-preview.yml@master + with: + build: ${{ github.run_number }} + version: pr.${{ github.event.pull_request.number }} + files: | + bungeecord:Geyser-BungeeCord.jar + fabric:Geyser-Fabric.jar + neoforge:Geyser-NeoForge.jar + spigot:Geyser-Spigot.jar + standalone:Geyser-Standalone.jar + velocity:Geyser-Velocity.jar + viaproxy:Geyser-ViaProxy.jar + project: geyserpreview + runId: ${{ github.run_id }} \ No newline at end of file diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml deleted file mode 100644 index 93be4711e..000000000 --- a/.github/workflows/pullrequest.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Build Pull Request - -on: - pull_request: - merge_group: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Set up JDK 17 - # See https://github.com/actions/setup-java/commits - uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 - with: - java-version: 17 - distribution: temurin - - - name: Check if the author has forked the API repo - # See https://github.com/Kas-tle/find-forks-action/commits - uses: Kas-tle/find-forks-action@1b5447d1e3c7a8ed79583dd817cc5399686eed3a - id: find_forks - with: - owner: GeyserMC - repo: api - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Use author's API repo if it exists - if: ${{ steps.find_forks.outputs.target_branch_found == 'true' }} - env: - API_FORK_URL: ${{ steps.find_forks.outputs.user_fork_url }} - API_FORK_BRANCH: ${{ github.event.pull_request.head.ref }} - run: | - git clone "${API_FORK_URL}" --single-branch --branch "${API_FORK_BRANCH}" api - cd api - ./gradlew publishToMavenLocal - - - name: Checkout repository and submodules - # See https://github.com/actions/checkout/commits - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - submodules: recursive - path: geyser - - - name: Validate Gradle Wrapper - # See https://github.com/gradle/wrapper-validation-action/commits - uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # v2.1.1 - - - name: Build Geyser - # See https://github.com/gradle/gradle-build-action/commits - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 from https://github.com/gradle/actions/commits - with: - arguments: build - build-root-directory: geyser - - - name: Archive artifacts (Geyser Fabric) - # See https://github.com/actions/upload-artifact/commits - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 - if: success() - with: - name: Geyser Fabric - path: geyser/bootstrap/mod/fabric/build/libs/Geyser-Fabric.jar - if-no-files-found: error - - name: Archive artifacts (Geyser NeoForge) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser NeoForge - path: geyser/bootstrap/mod/neoforge/build/libs/Geyser-NeoForge.jar - if-no-files-found: error - - name: Archive artifacts (Geyser Standalone) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser Standalone - path: geyser/bootstrap/standalone/build/libs/Geyser-Standalone.jar - if-no-files-found: error - - name: Archive artifacts (Geyser Spigot) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser Spigot - path: geyser/bootstrap/spigot/build/libs/Geyser-Spigot.jar - if-no-files-found: error - - name: Archive artifacts (Geyser BungeeCord) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser BungeeCord - path: geyser/bootstrap/bungeecord/build/libs/Geyser-BungeeCord.jar - if-no-files-found: error - - name: Archive artifacts (Geyser Velocity) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser Velocity - path: geyser/bootstrap/velocity/build/libs/Geyser-Velocity.jar - if-no-files-found: error - - name: Archive artifacts (Geyser ViaProxy) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 - if: success() - with: - name: Geyser ViaProxy - path: geyser/bootstrap/viaproxy/build/libs/Geyser-ViaProxy.jar - if-no-files-found: error diff --git a/.gitignore b/.gitignore index a44afd242..aff61aa60 100644 --- a/.gitignore +++ b/.gitignore @@ -249,6 +249,8 @@ locales/ /packs/ /dump.json /saved-refresh-tokens.json +/saved-auth-chains.json /custom_mappings/ /languages/ -/custom-skulls.yml \ No newline at end of file +/custom-skulls.yml +/permissions.yml diff --git a/.idea/copyright/Geyser.xml b/.idea/copyright/Geyser.xml index c6b553aaf..758c31cbd 100644 --- a/.idea/copyright/Geyser.xml +++ b/.idea/copyright/Geyser.xml @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/LICENSE b/LICENSE index bde252698..f7c7b3f8a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2023 GeyserMC. http://geysermc.org +Copyright (c) 2019-2025 GeyserMC. http://geysermc.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8a28e7177..b3a785a9d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Discord](https://img.shields.io/discord/613163671870242838.svg?color=%237289da&label=discord)](https://discord.gg/geysermc) -[![Crowdin](https://badges.crowdin.net/geyser/localized.svg)](https://translate.geysermc.org/) +[![Crowdin](https://badges.crowdin.net/e/51361b7f8a01644a238d0fe8f3bddc62/localized.svg)](https://translate.geysermc.org/) Geyser is a bridge between Minecraft: Bedrock Edition and Minecraft: Java Edition, closing the gap from those wanting to play true cross-platform. @@ -14,16 +14,15 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here! -### Currently supporting Minecraft Bedrock 1.20.40 - 1.20.72 and Minecraft Java 1.20.4 +## Supported Versions +Geyser is currently supporting Minecraft Bedrock 1.21.90 - 1.21.113 and Minecraft Java 1.21.9 - 1.21.10. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). ## Setting Up -Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. - -[![YouTube Video](https://img.youtube.com/vi/U7dZZ8w7Gi4/0.jpg)](https://www.youtube.com/watch?v=U7dZZ8w7Gi4) +Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser. ## Links: - Website: https://geysermc.org -- Docs: https://wiki.geysermc.org/geyser/ +- Docs: https://geysermc.org/wiki/geyser/ - Download: https://geysermc.org/download - Discord: https://discord.gg/geysermc - Donate: https://opencollective.com/geysermc @@ -32,10 +31,9 @@ Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Ge ## What's Left to be Added/Fixed - Near-perfect movement (to the point where anticheat on large servers is unlikely to ban you) - Some Entity Flags -- Structure block UI ## What can't be fixed -There are a few things Geyser is unable to support due to various differences between Minecraft Bedrock and Java. For a list of these limitations, see the [Current Limitations](https://wiki.geysermc.org/geyser/current-limitations/) page. +There are a few things Geyser is unable to support due to various differences between Minecraft Bedrock and Java. For a list of these limitations, see the [Current Limitations](https://geysermc.org/wiki/geyser/current-limitations/) page. ## Compiling 1. Clone the repo to your computer @@ -43,12 +41,12 @@ There are a few things Geyser is unable to support due to various differences be 3. Run `gradlew build` and locate to `bootstrap/build` folder. ## Contributing -Any contributions are appreciated. Please feel free to reach out to us on [Discord](http://discord.geysermc.org/) if +Any contributions are appreciated. Please feel free to reach out to us on [Discord](https://discord.gg/geysermc) if you're interested in helping out with Geyser. ## Libraries Used: - [Adventure Text Library](https://github.com/KyoriPowered/adventure) -- [NukkitX Bedrock Protocol Library](https://github.com/NukkitX/Protocol) -- [Steveice10's Java Protocol Library](https://github.com/Steveice10/MCProtocolLib) +- [CloudburstMC Bedrock Protocol Library](https://github.com/CloudburstMC/Protocol) +- [GeyserMC's Java Protocol Library](https://github.com/GeyserMC/MCProtocolLib) - [TerminalConsoleAppender](https://github.com/Minecrell/TerminalConsoleAppender) - [Simple Logging Facade for Java (slf4j)](https://github.com/qos-ch/slf4j) diff --git a/ap/build.gradle.kts b/ap/build.gradle.kts index e69de29bb..6c456c21b 100644 --- a/ap/build.gradle.kts +++ b/ap/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("geyser.base-conventions") +} diff --git a/api/build.gradle.kts b/api/build.gradle.kts index bd54a9ce4..eac02ebeb 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,8 +1,24 @@ plugins { + // Allow blossom to mark sources root of templates + idea id("geyser.publish-conventions") + alias(libs.plugins.blossom) } dependencies { api(libs.base.api) api(libs.math) -} \ No newline at end of file +} + +version = property("version")!! +val apiVersion = (version as String).removeSuffix("-SNAPSHOT") + +sourceSets { + main { + blossom { + javaSources { + property("version", apiVersion) + } + } + } +} diff --git a/api/src/main/java-templates/org/geysermc/geyser/api/BuildData.java b/api/src/main/java-templates/org/geysermc/geyser/api/BuildData.java new file mode 100644 index 000000000..f9a580e7b --- /dev/null +++ b/api/src/main/java-templates/org/geysermc/geyser/api/BuildData.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api; + +import org.geysermc.api.util.ApiVersion; + +/** + * Not a public API. For internal use only. May change without notice. + * This class is processed before compilation to insert build properties. + */ +class BuildData { + static final String VERSION = "{{ version }}"; + static final ApiVersion API_VERSION; + + static { + String[] parts = VERSION.split("\\."); + if (parts.length != 3) { + throw new RuntimeException("Invalid api version: " + VERSION); + } + + try { + int human = Integer.parseInt(parts[0]); + int major = Integer.parseInt(parts[1]); + int minor = Integer.parseInt(parts[2]); + API_VERSION = new ApiVersion(human, major, minor); + } catch (Exception e) { + throw new RuntimeException("Invalid api version: " + VERSION, e); + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java index a9327d0db..5c20d06e1 100644 --- a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java +++ b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java @@ -29,6 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.api.Geyser; import org.geysermc.api.GeyserApiBase; +import org.geysermc.api.util.ApiVersion; import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.EventBus; @@ -169,4 +170,14 @@ public interface GeyserApi extends GeyserApiBase { static GeyserApi api() { return Geyser.api(GeyserApi.class); } + + /** + * Returns the {@link ApiVersion} representing the current Geyser api version. + * See the Geyser version outline) + * + * @return the current geyser api version + */ + default ApiVersion geyserApiVersion() { + return BuildData.API_VERSION; + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java index 2f715fa1e..f208879d1 100644 --- a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java +++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java @@ -145,4 +145,36 @@ public interface CameraData { * @return whether the camera is currently locked */ boolean isCameraLocked(); -} \ No newline at end of file + + /** + * Hides a {@link GuiElement} on the client's side. + * + * @param element the {@link GuiElement} to hide + */ + void hideElement(@NonNull GuiElement... element); + + /** + * Resets a {@link GuiElement} on the client's side. + * This makes the client decide on its own - e.g. based on client settings - + * whether to show or hide the gui element. + *

+ * If no elements are specified, this will reset all currently hidden elements + * + * @param element the {@link GuiElement} to reset + */ + void resetElement(@NonNull GuiElement @Nullable... element); + + /** + * Determines whether a {@link GuiElement} is currently hidden. + * + * @param element the {@link GuiElement} to check + */ + boolean isHudElementHidden(@NonNull GuiElement element); + + /** + * Returns the currently hidden {@link GuiElement}s. + * + * @return an unmodifiable view of all currently hidden {@link GuiElement}s + */ + @NonNull Set hiddenElements(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/GuiElement.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/GuiElement.java new file mode 100644 index 000000000..4d3653648 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/GuiElement.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.bedrock.camera; + +/** + * Represent GUI elements on the players HUD display. + * These can be hidden using {@link CameraData#hideElement(GuiElement...)}, + * and one can reset their visibility using {@link CameraData#resetElement(GuiElement...)}. + */ +public class GuiElement { + public static final GuiElement PAPER_DOLL = new GuiElement(0); + public static final GuiElement ARMOR = new GuiElement(1); + public static final GuiElement TOOL_TIPS = new GuiElement(2); + public static final GuiElement TOUCH_CONTROLS = new GuiElement(3); + public static final GuiElement CROSSHAIR = new GuiElement(4); + public static final GuiElement HOTBAR = new GuiElement(5); + public static final GuiElement HEALTH = new GuiElement(6); + public static final GuiElement PROGRESS_BAR = new GuiElement(7); + public static final GuiElement FOOD_BAR = new GuiElement(8); + public static final GuiElement AIR_BUBBLES_BAR = new GuiElement(9); + public static final GuiElement VEHICLE_HEALTH = new GuiElement(10); + public static final GuiElement EFFECTS_BAR = new GuiElement(11); + public static final GuiElement ITEM_TEXT_POPUP = new GuiElement(12); + + private GuiElement(int id) { + this.id = id; + } + + private final int id; + + /** + * Internal use only; don't depend on these values being consistent. + */ + public int id() { + return this.id; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java deleted file mode 100644 index 5143246d9..000000000 --- a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.geysermc.geyser.api.block.custom.nonvanilla; - -import org.checkerframework.checker.index.qual.NonNegative; -import org.checkerframework.checker.nullness.qual.NonNull; - -public record JavaBlockItem(@NonNull String identifier, @NonNegative int javaId, @NonNegative int stackSize) { -} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java index f7da4b932..c1950f965 100644 --- a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java @@ -59,7 +59,9 @@ public interface JavaBlockState { * Gets the pick item of the block state * * @return the pick item of the block state + * @deprecated the pick item is sent by the Java server */ + @Deprecated @Nullable String pickItem(); /** @@ -70,10 +72,13 @@ public interface JavaBlockState { @Nullable String pistonBehavior(); /** - * Gets whether the block state has block entity + * Gets whether the block state has a block entity * * @return whether the block state has block entity + * @deprecated Does not have an effect. If you were using this to + * set piston behavior, use {@link #pistonBehavior()} instead. */ + @Deprecated(forRemoval = true) boolean hasBlockEntity(); /** @@ -100,10 +105,16 @@ public interface JavaBlockState { Builder canBreakWithHand(boolean canBreakWithHand); + @Deprecated Builder pickItem(@Nullable String pickItem); Builder pistonBehavior(@Nullable String pistonBehavior); + /** + * @deprecated Does not have an effect. If you were using this to + * * set piston behavior, use {@link #pistonBehavior(String)} instead. + */ + @Deprecated(forRemoval = true) Builder hasBlockEntity(boolean hasBlockEntity); JavaBlockState build(); diff --git a/api/src/main/java/org/geysermc/geyser/api/command/Command.java b/api/src/main/java/org/geysermc/geyser/api/command/Command.java index 2f1f2b24d..29922ae1e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/command/Command.java +++ b/api/src/main/java/org/geysermc/geyser/api/command/Command.java @@ -28,7 +28,9 @@ package org.geysermc.geyser.api.command; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.TriState; import java.util.Collections; import java.util.List; @@ -58,15 +60,15 @@ public interface Command { * Gets the permission node associated with * this command. * - * @return the permission node for this command + * @return the permission node for this command if defined, otherwise an empty string */ @NonNull String permission(); /** - * Gets the aliases for this command. + * Gets the aliases for this command, as an unmodifiable list * - * @return the aliases for this command + * @return the aliases for this command as an unmodifiable list */ @NonNull List aliases(); @@ -75,35 +77,39 @@ public interface Command { * Gets if this command is designed to be used only by server operators. * * @return if this command is designated to be used only by server operators. + * @deprecated this method is not guaranteed to provide meaningful or expected results. */ - boolean isSuggestedOpOnly(); - - /** - * Gets if this command is executable on console. - * - * @return if this command is executable on console - */ - boolean isExecutableOnConsole(); - - /** - * Gets the subcommands associated with this - * command. Mainly used within the Geyser Standalone - * GUI to know what subcommands are supported. - * - * @return the subcommands associated with this command - */ - @NonNull - default List subCommands() { - return Collections.emptyList(); + @Deprecated(forRemoval = true) + default boolean isSuggestedOpOnly() { + return false; } /** - * Used to send a deny message to Java players if this command can only be used by Bedrock players. - * - * @return true if this command can only be used by Bedrock players. + * @return true if this command is executable on console + * @deprecated use {@link #isPlayerOnly()} instead (inverted) */ - default boolean isBedrockOnly() { - return false; + @Deprecated(forRemoval = true) + default boolean isExecutableOnConsole() { + return !isPlayerOnly(); + } + + /** + * @return true if this command can only be used by players + */ + boolean isPlayerOnly(); + + /** + * @return true if this command can only be used by Bedrock players + */ + boolean isBedrockOnly(); + + /** + * @deprecated this method will always return an empty immutable list + */ + @Deprecated(forRemoval = true) + @NonNull + default List subCommands() { + return Collections.emptyList(); } /** @@ -128,7 +134,7 @@ public interface Command { * is an instance of this source. * * @param sourceType the source type - * @return the builder + * @return this builder */ Builder source(@NonNull Class sourceType); @@ -136,7 +142,7 @@ public interface Command { * Sets the command name. * * @param name the command name - * @return the builder + * @return this builder */ Builder name(@NonNull String name); @@ -144,23 +150,40 @@ public interface Command { * Sets the command description. * * @param description the command description - * @return the builder + * @return this builder */ Builder description(@NonNull String description); /** - * Sets the permission node. + * Sets the permission node required to run this command.
+ * It will not be registered with any permission registries, such as an underlying server, + * or a permissions Extension (unlike {@link #permission(String, TriState)}). * * @param permission the permission node - * @return the builder + * @return this builder */ Builder permission(@NonNull String permission); + /** + * Sets the permission node and its default value. The usage of the default value is platform dependant + * and may or may not be used. For example, it may be registered to an underlying server. + *

+ * Extensions may instead listen for {@link GeyserRegisterPermissionsEvent} to register permissions, + * especially if the same permission is required by multiple commands. Also see this event for TriState meanings. + * + * @param permission the permission node + * @param defaultValue the node's default value + * @return this builder + * @deprecated this method is experimental and may be removed in the future + */ + @Deprecated + Builder permission(@NonNull String permission, @NonNull TriState defaultValue); + /** * Sets the aliases. * * @param aliases the aliases - * @return the builder + * @return this builder */ Builder aliases(@NonNull List aliases); @@ -168,46 +191,62 @@ public interface Command { * Sets if this command is designed to be used only by server operators. * * @param suggestedOpOnly if this command is designed to be used only by server operators - * @return the builder + * @return this builder + * @deprecated this method is not guaranteed to produce meaningful or expected results */ + @Deprecated(forRemoval = true) Builder suggestedOpOnly(boolean suggestedOpOnly); /** * Sets if this command is executable on console. * * @param executableOnConsole if this command is executable on console - * @return the builder + * @return this builder + * @deprecated use {@link #isPlayerOnly()} instead (inverted) */ + @Deprecated(forRemoval = true) Builder executableOnConsole(boolean executableOnConsole); + /** + * Sets if this command can only be executed by players. + * + * @param playerOnly if this command is player only + * @return this builder + */ + Builder playerOnly(boolean playerOnly); + + /** + * Sets if this command can only be executed by bedrock players. + * + * @param bedrockOnly if this command is bedrock only + * @return this builder + */ + Builder bedrockOnly(boolean bedrockOnly); + /** * Sets the subcommands. * * @param subCommands the subcommands - * @return the builder + * @return this builder + * @deprecated this method has no effect */ - Builder subCommands(@NonNull List subCommands); - - /** - * Sets if this command is bedrock only. - * - * @param bedrockOnly if this command is bedrock only - * @return the builder - */ - Builder bedrockOnly(boolean bedrockOnly); + @Deprecated(forRemoval = true) + default Builder subCommands(@NonNull List subCommands) { + return this; + } /** * Sets the {@link CommandExecutor} for this command. * * @param executor the command executor - * @return the builder + * @return this builder */ Builder executor(@NonNull CommandExecutor executor); /** * Builds the command. * - * @return the command + * @return a new command from this builder */ @NonNull Command build(); diff --git a/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java b/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java index 45276e2c4..c1453f579 100644 --- a/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java +++ b/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java @@ -26,6 +26,10 @@ package org.geysermc.geyser.api.command; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.connection.GeyserConnection; + +import java.util.UUID; /** * Represents an instance capable of sending commands. @@ -64,6 +68,17 @@ public interface CommandSource { */ boolean isConsole(); + /** + * @return a Java UUID if this source represents a player, otherwise null + */ + @Nullable UUID playerUuid(); + + /** + * @return a GeyserConnection if this source represents a Bedrock player that is connected + * to this Geyser instance, otherwise null + */ + @Nullable GeyserConnection connection(); + /** * Returns the locale of the command source. * diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index 9bda4f903..23971305d 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.api.connection; import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.api.connection.Connection; @@ -35,8 +36,11 @@ import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.entity.EntityData; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; +import org.geysermc.geyser.api.skin.SkinData; +import java.util.NoSuchElementException; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; /** @@ -60,6 +64,108 @@ public interface GeyserConnection extends Connection, CommandSource { */ @NonNull EntityData entities(); + /** + * Returns the current ping of the connection. + */ + int ping(); + + /** + * @return {@code true} if the client currently has a form open. + * @since 2.8.0 + */ + boolean hasFormOpen(); + + /** + * Closes the currently open form on the client. + */ + void closeForm(); + + /** + * Gets the Bedrock protocol version of the player. + */ + int protocolVersion(); + + /** + * Attempts to open the {@code minecraft:pause_screen_additions} dialog tag. This method opens this dialog the same way Java does, that is: + * + *

+ * + *

Use {@link GeyserConnection#hasFormOpen()} to check if a dialog was opened.

+ * @since 2.8.0 + */ + void openPauseScreenAdditions(); + + /** + * Attempts to open the {@code minecraft:quick_actions} dialog tag. This method opens this dialog the same way Java does, that is: + * + * + * + *

Use {@link GeyserConnection#hasFormOpen()} to check if a dialog was opened.

+ * @since 2.8.0 + */ + void openQuickActions(); + + /** + * Sends a command as if the player had executed it. + * + * @param command the command without the leading forward-slash + * @since 2.8.0 + */ + void sendCommand(String command); + + /** + * Gets the hostname or ip address the player used to join this Geyser instance. + * Example: + * + * + * @throws NoSuchElementException if called before the session is fully initialized + * @return the ip address or hostname string the player used to join + * @since 2.8.3 + */ + @NonNull + String joinAddress(); + + /** + * Gets the port the player used to join this Geyser instance. + * Example: + * + * + * @throws NoSuchElementException if called before the session is fully initialized + * @return the port the player used to join + * @since 2.8.3 + */ + @Positive + int joinPort(); + + /** + * Applies a skin to a player seen by this Geyser connection. + * If the uuid matches the {@link GeyserConnection#javaUuid()}, this + * will update the skin of this Geyser connection. + * If the player uuid provided is not known to this connection, this method + * will silently return. + * + * @param player which player this skin should be applied to + * @param skinData the skin data to apply + * @since 2.8.3 + */ + void sendSkin(@NonNull UUID player, @NonNull SkinData skinData); + /** * @param javaId the Java entity ID to look up. * @return a {@link GeyserEntity} if present in this connection's entity tracker. diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java index 90b3fc821..48c717089 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java @@ -81,4 +81,10 @@ public interface EntityData { * @return whether the movement is locked */ boolean isMovementLocked(); + + /** + * Sends a request to the Java server to switch the items in the main and offhand. + * There is no guarantee of the server accepting the request. + */ + void switchHands(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java new file mode 100644 index 000000000..308d23cd1 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.entity.property; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; + +/** + * Collects property changes to be applied as a single, batched update to an entity. + *

+ * Notes: + *

+ * + *
{@code
+ * entity.updatePropertiesBatched(updater -> {
+ *     updater.update(SOME_FLOAT_PROPERTY, 0.15f);
+ *     updater.update(SOME_BOOLEAN_PROPERTY, true);
+ *     updater.update(SOME_INT_PROPERTY, null); // reset to default
+ * });
+ * }
+ * + * @since 2.9.0 + */ +@FunctionalInterface +public interface BatchPropertyUpdater { + + /** + * Queues an update for the given property within the current batch. + *

+ * If {@code value} is {@code null}, the property will be reset to its default value + * as declared when the property was registered during the {@link GeyserDefineEntityPropertiesEvent}. + * + * @param property a {@link GeyserEntityProperty} registered for the target entity type + * @param value the new value, or {@code null} to reset to the default + * @param the property's value type + * + * @since 2.9.0 + */ + void update(@NonNull GeyserEntityProperty property, @Nullable T value); +} diff --git a/core/src/main/java/org/geysermc/geyser/util/JavaCodecUtil.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java similarity index 51% rename from core/src/main/java/org/geysermc/geyser/util/JavaCodecUtil.java rename to api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java index 795d45490..9489a9946 100644 --- a/core/src/main/java/org/geysermc/geyser/util/JavaCodecUtil.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,42 +23,43 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.util; +package org.geysermc.geyser.api.entity.property; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.ListTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.util.Identifier; -import java.util.Iterator; - -public final class JavaCodecUtil { +/** + * Represents a property that can be attached to an entity. + *

+ * Entity properties are used to describe metadata about an entity, such as + * integers, floats, booleans, or enums. + * @see + * Official documentation for info + * + * @param the type of value stored by this property + * + * @since 2.9.0 + */ +public interface GeyserEntityProperty { /** - * Iterate over a Java Edition codec and return each entry as a CompoundTag + * Gets the unique name of this property. + * Custom properties cannot use the vanilla namespace + * to avoid collisions with vanilla entity properties. + * + * @return the property identifier + * @since 2.9.0 */ - public static Iterable iterateAsTag(CompoundTag tag) { - ListTag value = tag.get("value"); - Iterator originalIterator = value.iterator(); - return new Iterable<>() { - @NonNull - @Override - public Iterator iterator() { - return new Iterator<>() { - @Override - public boolean hasNext() { - return originalIterator.hasNext(); - } + @NonNull + Identifier identifier(); - @Override - public CompoundTag next() { - return (CompoundTag) originalIterator.next(); - } - }; - } - }; - } - - private JavaCodecUtil() { - } + /** + * Gets the default value of this property which + * is set upon spawning entities. + * + * @return the default value of this property + * @since 2.9.0 + */ + @NonNull + T defaultValue(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java new file mode 100644 index 000000000..1c988d5d7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; + +/** + * Represents a boolean entity property. + * @since 2.9.0 + */ +public interface GeyserBooleanEntityProperty extends GeyserEntityProperty { +} diff --git a/core/src/main/java/org/geysermc/geyser/util/collection/package-info.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java similarity index 65% rename from core/src/main/java/org/geysermc/geyser/util/collection/package-info.java rename to api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java index 46fa5df11..283111c7f 100644 --- a/core/src/main/java/org/geysermc/geyser/util/collection/package-info.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,12 +23,20 @@ * @link https://github.com/GeyserMC/Geyser */ +package org.geysermc.geyser.api.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; + /** - * Contains useful collections for use in Geyser. - *

- * Of note are the fixed int maps. Designed for use with block states that are positive and sequential, they do not allow keys to be - * added that are not greater by one versus the previous key. Because of this, speedy operations of {@link java.util.Map#get(java.lang.Object)} - * and {@link java.util.Map#containsKey(java.lang.Object)} can be performed by simply checking the bounds of the map - * size and its "start" integer. + * Represents a Java enum-backed enum property. + * There are a few key limitations: + *

+ * + * @param the enum type + * @since 2.9.0 */ -package org.geysermc.geyser.util.collection; \ No newline at end of file +public interface GeyserEnumEntityProperty> extends GeyserEntityProperty { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java new file mode 100644 index 000000000..8337e09fa --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; +import org.geysermc.geyser.api.util.Identifier; + +/** + * Represents a float-backed entity property with inclusive bounds. + * Values associated with this property must be always within the {@code [min(), max()]} bounds. + * + * @see GeyserDefineEntityPropertiesEvent#registerFloatProperty(Identifier, Identifier, float, float, Float) + * @since 2.9.0 + */ +public interface GeyserFloatEntityProperty extends GeyserEntityProperty { + + /** + * @return the inclusive lower bound for this property + * @since 2.9.0 + */ + float min(); + + /** + * @return the inclusive upper bound for this property + * @since 2.9.0 + */ + float max(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java new file mode 100644 index 000000000..36c4996bd --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; +import org.geysermc.geyser.api.util.Identifier; + +/** + * Represents an int-backed entity property with inclusive bounds. + * There are a few key limitations: + *
    + *
  • Values must be always within the {@code [min(), max()]} bounds
  • + *
  • Molang evaluation uses floats under the hood; very large integers can lose precision. + * Prefer keeping values in a practical range to avoid rounding issues.
  • + *
+ * + * @see GeyserDefineEntityPropertiesEvent#registerIntegerProperty(Identifier, Identifier, int, int, Integer) + * @since 2.9.0 + */ +public interface GeyserIntEntityProperty extends GeyserEntityProperty { + + /** + * @return the inclusive lower bound for this property + * @since 2.9.0 + */ + int min(); + + /** + * @return the inclusive upper bound for this property + * @since 2.9.0 + */ + int max(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java new file mode 100644 index 000000000..ebd541ef7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; + +import java.util.List; + +/** + * Represents a string-backed enum property. + * There are a few key limitations: + *
    + *
  • There cannot be more than 16 values
  • + *
  • The values' names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores
  • + *
+ * + * @since 2.9.0 + */ +public interface GeyserStringEnumProperty extends GeyserEntityProperty { + + /** + * @return an unmodifiable list of all registered values + * @since 2.9.0 + */ + List values(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java index 02acb4e21..1f8698518 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java @@ -26,9 +26,17 @@ package org.geysermc.geyser.api.entity.type; import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.entity.property.BatchPropertyUpdater; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; + +import java.util.function.Consumer; /** - * Represents a unique instance of an entity. Each {@link org.geysermc.geyser.api.connection.GeyserConnection} + * Represents a unique instance of an entity. Each {@link GeyserConnection} * have their own sets of entities - no two instances will share the same GeyserEntity instance. */ public interface GeyserEntity { @@ -37,4 +45,24 @@ public interface GeyserEntity { */ @NonNegative int javaId(); + + /** + * Updates an entity property with a new value. + * If the new value is null, the property is reset to the default value. + * + * @param property a {@link GeyserEntityProperty} registered for this type in the {@link GeyserDefineEntityPropertiesEvent} + * @param value the new property value + * @param the type of the value + * @since 2.9.0 + */ + default void updateProperty(@NonNull GeyserEntityProperty property, @Nullable T value) { + this.updatePropertiesBatched(consumer -> consumer.update(property, value)); + } + + /** + * Updates multiple properties with just one update packet. + * @see BatchPropertyUpdater + * @since 2.9.0 + */ + void updatePropertiesBatched(Consumer consumer); } diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java index bba4dbf3e..d31def996 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java @@ -31,9 +31,9 @@ import org.geysermc.geyser.api.entity.type.GeyserEntity; public interface GeyserPlayerEntity extends GeyserEntity { /** - * Gets the position of the player. + * Gets the position of the player, as it is known to the Java server. * - * @return the position of the player. + * @return the player's position */ Vector3f position(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionAcceptCodeOfConductEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionAcceptCodeOfConductEvent.java new file mode 100644 index 000000000..0df79765e --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionAcceptCodeOfConductEvent.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.bedrock; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; +import org.geysermc.geyser.api.event.java.ServerCodeOfConductEvent; + +/** + * Fired when a player accepts a code of conduct sent by the Java server. API users can listen to this event + * to store the acceptance in a cache, and tell Geyser not to do so. + * + *

Java clients cache acceptance locally, but bedrock clients don't. Normally Geyser uses a simple JSON file to implement this, + * but an alternative solution may be preferred when using multiple Geyser instances. Such a solution can be implemented through this event and {@link ServerCodeOfConductEvent}.

+ * + * @see ServerCodeOfConductEvent + * @since 2.9.0 + */ +public class SessionAcceptCodeOfConductEvent extends ConnectionEvent { + private final String codeOfConduct; + private boolean skipSaving = false; + + public SessionAcceptCodeOfConductEvent(@NonNull GeyserConnection connection, String codeOfConduct) { + super(connection); + this.codeOfConduct = codeOfConduct; + } + + /** + * @return the code of conduct sent by the server + * @since 2.9.0 + */ + public String codeOfConduct() { + return codeOfConduct; + } + + /** + * @return {@code true} if Geyser should not save the acceptance of the code of conduct in its own cache (through a JSON file), because it was saved elsewhere + * @since 2.9.0 + */ + public boolean shouldSkipSaving() { + return skipSaving; + } + + /** + * Sets {@link SessionAcceptCodeOfConductEvent#shouldSkipSaving()} to {@code true}. + * @since 2.9.0 + */ + public void skipSaving() { + this.skipSaving = true; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java index 05e3415a0..f97f32f92 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java @@ -50,7 +50,7 @@ public class SessionDisconnectEvent extends ConnectionEvent { } /** - * Sets the disconnect reason, thereby overriding th original reason. + * Sets the disconnect message shown to the Bedrock client. * * @param disconnectReason the reason for the disconnect */ diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java index ab2088c00..0228214a7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java @@ -31,6 +31,7 @@ import org.geysermc.geyser.api.event.connection.ConnectionEvent; /** * Called when Geyser session connected to a Java remote server and is in a play-ready state. + * @since 2.1.1 */ public final class SessionJoinEvent extends ConnectionEvent { public SessionJoinEvent(@NonNull GeyserConnection connection) { diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index c2f1cd427..9ea0a04a3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -26,15 +26,20 @@ package org.geysermc.geyser.api.event.bedrock; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import java.util.Collection; import java.util.List; import java.util.UUID; /** - * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending resource packs. + * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending {@link ResourcePack}'s. + * @since 2.1.1 */ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { @@ -42,26 +47,80 @@ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { } /** - * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to the client. + * Gets the {@link ResourcePack}'s that will be sent to this {@link GeyserConnection}. + * To remove packs, use {@link #unregister(UUID)}, as the list returned + * by this method is unmodifiable. * - * @return an unmodifiable list of resource packs that will be sent to the client. + * @return an unmodifiable list of {@link ResourcePack}'s + * @since 2.1.1 */ public abstract @NonNull List resourcePacks(); /** - * Registers a {@link ResourcePack} to be sent to the client. - * - * @param resourcePack a resource pack that will be sent to the client. - * @return true if the resource pack was added successfully, - * or false if already present + * @deprecated Use {{@link #register(ResourcePack, ResourcePackOption[])}} instead */ - public abstract boolean register(@NonNull ResourcePack resourcePack); + @Deprecated + public abstract boolean register(@NonNull ResourcePack pack); /** - * Unregisters a resource pack from being sent to the client. + * Registers a {@link ResourcePack} to be sent to the client, optionally alongside + * specific {@link ResourcePackOption}'s specifying how it will be applied by the client. * - * @param uuid the UUID of the resource pack - * @return true whether the resource pack was removed from the list of resource packs. + * @param pack the {@link ResourcePack} that will be sent to the client + * @param options {@link ResourcePackOption}'s that specify how the client loads the pack + * @throws ResourcePackException if an issue occurred during pack registration + * @since 2.6.2 + */ + public abstract void register(@NonNull ResourcePack pack, @Nullable ResourcePackOption... options); + + /** + * Sets {@link ResourcePackOption}'s for a {@link ResourcePack}. + * This method can also be used to override options for resource packs already registered in the + * {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent}. + * + * @param uuid the uuid of the resource pack to register the options for + * @param options the {@link ResourcePackOption}'s to register for the resource pack + * @throws ResourcePackException if an issue occurred during {@link ResourcePackOption} registration + * @since 2.6.2 + */ + public abstract void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options); + + /** + * Returns a collection of {@link ResourcePackOption}'s for a registered {@link ResourcePack}. + * The collection returned here is not modifiable. + * + * @param uuid the {@link ResourcePack} for which the options are set + * @return a collection of {@link ResourcePackOption}'s + * @throws ResourcePackException if the pack was not registered + * @since 2.6.2 + */ + public abstract Collection> options(@NonNull UUID uuid); + + /** + * Returns the current {@link ResourcePackOption}, or null, for a given {@link ResourcePackOption.Type}. + * + * @param uuid the {@link ResourcePack} for which to query this option type + * @param type the {@link ResourcePackOption.Type} of the option to query + * @throws ResourcePackException if the queried option is invalid or not present on the resource pack + * @since 2.6.2 + */ + public abstract @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type); + + /** + * Unregisters a {@link ResourcePack} from the list of packs sent to this {@link GeyserConnection}. + * + * @param uuid the UUID of the {@link ResourcePack} to be removed + * @since 2.1.1 */ public abstract boolean unregister(@NonNull UUID uuid); + + /** + * Whether to forcefully disable vibrant visuals for joining clients. + * While vibrant visuals are nice to look at, they can cause issues with + * some resource packs. + * + * @param enabled Whether vibrant visuals are allowed. This is true by default. + * @since 2.7.2 + */ + public abstract void allowVibrantVisuals(boolean enabled); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java index c3c8198c1..9200c21f3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java @@ -31,19 +31,29 @@ import org.geysermc.event.Cancellable; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.network.RemoteServer; +import org.geysermc.geyser.api.util.PlatformType; + +import java.util.Map; +import java.util.Objects; /** - * Called when a session has logged in, and is about to connect to a remote java server. + * Called when a session has logged in, and is about to connect to a remote Java server. * This event is cancellable, and can be used to prevent the player from connecting to the remote server. */ public final class SessionLoginEvent extends ConnectionEvent implements Cancellable { private RemoteServer remoteServer; private boolean cancelled; private String disconnectReason; + private Map cookies; + private boolean transferring; - public SessionLoginEvent(@NonNull GeyserConnection connection, @NonNull RemoteServer remoteServer) { + public SessionLoginEvent(@NonNull GeyserConnection connection, + @NonNull RemoteServer remoteServer, + @NonNull Map cookies) { super(connection); this.remoteServer = remoteServer; + this.cookies = cookies; + this.transferring = false; } /** @@ -90,9 +100,9 @@ public final class SessionLoginEvent extends ConnectionEvent implements Cancella } /** - * Gets the {@link RemoteServer} the section will attempt to connect to. + * Gets the {@link RemoteServer} the session will attempt to connect to. * - * @return the {@link RemoteServer} the section will attempt to connect to. + * @return the {@link RemoteServer} the session will attempt to connect to. */ public @NonNull RemoteServer remoteServer() { return this.remoteServer; @@ -100,10 +110,44 @@ public final class SessionLoginEvent extends ConnectionEvent implements Cancella /** * Sets the {@link RemoteServer} to connect the session to. + * This method will only work as expected on {@link PlatformType#STANDALONE}, + * as on other Geyser platforms, the remote server is not determined by Geyser. * * @param remoteServer Sets the {@link RemoteServer} to connect to. */ public void remoteServer(@NonNull RemoteServer remoteServer) { this.remoteServer = remoteServer; } + + /** + * Sets a map of cookies from a possible previous session. The Java server can send and request these + * to store information on the client across server transfers. + */ + public void cookies(@NonNull Map cookies) { + Objects.requireNonNull(cookies); + this.cookies = cookies; + } + + /** + * Gets a map of the sessions cookies, if set. + * @return the connections cookies + */ + public @NonNull Map cookies() { + return cookies; + } + + /** + * Determines the connection intent of the connection + */ + public void transferring(boolean transferring) { + this.transferring = transferring; + } + + /** + * Gets whether this login attempt to the Java server + * has the transfer intent + */ + public boolean transferring() { + return this.transferring; + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSkinApplyEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSkinApplyEvent.java new file mode 100644 index 000000000..f22241e41 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSkinApplyEvent.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.bedrock; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; +import org.geysermc.geyser.api.skin.Cape; +import org.geysermc.geyser.api.skin.Skin; +import org.geysermc.geyser.api.skin.SkinData; +import org.geysermc.geyser.api.skin.SkinGeometry; + +import java.util.UUID; + +/** + * Called when a skin is applied to a player. + *

+ * Won't be called when a fake player is spawned for a player skull. + */ +public abstract class SessionSkinApplyEvent extends ConnectionEvent { + + private final String username; + private final UUID uuid; + private final boolean slim; + private final boolean bedrock; + private final SkinData originalSkinData; + + public SessionSkinApplyEvent(@NonNull GeyserConnection connection, String username, UUID uuid, boolean slim, boolean bedrock, SkinData skinData) { + super(connection); + this.username = username; + this.uuid = uuid; + this.slim = slim; + this.bedrock = bedrock; + this.originalSkinData = skinData; + } + + /** + * The username of the player. + * + * @return the username of the player + */ + public @NonNull String username() { + return username; + } + + /** + * The UUID of the player. + * + * @return the UUID of the player + */ + public @NonNull UUID uuid() { + return uuid; + } + + /** + * If the player is using a slim model. + * + * @return if the player is using a slim model + */ + public boolean slim() { + return slim; + } + + /** + * If the player is a Bedrock player. + * + * @return if the player is a Bedrock player + */ + public boolean bedrock() { + return bedrock; + } + + /** + * The original skin data of the player. + * + * @return the original skin data of the player + */ + public @NonNull SkinData originalSkin() { + return originalSkinData; + } + + /** + * The skin data of the player. + * + * @return the current skin data of the player + */ + public abstract @NonNull SkinData skinData(); + + /** + * Change the skin of the player. + * + * @param newSkin the new skin + */ + public abstract void skin(@NonNull Skin newSkin); + + /** + * Change the cape of the player. + * + * @param newCape the new cape + */ + public abstract void cape(@NonNull Cape newCape); + + /** + * Change the geometry of the player. + * + * @param newGeometry the new geometry + */ + public abstract void geometry(@NonNull SkinGeometry newGeometry); + + /** + * Change the geometry of the player. + *

+ * Constructs a generic {@link SkinGeometry} object with the given data. + * + * @param geometryName the name of the geometry + * @param geometryData the data of the geometry + */ + public void geometry(@NonNull String geometryName, @NonNull String geometryData) { + geometry(new SkinGeometry("{\"geometry\" :{\"default\" :\"" + geometryName + "\"}}", geometryData)); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/connection/ConnectionRequestEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/connection/ConnectionRequestEvent.java new file mode 100644 index 000000000..b36ee8bfb --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/connection/ConnectionRequestEvent.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.connection; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.event.Cancellable; +import org.geysermc.event.Event; + +import java.net.InetSocketAddress; + +/** + * Called whenever a client attempts to connect to the server, before the connection is accepted. + */ +public final class ConnectionRequestEvent implements Event, Cancellable { + + private boolean cancelled; + private final InetSocketAddress ip; + private final InetSocketAddress proxyIp; + + public ConnectionRequestEvent(@NonNull InetSocketAddress ip, @Nullable InetSocketAddress proxyIp) { + this.ip = ip; + this.proxyIp = proxyIp; + } + + /** + * The IP address of the client attempting to connect + * + * @return the IP address of the client attempting to connect + * @deprecated Use {@link #inetSocketAddress()} instead + */ + @NonNull @Deprecated(forRemoval = true) + public InetSocketAddress getInetSocketAddress() { + return ip; + } + + /** + * The IP address of the proxy handling the connection. It will return null if there is no proxy. + * + * @return the IP address of the proxy handling the connection + * @deprecated Use {@link #proxyIp()} instead + */ + @Nullable @Deprecated(forRemoval = true) + public InetSocketAddress getProxyIp() { + return proxyIp; + } + + /** + * The IP address of the client attempting to connect + * + * @return the IP address of the client attempting to connect + */ + @NonNull + public InetSocketAddress inetSocketAddress() { + return ip; + } + + /** + * The IP address of the proxy handling the connection. It will return null if there is no proxy. + * + * @return the IP address of the proxy handling the connection + */ + @Nullable + public InetSocketAddress proxyIp() { + return proxyIp; + } + + /** + * The cancel status of this event. If this event is cancelled, the connection will be rejected. + * + * @return the cancel status of this event + */ + @Override + public boolean isCancelled() { + return cancelled; + } + + /** + * Sets the cancel status of this event. If this event is canceled, the connection will be rejected. + * + * @param cancelled the cancel status of this event. + */ + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java index 10ccb93d5..64d3cb44f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java @@ -33,10 +33,10 @@ import org.geysermc.event.Event; import java.net.InetSocketAddress; /** - * Called whenever Geyser gets pinged + * Called whenever Geyser gets pinged by a Bedrock client. *

- * This event allows you to modify/obtain the MOTD, maximum player count, and current number of players online, - * Geyser will reply to the client with what was given. + * This event allows you to modify/obtain the MOTD, maximum player count, and current number of players online. + * Geyser will reply to the client with the information provided in this event. */ public interface GeyserBedrockPingEvent extends Event { diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerCodeOfConductEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerCodeOfConductEvent.java new file mode 100644 index 000000000..d40094fb6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerCodeOfConductEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.java; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.bedrock.SessionAcceptCodeOfConductEvent; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; + +/** + * Fired when the Java server sends a code of conduct during the configuration phase. + * API users can listen to this event and tell Geyser the player has accepted the code of conduct before, which will result in the + * code of conduct not being shown to the player. + * + *

Java clients cache this locally, but bedrock clients don't. Normally Geyser uses a simple JSON file to implement this, + * but an alternative solution may be preferred when using multiple Geyser instances. Such a solution can be implemented through this event and {@link SessionAcceptCodeOfConductEvent}.

+ * + * @see SessionAcceptCodeOfConductEvent + * @since 2.9.0 + */ +public final class ServerCodeOfConductEvent extends ConnectionEvent { + private final String codeOfConduct; + private boolean hasAccepted = false; + + public ServerCodeOfConductEvent(@NonNull GeyserConnection connection, String codeOfConduct) { + super(connection); + this.codeOfConduct = codeOfConduct; + } + + /** + * @return the code of conduct sent by the server + * @since 2.9.0 + */ + public String codeOfConduct() { + return codeOfConduct; + } + + /** + * @return {@code true} if Geyser should not show the code of conduct to the player, because they have already accepted it + * @since 2.9.0 + */ + public boolean accepted() { + return hasAccepted; + } + + /** + * Sets {@link ServerCodeOfConductEvent#accepted()} to {@code true}. + * @since 2.9.0 + */ + public void accept() { + this.hasAccepted = true; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java index 299c9d6dd..40268d5b2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerDefineCommandsEvent.java @@ -37,7 +37,7 @@ import java.util.Set; *
* This event is mapped to the existence of Brigadier on the server. */ -public class ServerDefineCommandsEvent extends ConnectionEvent implements Cancellable { +public final class ServerDefineCommandsEvent extends ConnectionEvent implements Cancellable { private final Set commands; private boolean cancelled; diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java new file mode 100644 index 000000000..f32d84f6a --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.java; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.value.qual.IntRange; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; + +import java.util.Map; + +/** + * Fired when the Java server sends a transfer request to a different Java server. + * Geyser Extensions can listen to this event and set a target server ip/port for Bedrock players to be transferred to. + */ +public final class ServerTransferEvent extends ConnectionEvent { + + private final String host; + private final int port; + private String bedrockHost; + private int bedrockPort; + private final Map cookies; + + public ServerTransferEvent(@NonNull GeyserConnection connection, + @NonNull String host, int port, @NonNull Map cookies) { + super(connection); + this.host = host; + this.port = port; + this.cookies = cookies; + this.bedrockHost = null; + this.bedrockPort = -1; + } + + /** + * The host that the Java server requests a transfer to. + * + * @return the host + */ + public @NonNull String host() { + return this.host; + } + + /** + * The port that the Java server requests a transfer to. + * + * @return the port + */ + public int port() { + return this.port; + } + + /** + * The host that the Bedrock player should try and connect to. + * If this is not set, the Bedrock player will just be disconnected. + * + * @return the host where the Bedrock client will be transferred to, or null if not set. + */ + public @Nullable String bedrockHost() { + return this.bedrockHost; + } + + /** + * The port that the Bedrock player should try and connect to. + * If this is not set, the Bedrock player will just be disconnected. + * + * @return the port where the Bedrock client will be transferred to, or -1 if not set. + */ + public int bedrockPort() { + return this.bedrockPort; + } + + /** + * Sets the host for the Bedrock player to be transferred to + */ + public void bedrockHost(@NonNull String host) { + if (host == null || host.isBlank()) { + throw new IllegalArgumentException("Server address cannot be null or blank"); + } + this.bedrockHost = host; + } + + /** + * Sets the port for the Bedrock player to be transferred to + */ + public void bedrockPort(@IntRange(from = 0, to = 65535) int port) { + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Server port must be between 0 and 65535, was " + port); + } + this.bedrockPort = port; + } + + /** + * Gets a map of the sessions current cookies. + * + * @return the connections cookies + */ + public @NonNull Map cookies() { + return cookies; + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java index 994373752..d136202bd 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java @@ -50,7 +50,7 @@ public interface GeyserDefineCommandsEvent extends Event { /** * Gets all the registered built-in {@link Command}s. * - * @return all the registered built-in commands + * @return all the registered built-in commands as an unmodifiable map */ @NonNull Map commands(); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java new file mode 100644 index 000000000..ef8380041 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.entity.EntityData; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserIntEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.entity.type.GeyserEntity; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * Lifecycle event fired during Geyser's startup to allow custom entity properties + * to be registered for a specific entity type. + *

+ * Listeners can add new properties for any entity by passing the target entity's + * identifier (e.g., {@code Identifier.of("player")}) to the registration methods. + * The returned {@link GeyserEntityProperty} is used to identify the properties and to + * update the value of a specific entity instance. + * + *

Example usage

+ *
{@code
+ * public void onDefine(GeyserDefineEntityPropertiesEvent event) {
+ *     Identifier player = Identifier.of("player");
+ *     GeyserFloatEntityProperty ANIMATION_SPEED =
+ *         event.registerFloatProperty(player, Identifier.of("my_group:animation_speed"), 0.0f, 1.0f, 0.1f);
+ *     GeyserBooleanEntityProperty SHOW_SHORTS =
+ *         event.registerBooleanProperty(player, Identifier.of("my_group:show_shorts"), false);
+ * }
+ * }
+ * + * Retrieving entity instances is possible with the {@link EntityData#entityByJavaId(int)} method, or + * {@link EntityData#playerEntity()} for the connection player entity. + * To update the value of a property on a specific entity, use {@link GeyserEntity#updateProperty(GeyserEntityProperty, Object)}, + * or {@link GeyserEntity#updatePropertiesBatched(Consumer)} to update multiple properties efficiently at once. + * + *

Notes: + *

    + *
  • Default values must fall within the provided bounds.
  • + *
  • There cannot be more than 32 properties registered per entity type in total
  • + *
  • {@link #properties(Identifier)} returns properties registered for the given entity + * (including those added earlier in the same callback), including vanilla properties.
  • + *
+ * + * @since 2.9.0 + */ +public interface GeyserDefineEntityPropertiesEvent extends Event { + + /** + * Returns an unmodifiable view of all properties that have been registered + * so far for the given entity type. This includes entity properties used for vanilla gameplay, + * such as those used for creaking animations. + * + * @param entityType the Java edition entity type identifier + * @return an unmodifiable collection of registered properties + * + * @since 2.9.0 + */ + Collection> properties(@NonNull Identifier entityType); + + /** + * Registers a {@code float}-backed entity property. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @param defaultValue the default value assigned initially on entity spawn - if null, it will be the minimum value + * @return the created float property + * + * @since 2.9.0 + */ + GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, float min, float max, @Nullable Float defaultValue); + + /** + * Registers a {@code float}-backed entity property with a default value set to the minimum value. + * @see #registerFloatProperty(Identifier, Identifier, float, float, Float) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @return the created float property + * + * @since 2.9.0 + */ + default GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, float min, float max) { + return registerFloatProperty(entityType, propertyIdentifier, min, max, null); + } + + /** + * Registers an {@code int}-backed entity property. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @param defaultValue the default value assigned initially on entity spawn - if null, it will be the minimum value + * @return the created int property + * + * @since 2.9.0 + */ + GeyserIntEntityProperty registerIntegerProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, int min, int max, @Nullable Integer defaultValue); + + /** + * Registers an {@code int}-backed entity property with a default value set to the minimum value. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @return the created int property + * + * @since 2.9.0 + */ + default GeyserIntEntityProperty registerIntegerProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, int min, int max) { + return registerIntegerProperty(entityType, propertyIdentifier, min, max, null); + } + + /** + * Registers a {@code boolean}-backed entity property. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param defaultValue the default boolean value + * @return the created boolean property handle + * + * @since 2.9.0 + */ + GeyserBooleanEntityProperty registerBooleanProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, boolean defaultValue); + + /** + * Registers a {@code boolean}-backed entity property with a default of {@code false}. + * @see #registerBooleanProperty(Identifier, Identifier, boolean) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @return the created boolean property + * @since 2.9.0 + */ + default GeyserBooleanEntityProperty registerBooleanProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier) { + return registerBooleanProperty(entityType, propertyIdentifier, false); + } + + /** + * Registers a typed {@linkplain Enum enum}-backed entity property. + *

+ * The enum constants define the allowed values. If {@code defaultValue} is {@code null}, + * the first enum value is set as the default. + * @see GeyserEnumEntityProperty for further limitations + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param enumClass the enum class that defines allowed values + * @param defaultValue the default enum value, or {@code null} for the first enum value to be the default + * @param the enum type + * @return the created enum property + * + * @since 2.9.0 + */ + > GeyserEnumEntityProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class enumClass, @Nullable E defaultValue); + + /** + * Registers a typed {@linkplain Enum enum}-backed entity property with the first value set as the default. + * @see #registerEnumProperty(Identifier, Identifier, Class, Enum) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param enumClass the enum class that defines allowed values + * @param the enum type + * @return the created enum property + * + * @since 2.9.0 + */ + default > GeyserEnumEntityProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class enumClass) { + return registerEnumProperty(entityType, propertyIdentifier, enumClass, null); + } + + /** + * Registers a string-backed "enum-like" entity property where the set of allowed values + * is defined by the provided list. If {@code defaultValue} is {@code null}, the first value is used as the default + * on entity spawn. The default must be one of the values in {@code values}. + * @see GeyserStringEnumProperty + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param values the allowed string values + * @param defaultValue the default string value, or {@code null} for the first value to be used + * @return the created string-enum property + * + * @since 2.9.0 + */ + GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull List values, @Nullable String defaultValue); + + /** + * Registers a string-backed "enum-like" entity property with the first value as the default. + * @see #registerEnumProperty(Identifier, Identifier, List, String) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param values the allowed string values + * @return the created string-enum property handle + * + * @since 2.9.0 + */ + default GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull List values) { + return registerEnumProperty(entityType, propertyIdentifier, values, null); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java new file mode 100644 index 000000000..4128f1c47 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Called when {@link ResourcePack}'s are loaded within Geyser. + * @since 2.6.2 + */ +public abstract class GeyserDefineResourcePacksEvent implements Event { + + /** + * Gets the {@link ResourcePack}'s that will be sent to connecting Bedrock clients. + * To remove packs, use {@link #unregister(UUID)}, as the list returned + * by this method is unmodifiable. + * + * @return an unmodifiable list of {@link ResourcePack}'s + * @since 2.6.2 + */ + public abstract @NonNull List resourcePacks(); + + /** + * Registers a {@link ResourcePack} to be sent to the client, optionally alongside + * {@link ResourcePackOption}'s specifying how it will be applied on clients. + * + * @param pack a resource pack that will be sent to the client + * @param options {@link ResourcePackOption}'s that specify how clients load the pack + * @throws ResourcePackException if an issue occurred during pack registration + * @since 2.6.2 + */ + public abstract void register(@NonNull ResourcePack pack, @Nullable ResourcePackOption... options); + + /** + * Sets {@link ResourcePackOption}'s for a {@link ResourcePack}. + * + * @param uuid the uuid of the resource pack to register the options for + * @param options the {@link ResourcePackOption}'s to register for the resource pack + * @throws ResourcePackException if an issue occurred during {@link ResourcePackOption} registration + * @since 2.6.2 + */ + public abstract void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options); + + /** + * Returns a collection of {@link ResourcePackOption}'s for a registered {@link ResourcePack}. + * The collection returned here is not modifiable. + * + * @param uuid the uuid of the {@link ResourcePack} for which the options are set + * @return a collection of {@link ResourcePackOption}'s + * @throws ResourcePackException if the pack was not registered + * @since 2.6.2 + */ + public abstract Collection> options(@NonNull UUID uuid); + + /** + * Returns the current option, or null, for a given {@link ResourcePackOption.Type}. + * + * @param uuid the {@link ResourcePack} for which to query this option type + * @param type the {@link ResourcePackOption.Type} of the option to query + * @throws ResourcePackException if the queried option is invalid or not present on the resource pack + * @since 2.6.2 + */ + public abstract @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type); + + /** + * Unregisters a {@link ResourcePack} from the list of packs sent to connecting Bedrock clients. + * + * @param uuid the UUID of the {@link ResourcePack} to be removed + * @since 2.6.2 + */ + public abstract void unregister(@NonNull UUID uuid); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java index e9b283ecb..047f9df57 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java @@ -32,9 +32,8 @@ import java.nio.file.Path; import java.util.List; /** - * Called when resource packs are loaded within Geyser. - * - * @param resourcePacks a mutable list of the currently listed resource packs + * @deprecated Use the {@link GeyserDefineResourcePacksEvent} instead. */ +@Deprecated public record GeyserLoadResourcePacksEvent(@NonNull List resourcePacks) implements Event { } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java index 16d5058da..9147ad4b8 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java @@ -33,7 +33,7 @@ import org.geysermc.geyser.api.extension.ExtensionManager; /** * Called when Geyser is about to reload. Primarily aimed at extensions, so they can decide on their own what to reload. - * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserLoadResourcePacksEvent}. + * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserDefineResourcePacksEvent}. * * @param extensionManager the extension manager * @param eventBus the event bus diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java new file mode 100644 index 000000000..43ebc2c50 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.geysermc.event.Event; +import org.geysermc.event.PostOrder; +import org.geysermc.geyser.api.permission.PermissionChecker; + +/** + * Fired by any permission manager implementations that wish to add support for custom permission checking. + * This event is not guaranteed to be fired - it is currently only fired on Geyser-Standalone and ViaProxy. + *

+ * Subscribing to this event with an earlier {@link PostOrder} and registering a {@link PermissionChecker} + * will result in that checker having a higher priority than others. + */ +public interface GeyserRegisterPermissionCheckersEvent extends Event { + + void register(PermissionChecker checker); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java new file mode 100644 index 000000000..4f06c4e5f --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.util.TriState; + +/** + * Fired by anything that wishes to gather permission nodes and defaults. + *

+ * This event is not guaranteed to be fired, as certain Geyser platforms do not have a native permission system. + * It can be expected to fire on Geyser-Spigot, Geyser-NeoForge, Geyser-Standalone, and Geyser-ViaProxy + * It may be fired by a 3rd party regardless of the platform. + */ +public interface GeyserRegisterPermissionsEvent extends Event { + + /** + * Registers a permission node and its default value with the firer.

+ * {@link TriState#TRUE} corresponds to all players having the permission by default.
+ * {@link TriState#NOT_SET} corresponds to only server operators having the permission by default (if such a concept exists on the platform).
+ * {@link TriState#FALSE} corresponds to no players having the permission by default.
+ * + * @param permission the permission node to register + * @param defaultValue the default value of the node + */ + void register(@NonNull String permission, @NonNull TriState defaultValue); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java b/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java index 993bdee44..1eacfea9a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java +++ b/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java @@ -107,6 +107,15 @@ public interface Extension extends EventRegistrar { return this.extensionLoader().description(this); } + /** + * @return the root command that all of this extension's commands will stem from. + * By default, this is the extension's id. + */ + @NonNull + default String rootCommand() { + return this.description().id(); + } + /** * Gets the extension's logger * diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java index 2df3ee815..25daf450f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java +++ b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java @@ -59,33 +59,46 @@ public interface ExtensionDescription { String main(); /** - * Gets the extension's major api version + * Represents the human api version that the extension requires. + * See the Geyser version outline) + * for more details on the Geyser API version. * - * @return the extension's major api version + * @return the extension's requested human api version + */ + int humanApiVersion(); + + /** + * Represents the major api version that the extension requires. + * See the Geyser version outline) + * for more details on the Geyser API version. + * + * @return the extension's requested major api version */ int majorApiVersion(); /** - * Gets the extension's minor api version + * Represents the minor api version that the extension requires. + * See the Geyser version outline) + * for more details on the Geyser API version. * - * @return the extension's minor api version + * @return the extension's requested minor api version */ int minorApiVersion(); /** - * Gets the extension's patch api version - * - * @return the extension's patch api version + * No longer in use. Geyser is now using an adaption of the romantic versioning scheme. + * See here for details. */ - int patchApiVersion(); + @Deprecated(forRemoval = true) + default int patchApiVersion() { + return minorApiVersion(); + } /** - * Gets the extension's api version. - * - * @return the extension's api version + * Returns the extension's requested Geyser Api version. */ default String apiVersion() { - return majorApiVersion() + "." + minorApiVersion() + "." + patchApiVersion(); + return humanApiVersion() + "." + majorApiVersion() + "." + minorApiVersion(); } /** diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java index 59e2faad8..2fff247a7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -80,10 +80,9 @@ public interface NonVanillaCustomItemData extends CustomItemData { @Nullable String toolType(); /** - * Gets the tool tier of the item. - * - * @return the tool tier of the item + * @deprecated no longer used */ + @Deprecated(forRemoval = true) @Nullable String toolTier(); /** @@ -108,10 +107,9 @@ public interface NonVanillaCustomItemData extends CustomItemData { @Nullable String translationString(); /** - * Gets the repair materials of the item. - * - * @return the repair materials of the item + * @deprecated No longer used. */ + @Deprecated(forRemoval = true) @Nullable Set repairMaterials(); /** @@ -161,6 +159,13 @@ public interface NonVanillaCustomItemData extends CustomItemData { return displayHandheld(); } + /** + * Gets the block the item places. + * + * @return the block the item places + */ + String block(); + static NonVanillaCustomItemData.Builder builder() { return GeyserApi.api().provider(NonVanillaCustomItemData.Builder.class); } @@ -201,6 +206,8 @@ public interface NonVanillaCustomItemData extends CustomItemData { Builder chargeable(boolean isChargeable); + Builder block(String block); + /** * @deprecated Use {@link #displayHandheld(boolean)} instead. */ diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index 884129fa3..b6626aa6a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -35,6 +35,7 @@ import java.nio.file.Path; /** * Represents a pack codec that can be used * to provide resource packs to clients. + * @since 2.1.1 */ public abstract class PackCodec { @@ -42,6 +43,7 @@ public abstract class PackCodec { * Gets the sha256 hash of the resource pack. * * @return the hash of the resource pack + * @since 2.1.1 */ public abstract byte @NonNull [] sha256(); @@ -49,34 +51,66 @@ public abstract class PackCodec { * Gets the resource pack size. * * @return the resource pack file size + * @since 2.1.1 */ public abstract long size(); /** - * Serializes the given resource pack into a byte buffer. + * @deprecated use {@link #serialize()} instead. + */ + @Deprecated + @NonNull + public SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + return serialize(); + }; + + /** + * Serializes the given codec into a byte buffer. * - * @param resourcePack the resource pack to serialize * @return the serialized resource pack + * @since 2.6.2 */ @NonNull - public abstract SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException; + public abstract SeekableByteChannel serialize() throws IOException; /** * Creates a new resource pack from this codec. * * @return the new resource pack + * @since 2.1.1 */ @NonNull protected abstract ResourcePack create(); + /** + * Creates a new resource pack builder from this codec. + * + * @return the new resource pack builder + * @since 2.6.2 + */ + protected abstract ResourcePack.@NonNull Builder createBuilder(); + /** * Creates a new pack provider from the given path. * * @param path the path to create the pack provider from * @return the new pack provider + * @since 2.1.1 */ @NonNull public static PackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } + + /** + * Creates a new pack provider from the given url. + * + * @param url the url to create the pack provider from + * @return the new pack provider + * @since 2.6.2 + */ + @NonNull + public static PackCodec url(@NonNull String url) { + return GeyserApi.api().provider(UrlPackCodec.class, url); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java index a3770451a..d6d668fb2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java @@ -32,6 +32,7 @@ import java.nio.file.Path; /** * Represents a pack codec that creates a resource * pack from a path on the filesystem. + * @since 2.1.1 */ public abstract class PathPackCodec extends PackCodec { @@ -39,7 +40,8 @@ public abstract class PathPackCodec extends PackCodec { * Gets the path of the resource pack. * * @return the path of the resource pack + * @since 2.1.1 */ @NonNull public abstract Path path(); -} \ No newline at end of file +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java index de1beaf65..a8572070d 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java @@ -26,12 +26,16 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; + +import java.util.UUID; /** * Represents a resource pack sent to Bedrock clients *

* This representation of a resource pack only contains what * Geyser requires to send it to the client. + * @since 2.1.1 */ public interface ResourcePack { @@ -39,6 +43,7 @@ public interface ResourcePack { * The {@link PackCodec codec} for this pack. * * @return the codec for this pack + * @since 2.1.1 */ @NonNull PackCodec codec(); @@ -47,6 +52,7 @@ public interface ResourcePack { * Gets the resource pack manifest. * * @return the resource pack manifest + * @since 2.1.1 */ @NonNull ResourcePackManifest manifest(); @@ -55,18 +61,83 @@ public interface ResourcePack { * Gets the content key of the resource pack. Lack of a content key is represented by an empty String. * * @return the content key of the resource pack + * @since 2.1.1 */ @NonNull String contentKey(); + /** + * Shortcut for getting the UUID from the {@link ResourcePackManifest}. + * + * @return the resource pack uuid + * @since 2.6.2 + */ + @NonNull + default UUID uuid() { + return manifest().header().uuid(); + } + /** * Creates a resource pack with the given {@link PackCodec}. * * @param codec the pack codec * @return the resource pack + * @since 2.1.1 */ @NonNull static ResourcePack create(@NonNull PackCodec codec) { return codec.create(); } + + /** + * Returns a {@link Builder} for a resource pack. + * It can be used to set a content key. + * + * @param codec the {@link PackCodec} to base the builder on + * @return a {@link Builder} to build a resource pack + * @since 2.6.2 + */ + static Builder builder(@NonNull PackCodec codec) { + return codec.createBuilder(); + } + + /** + * A builder for a resource pack. It allows providing a content key manually. + * @since 2.6.2 + */ + interface Builder { + + /** + * @return the {@link ResourcePackManifest} of this resource pack + * @since 2.6.2 + */ + ResourcePackManifest manifest(); + + /** + * @return the {@link PackCodec} of this resource pack + * @since 2.6.2 + */ + PackCodec codec(); + + /** + * @return the current content key, or an empty string if not set + * @since 2.6.2 + */ + String contentKey(); + + /** + * Sets a content key for this resource pack. + * + * @param contentKey the content key + * @return this builder + * @since 2.6.2 + */ + @This Builder contentKey(@NonNull String contentKey); + + /** + * @return the resource pack + * @since 2.6.2 + */ + ResourcePack build(); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java index c9ccdd6c5..737682e22 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java @@ -26,55 +26,99 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.option.SubpackOption; import java.util.Collection; import java.util.UUID; /** - * Represents a resource pack manifest. + * Represents a Bedrock edition resource pack manifest (manifest.json). + * All resource packs are required to have such a file as it identifies the resource pack. + * See + * Microsoft's docs for more info. + * @since 2.1.1 */ public interface ResourcePackManifest { /** * Gets the format version of the resource pack. + *

+ * "1" is used for skin packs, + * "2" is used for resource and behavior packs, and world templates. * * @return the format version + * @since 2.1.1 */ int formatVersion(); /** - * Gets the header of the resource pack. + * Gets the {@link Header} of the resource pack. * - * @return the header + * @return the {@link Header} + * @since 2.1.1 */ @NonNull Header header(); /** - * Gets the modules of the resource pack. + * Gets the {@link Module}'s of the resource pack. * - * @return the modules + * @return a collection of modules + * @since 2.1.1 */ @NonNull Collection modules(); /** - * Gets the dependencies of the resource pack. + * Gets the {@link Dependency}'s of the resource pack. * - * @return the dependencies + * @return a collection of dependencies + * @since 2.6.2 */ @NonNull Collection dependencies(); + /** + * Gets the {@link Subpack}'s of the resource pack. + * See Microsoft's docs on subpacks + * for more information. + * + * @return a collection of subpacks + * @since 2.6.2 + */ + @NonNull + Collection subpacks(); + + /** + * Gets the {@link Setting}'s of the resource pack. + * These are shown to Bedrock client's in the resource pack settings menu (see here) + * to inform users about what the resource pack and sub-packs include. + * + * @return a collection of settings + * @since 2.6.2 + */ + @NonNull + Collection settings(); + /** * Represents the header of a resource pack. + * It contains the main information about the resource pack, such as + * the name, description, or uuid. + * See + * Microsoft's docs for further details on headers. + * @since 2.1.1 */ interface Header { /** - * Gets the UUID of the resource pack. + * Gets the UUID of the resource pack. It is a unique identifier that differentiates this resource pack from any other resource pack. + * Bedrock clients will cache resource packs, and download resource packs when the uuid is new (or the version changes). * * @return the UUID + * @since 2.1.1 */ @NonNull UUID uuid(); @@ -83,6 +127,7 @@ public interface ResourcePackManifest { * Gets the version of the resource pack. * * @return the version + * @since 2.1.1 */ @NonNull Version version(); @@ -91,6 +136,7 @@ public interface ResourcePackManifest { * Gets the name of the resource pack. * * @return the name + * @since 2.1.1 */ @NonNull String name(); @@ -99,6 +145,7 @@ public interface ResourcePackManifest { * Gets the description of the resource pack. * * @return the description + * @since 2.1.1 */ @NonNull String description(); @@ -107,6 +154,7 @@ public interface ResourcePackManifest { * Gets the minimum supported Minecraft version of the resource pack. * * @return the minimum supported Minecraft version + * @since 2.1.1 */ @NonNull Version minimumSupportedMinecraftVersion(); @@ -114,21 +162,29 @@ public interface ResourcePackManifest { /** * Represents a module of a resource pack. + * It contains information about the content type that is + * offered by this resource pack. + * See + * Microsoft's docs for further details on modules. + * @since 2.1.1 */ interface Module { /** * Gets the UUID of the module. + * This should usually be different from the UUID in the {@link Header}. * * @return the UUID + * @since 2.1.1 */ @NonNull UUID uuid(); /** - * Gets the version of the module. + * Gets the {@link Version} of the module. * - * @return the version + * @return the {@link Version} + * @since 2.1.1 */ @NonNull Version version(); @@ -137,6 +193,7 @@ public interface ResourcePackManifest { * Gets the type of the module. * * @return the type + * @since 2.1.1 */ @NonNull String type(); @@ -145,6 +202,7 @@ public interface ResourcePackManifest { * Gets the description of the module. * * @return the description + * @since 2.1.1 */ @NonNull String description(); @@ -152,28 +210,102 @@ public interface ResourcePackManifest { /** * Represents a dependency of a resource pack. + * These are references to other resource packs that must be + * present in order for this resource pack to apply. + * See + * Microsoft's docs for further details on dependencies. + * @since 2.1.1 */ interface Dependency { /** - * Gets the UUID of the dependency. + * Gets the UUID of the resource pack dependency. * * @return the uuid + * @since 2.1.1 */ @NonNull UUID uuid(); /** - * Gets the version of the dependency. + * Gets the {@link Version} of the dependency. * - * @return the version + * @return the {@link Version} + * @since 2.1.1 */ @NonNull Version version(); } + /** + * Represents a subpack of a resource pack. These are often used for "variants" of the resource pack, + * such as lesser details, or additional features either to be determined by player's taste or adapted to the player device's performance. + * See Micoroft's docs for more information. + */ + interface Subpack { + + /** + * Gets the folder name where this sub-pack is placed in. + * + * @return the folder name + * @since 2.6.2 + */ + @NonNull + String folderName(); + + /** + * Gets the name of this subpack. Required for each subpack to be valid. + * To make a Bedrock client load any subpack, register the resource pack + * in the {@link SessionLoadResourcePacksEvent} or {@link GeyserDefineResourcePacksEvent} and specify a + * {@link SubpackOption} with the name of the subpack to load. + * + * @return the subpack name + * @since 2.6.2 + */ + @NonNull + String name(); + + /** + * Gets the memory tier of this Subpack, representing how much RAM a device must have to run it. + * Each memory tier requires 0.25 GB of RAM. For example, a memory tier of 0 is no requirement, + * and a memory tier of 4 requires 1GB of RAM. + * + * @return the memory tier + * @since 2.6.2 + */ + @Nullable + Float memoryTier(); + } + + /** + * Represents a setting that is shown client-side that describe what a pack does. + * Multiple setting entries are shown in separate paragraphs. + * @since 2.6.2 + */ + interface Setting { + + /** + * The type of the setting. Usually just "label". + * + * @return the type + * @since 2.6.2 + */ + @NonNull + String type(); + + /** + * The text shown for the setting. + * + * @return the text content + * @since 2.6.2 + */ + @NonNull + String text(); + } + /** * Represents a version of a resource pack. + * @since 2.1.1 */ interface Version { @@ -181,6 +313,7 @@ public interface ResourcePackManifest { * Gets the major version. * * @return the major version + * @since 2.1.1 */ int major(); @@ -188,6 +321,7 @@ public interface ResourcePackManifest { * Gets the minor version. * * @return the minor version + * @since 2.1.1 */ int minor(); @@ -195,6 +329,7 @@ public interface ResourcePackManifest { * Gets the patch version. * * @return the patch version + * @since 2.1.1 */ int patch(); @@ -202,6 +337,7 @@ public interface ResourcePackManifest { * Gets the version formatted as a String. * * @return the version string + * @since 2.1.1 */ @NonNull String toString(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java new file mode 100644 index 000000000..e729e3fbb --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a pack codec that creates a resource + * pack from a URL. + *

+ * Due to Bedrock limitations, the URL must: + *

    + *
  • be a direct download link to a .zip or .mcpack resource pack
  • + *
  • use the application type `application/zip` and set a correct content length
  • + *
+ * @since 2.6.2 + */ +public abstract class UrlPackCodec extends PackCodec { + + /** + * Gets the URL to the resource pack location. + * + * @return the URL of the resource pack + * @since 2.6.2 + */ + @NonNull + public abstract String url(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java b/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java new file mode 100644 index 000000000..66b3924d5 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack.exception; + +import java.io.Serial; + +/** + * Used to indicate an exception that occurred while handling resource pack registration, + * or during resource pack option validation. + * @since 2.6.2 + */ +public class ResourcePackException extends IllegalArgumentException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The {@link Cause} of this exception. + */ + private final Cause cause; + + /** + * @param cause the cause of this exception + * @since 2.6.2 + */ + public ResourcePackException(Cause cause) { + super(cause.message()); + this.cause = cause; + } + + /** + * @param cause the cause of this exception + * @param message an additional, more in-depth message about the issue. + * @since 2.6.2 + */ + public ResourcePackException(Cause cause, String message) { + super(message); + this.cause = cause; + } + + /** + * @return the cause of this exception + * @since 2.6.2 + */ + public Cause cause() { + return cause; + } + + /** + * Represents different causes with explanatory messages stating which issue occurred. + * @since 2.6.2 + */ + public enum Cause { + DUPLICATE("A resource pack with this UUID was already registered!"), + INVALID_PACK("This resource pack is not a valid Bedrock edition resource pack!"), + INVALID_PACK_OPTION("Attempted to register an invalid resource pack option!"), + PACK_NOT_FOUND("No resource pack was found!"), + UNKNOWN_IMPLEMENTATION("Use the resource pack codecs to create resource packs."); + + private final String message; + + /** + * @return the message of this cause + * @since 2.6.2 + */ + public String message() { + return message; + } + + Cause(String message) { + this.message = message; + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java new file mode 100644 index 000000000..f1f91e9a9 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack.option; + +import org.geysermc.geyser.api.GeyserApi; + +/** + * Allows specifying a pack priority that decides the order on how packs are sent to the client. + * If two resource packs modify the same texture - for example if one removes the pumpkin overlay and + * the other is just making it translucent, one of the packs will override the other. + * Specifically, the pack with the higher priority will override the pack changes of the lower priority. + * @since 2.6.2 + */ +public interface PriorityOption extends ResourcePackOption { + + PriorityOption HIGHEST = PriorityOption.priority(100); + PriorityOption HIGH = PriorityOption.priority(50); + PriorityOption NORMAL = PriorityOption.priority(0); + PriorityOption LOW = PriorityOption.priority(-50); + PriorityOption LOWEST = PriorityOption.priority(-100); + + /** + * Constructs a priority option based on a value between 0 and 10. + * The higher the number, the higher will this pack appear in the resource pack stack. + * + * @param priority an integer that is above 0, but smaller than 10 + * @return the priority option + * @since 2.6.2 + */ + static PriorityOption priority(int priority) { + if (priority < -100 || priority > 100) { + throw new IllegalArgumentException("Priority must be between 0 and 10 inclusive!"); + } + return GeyserApi.api().provider(PriorityOption.class, priority); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java new file mode 100644 index 000000000..a86ad88db --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; + +/** + * Represents a resource pack option that can be used to specify how a resource + * pack is sent to Bedrock clients. + *

+ * Not all options can be applied to all resource packs. For example, you cannot specify + * a specific subpack to be loaded on resource packs that do not have subpacks. + * To see which limitations apply to specific resource pack options, check the javadocs + * or see the {@link #validate(ResourcePack)} method. + * @since 2.6.2 + */ +public interface ResourcePackOption { + + /** + * @return the option type + * @since 2.6.2 + */ + @NonNull Type type(); + + /** + * @return the value of the option + * @since 2.6.2 + */ + @NonNull T value(); + + /** + * Used to validate a specific options for a pack. + * Some options are not applicable to some packs. + * + * @param pack the resource pack to validate the option for + * @throws ResourcePackException with the {@link ResourcePackException.Cause#INVALID_PACK_OPTION} cause + * @since 2.6.2 + */ + void validate(@NonNull ResourcePack pack); + + /** + * Represents the different types of resource pack options. + * @since 2.6.2 + */ + enum Type { + SUBPACK, + PRIORITY, + FALLBACK + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java new file mode 100644 index 000000000..a0462afa7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.ResourcePackManifest; + +/** + * Can be used to specify which subpack from a resource pack a player should load. + * Available subpacks can be seen in a resource pack manifest {@link ResourcePackManifest#subpacks()}. + * @since 2.6.2 + */ +public interface SubpackOption extends ResourcePackOption { + + /** + * Creates a subpack option based on a {@link ResourcePackManifest.Subpack}. + * + * @param subpack the chosen subpack + * @return a subpack option specifying that subpack + * @since 2.6.2 + */ + static SubpackOption subpack(ResourcePackManifest.@NonNull Subpack subpack) { + return named(subpack.name()); + } + + /** + * Creates a subpack option based on a subpack name. + * + * @param subpackName the name of the subpack + * @return a subpack option specifying a subpack with that name + * @since 2.6.2 + */ + static SubpackOption named(@NonNull String subpackName) { + return GeyserApi.api().provider(SubpackOption.class, subpackName); + } + + /** + * Creates a subpack option with no subpack specified. + * + * @return a subpack option specifying no subpack + * @since 2.6.2 + */ + static SubpackOption empty() { + return GeyserApi.api().provider(SubpackOption.class, ""); + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java new file mode 100644 index 000000000..9431c8cdd --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack.option; + +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.PathPackCodec; +import org.geysermc.geyser.api.pack.UrlPackCodec; + +/** + * Can be used for resource packs created with the {@link UrlPackCodec}. + * When a Bedrock client is unable to download a resource pack from a URL, Geyser will, by default, + * serve the resource pack over raknet (as packs are served with the {@link PathPackCodec}). + * This option can be used to disable that behavior, and disconnect the player instead. + * By default, the {@link UrlFallbackOption#TRUE} option is set. + * @since 2.6.2 + */ +public interface UrlFallbackOption extends ResourcePackOption { + + UrlFallbackOption TRUE = fallback(true); + UrlFallbackOption FALSE = fallback(false); + + /** + * Whether to fall back to serving packs over the raknet connection + * + * @param fallback whether to fall back + * @return a UrlFallbackOption with the specified behavior + * @since 2.6.2 + */ + static UrlFallbackOption fallback(boolean fallback) { + return GeyserApi.api().provider(UrlFallbackOption.class, fallback); + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ChestItem.java b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java similarity index 55% rename from core/src/main/java/org/geysermc/geyser/item/type/ChestItem.java rename to api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java index 99857006c..c0d4af2f4 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/ChestItem.java +++ b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java @@ -23,31 +23,27 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item.type; +package org.geysermc.geyser.api.permission; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.api.command.CommandSource; +import org.geysermc.geyser.api.util.TriState; -public class ChestItem extends BlockItem { +/** + * Something capable of checking if a {@link CommandSource} has a permission + */ +@FunctionalInterface +public interface PermissionChecker { - public ChestItem(String javaIdentifier, Builder builder) { - super(javaIdentifier, builder); - } - - @Override - public void translateNbtToBedrock(@NonNull GeyserSession session, @NonNull CompoundTag tag) { - super.translateNbtToBedrock(session, tag); - - // Strip the BlockEntityTag from the chests contents - // sent to the client. The client does not parse this - // or use it for anything, as this tag is fully - // server-side, so we remove it to reduce bandwidth and - // solve potential issues with very large tags. - - // There was a problem in the past where this would strip - // NBT data in creative mode, however with the new server - // authoritative inventories, this is no longer a concern. - tag.remove("BlockEntityTag"); - } + /** + * Checks if the given source has a permission + * + * @param source the {@link CommandSource} whose permissions should be queried + * @param permission the permission node to check + * @return a {@link TriState} as the value of the node. {@link TriState#NOT_SET} generally means that the permission + * node itself was not found, and the source does not have such permission. + * {@link TriState#TRUE} and {@link TriState#FALSE} represent explicitly set values. + */ + @NonNull + TriState hasPermission(@NonNull CommandSource source, @NonNull String permission); } diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/Cape.java b/api/src/main/java/org/geysermc/geyser/api/skin/Cape.java new file mode 100644 index 000000000..1e7341ae4 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/skin/Cape.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.skin; + +/** + * Represents a cape. + * + * @param textureUrl The URL of the cape texture + * @param capeId The ID of the cape + * @param capeData The raw cape image data in ARGB format + * @param failed If the cape failed to load, this is for things like fallback capes + */ +public record Cape(String textureUrl, String capeId, byte[] capeData, boolean failed) { + public Cape(String textureUrl, String capeId, byte[] capeData) { + this(textureUrl, capeId, capeData, false); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/Skin.java b/api/src/main/java/org/geysermc/geyser/api/skin/Skin.java new file mode 100644 index 000000000..9b39ddfe8 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/skin/Skin.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.skin; + +/** + * Represents a skin. + * + * @param textureUrl The URL/ID of the skin texture + * @param skinData The raw skin image data in ARGB + * @param failed If the skin failed to load, this is for things like fallback skins + */ +public record Skin(String textureUrl, byte[] skinData, boolean failed) { + public Skin(String textureUrl, byte[] skinData) { + this(textureUrl, skinData, false); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java b/api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java new file mode 100644 index 000000000..9de4a3534 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.skin; + +/** + * Represents a full package of {@link Skin}, {@link Cape}, and {@link SkinGeometry}. + */ +public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java b/api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java new file mode 100644 index 000000000..5b40d2022 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.skin; + +/** + * Represents geometry of a skin. + * + * @param geometryName The name of the geometry (JSON) + * @param geometryData The geometry data (JSON) + */ +public record SkinGeometry(String geometryName, String geometryData) { + + public static SkinGeometry WIDE = getLegacy(false); + public static SkinGeometry SLIM = getLegacy(true); + + /** + * Generate generic geometry + * + * @param isSlim if true, it will be the slimmer alex model + * @return The generic geometry object + */ + private static SkinGeometry getLegacy(boolean isSlim) { + return new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", ""); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java index 207320b1e..245eb9bc2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java +++ b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -31,11 +31,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; * Represents the creative menu categories or tabs. */ public enum CreativeCategory { - COMMANDS("commands", 1), - CONSTRUCTION("construction", 2), + CONSTRUCTION("construction", 1), + NATURE("nature", 2), EQUIPMENT("equipment", 3), ITEMS("items", 4), - NATURE("nature", 5), NONE("none", 6); private final String internalName; diff --git a/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java new file mode 100644 index 000000000..e82a695d1 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024-2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; + +/** + * An identifying object for representing unique objects. + * This identifier consists of two parts: + *

    + *
  • + * a namespace, which is usually a name identifying your work + *
  • + *
  • + * a path, which holds a value. + *
  • + *
+ * + * Examples of identifiers: + *
    + *
  • {@code minecraft:fox}
  • + *
  • {@code geysermc:one_fun_example}
  • + *
+ * + * If this identifier is referencing anything not in the + * vanilla Minecraft game, the namespace cannot be "minecraft". + * Further, paths cannot contain colons ({@code :}). + * + * @since 2.9.0 + */ +public interface Identifier { + + /** + * The namespace for Minecraft. + * @since 2.9.0 + */ + String DEFAULT_NAMESPACE = "minecraft"; + + /** + * Attempts to create a new identifier from a namespace and path. + * + * @return the identifier for this namespace and path + * @throws IllegalArgumentException if either namespace or path are invalid. + * @since 2.9.0 + */ + static Identifier of(@NonNull String namespace, @NonNull String path) { + return GeyserApi.api().provider(Identifier.class, namespace, path); + } + + /** + * Attempts to create a new identifier from a string representation. + * + * @return the identifier for this namespace and path + * @throws IllegalArgumentException if either the namespace or path are invalid + * @since 2.9.0 + */ + static Identifier of(String identifier) { + String[] split = identifier.split(":"); + String namespace; + String path; + if (split.length == 1) { + namespace = DEFAULT_NAMESPACE; + path = split[0]; + } else if (split.length == 2) { + namespace = split[0]; + path = split[1]; + } else { + throw new IllegalArgumentException("':' in identifier path: " + identifier); + } + return of(namespace, path); + } + + /** + * @return the namespace of this identifier. + * @since 2.9.0 + */ + String namespace(); + + /** + * @return the path of this identifier. + * @since 2.9.0 + */ + String path(); + + /** + * Checks whether this identifier is using the "minecraft" namespace. + * @since 2.9.0 + */ + default boolean vanilla() { + return namespace().equals(DEFAULT_NAMESPACE); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.java b/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.java index 34a4b59af..6171956a6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.java +++ b/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.java @@ -34,7 +34,7 @@ public interface MinecraftVersion { /** * Gets the Minecraft version as a String. - * Example: "1.20.2", or "1.20.40/1.20.41" + * Example formats: "1.21", "1.21.1", "1.21.22" * * @return the version string */ diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 1b57ffa5a..0216a43dd 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -1,20 +1,31 @@ +plugins { + id("geyser.platform-conventions") + id("geyser.modrinth-uploading-conventions") +} + dependencies { api(projects.core) + + implementation(libs.cloud.bungee) implementation(libs.adventure.text.serializer.bungeecord) - compileOnlyApi(libs.bungeecord.proxy) + compileOnlyApi(libs.bungeecord.proxy) { + isTransitive = false + } + compileOnlyApi(libs.bungeecord.api) } platformRelocate("net.md_5.bungee.jni") platformRelocate("com.fasterxml.jackson") -platformRelocate("io.netty.channel.kqueue") // This is not used because relocating breaks natives, but we must include it or else we get ClassDefNotFound platformRelocate("net.kyori") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated platformRelocate("org.yaml") // Broken as of 1.20 // These dependencies are already present on the platform provided(libs.bungeecord.proxy) -application { - mainClass.set("org.geysermc.geyser.platform.bungeecord.GeyserBungeeMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.bungeecord.GeyserBungeeMain" } tasks.withType { @@ -22,14 +33,11 @@ tasks.withType { dependencies { exclude(dependency("com.google.*:.*")) - exclude(dependency("io.netty:netty-transport-native-epoll:.*")) - exclude(dependency("io.netty:netty-transport-native-unix-common:.*")) - exclude(dependency("io.netty:netty-handler:.*")) - exclude(dependency("io.netty:netty-common:.*")) - exclude(dependency("io.netty:netty-buffer:.*")) - exclude(dependency("io.netty:netty-resolver:.*")) - exclude(dependency("io.netty:netty-transport:.*")) - exclude(dependency("io.netty:netty-codec:.*")) - exclude(dependency("io.netty:netty-resolver-dns:.*")) + exclude(dependency("io.netty.*:.*")) } } + +modrinth { + uploadFile.set(tasks.getByPath("shadowJar")) + loaders.add("bungeecord") +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java index 485079a05..23ab36363 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.platform.bungeecord; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; +import net.md_5.bungee.netty.LengthPrependerAndCompressor; import net.md_5.bungee.protocol.packet.LoginSuccess; import net.md_5.bungee.protocol.packet.SetCompression; @@ -40,8 +41,9 @@ public class GeyserBungeeCompressionDisabler extends ChannelOutboundHandlerAdapt // Fixes https://github.com/GeyserMC/Geyser/issues/4281 // The server may send a LoginDisconnect packet after compression is set. if (!compressionDisabled) { - if (ctx.pipeline().get("compress") != null) { - ctx.pipeline().remove("compress"); + LengthPrependerAndCompressor compressor = ctx.pipeline().get(LengthPrependerAndCompressor.class); + if (compressor.isCompress()) { + compressor.setCompress(false); compressionDisabled = true; } if (ctx.pipeline().get("decompress") != null) { diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java index 7c60ba95d..3c28c1d18 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java @@ -30,8 +30,22 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoop; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.IoHandler; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SingleThreadIoEventLoop; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalIoHandler; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringIoHandler; import io.netty.util.AttributeKey; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadAwareExecutor; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.event.ProxyReloadEvent; @@ -43,12 +57,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.network.netty.GeyserInjector; +import org.geysermc.geyser.network.netty.IoHandlerWrapper; import org.geysermc.geyser.network.netty.LocalServerChannelWrapper; import org.geysermc.geyser.network.netty.LocalSession; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; public class GeyserBungeeInjector extends GeyserInjector implements Listener { private final Plugin plugin; @@ -73,26 +90,58 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { throw new UnsupportedOperationException("Geyser does not currently support multiple listeners with injection! " + "Please reach out to us on our Discord at https://discord.gg/GeyserMC so we can hear feedback on your setup."); } + + try { + Class.forName("io.netty.channel.MultiThreadIoEventLoopGroup"); + } catch (ClassNotFoundException e) { + bootstrap.getGeyserLogger().error("use-direct-connection disabled as BungeeCord is not up-to-date."); + return; + } + + // TODO remove + try { + ProxyServer.class.getMethod("unsafe"); + } catch (NoSuchMethodException e) { + throw new UnsupportedOperationException("You're using an outdated version of BungeeCord - please update. Thank you!"); + } + ListenerInfo listenerInfo = proxy.getConfig().getListeners().stream().findFirst().orElseThrow(IllegalStateException::new); Class proxyClass = proxy.getClass(); // Using the specified EventLoop is required, or else an error will be thrown - EventLoopGroup bossGroup; - EventLoopGroup workerGroup; + MultiThreadIoEventLoopGroup workerGroup; try { - EventLoopGroup eventLoops = (EventLoopGroup) proxyClass.getField("eventLoops").get(proxy); - // Netty redirects ServerBootstrap#group(EventLoopGroup) to #group(EventLoopGroup, EventLoopGroup) and uses the same event loop for both. - bossGroup = eventLoops; - workerGroup = eventLoops; + workerGroup = (MultiThreadIoEventLoopGroup) proxyClass.getField("eventLoops").get(proxy); bootstrap.getGeyserLogger().debug("BungeeCord event loop style detected."); } catch (NoSuchFieldException e) { // Waterfall uses two separate event loops // https://github.com/PaperMC/Waterfall/blob/fea7ec356dba6c6ac28819ff11be604af6eb484e/BungeeCord-Patches/0022-Use-a-worker-and-a-boss-event-loop-group.patch - bossGroup = (EventLoopGroup) proxyClass.getField("bossEventLoopGroup").get(proxy); - workerGroup = (EventLoopGroup) proxyClass.getField("workerEventLoopGroup").get(proxy); + workerGroup = (MultiThreadIoEventLoopGroup) proxyClass.getField("workerEventLoopGroup").get(proxy); bootstrap.getGeyserLogger().debug("Waterfall event loop style detected."); } + final IoEventLoopGroup finalWorkerGroup = workerGroup; + var factory = LocalIoHandler.newFactory(); + var nativeFactory = getNativeHandlerFactory(); + var wrapperFactory = new IoHandlerFactory() { + @Override + public IoHandler newHandler(ThreadAwareExecutor ioExecutor) { + return new IoHandlerWrapper(factory.newHandler(ioExecutor), nativeFactory.newHandler(ioExecutor)); + } + }; + + EventLoopGroup wrapperGroup = new MultiThreadIoEventLoopGroup(factory) { + @Override + protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory("Geyser Backend Worker Group", Thread.MAX_PRIORITY); + } + + @Override + protected IoEventLoop newChild(Executor executor, IoHandlerFactory ioHandlerFactory, Object... args) { + return new SingleThreadIoEventLoop(finalWorkerGroup, executor, wrapperFactory); + } + }; + // Is currently just AttributeKey.valueOf("ListerInfo") but we might as well copy the value itself. AttributeKey listener = PipelineUtils.LISTENER; listenerInfo = new ListenerInfo( @@ -138,7 +187,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { if (channelInitializer == null) { // Proxy has finished initializing; we can safely grab this variable without fear of plugins modifying it // (Older versions of ViaVersion replace this to inject) - channelInitializer = PipelineUtils.SERVER_CHILD; + channelInitializer = proxy.unsafe().getFrontendChannelInitializer().getChannelInitializer(); } initChannel.invoke(channelInitializer, ch); @@ -149,7 +198,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { } }) .childAttr(listener, listenerInfo) - .group(bossGroup, workerGroup) + .group(new MultiThreadIoEventLoopGroup(LocalIoHandler.newFactory()), wrapperGroup) .localAddress(LocalAddress.ANY)) .bind() .syncUninterruptibly(); @@ -194,4 +243,16 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { initializeLocalChannel(GeyserImpl.getInstance().getBootstrap()); } } + + private static IoHandlerFactory getNativeHandlerFactory() { + // Match whatever settings the Bungee proxy is using + // https://github.com/SpigotMC/BungeeCord/blob/617c2728a25347487eee4e8649d52fe57f1ff6e2/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L139-L162 + if (Boolean.parseBoolean(System.getProperty("bungee.io_uring", "false")) && IoUring.isAvailable()) { + return IoUringIoHandler.newFactory(); + } + if (Boolean.parseBoolean(System.getProperty("bungee.epoll", "true")) && Epoll.isAvailable()) { + return EpollIoHandler.newFactory(); + } + return NioIoHandler.newFactory(); + } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java index daeb20102..e9b18acca 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java @@ -26,22 +26,19 @@ package org.geysermc.geyser.platform.bungeecord; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import org.geysermc.geyser.GeyserLogger; import java.util.logging.Level; import java.util.logging.Logger; +@RequiredArgsConstructor public class GeyserBungeeLogger implements GeyserLogger { private final Logger logger; @Getter @Setter private boolean debug; - public GeyserBungeeLogger(Logger logger, boolean debug) { - this.logger = logger; - this.debug = debug; - } - @Override public void severe(String message) { logger.severe(message); @@ -78,4 +75,11 @@ public class GeyserBungeeLogger implements GeyserLogger { info(message); } } + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + info(String.format(message, arguments)); + } + } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java index 3c3853ed8..e1f22c838 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.platform.bungeecord; import lombok.AllArgsConstructor; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ServerPing; import net.md_5.bungee.api.chat.BaseComponent; @@ -36,6 +38,7 @@ import net.md_5.bungee.api.event.ProxyPingEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.protocol.ProtocolConstants; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; @@ -43,6 +46,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @AllArgsConstructor public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, Listener { @@ -59,10 +63,20 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List future.complete(event); } })); - ProxyPingEvent event = future.join(); + + ProxyPingEvent event; + + try { + event = future.get(100, TimeUnit.MILLISECONDS); + } catch (Throwable cause) { + String address = GeyserImpl.getInstance().getConfig().isLogPlayerIpAddresses() ? inetSocketAddress.toString() : ""; + GeyserImpl.getInstance().getLogger().error("Failed to get ping information for " + address, cause); + return null; + } + ServerPing response = event.getResponse(); return new GeyserPingInfo( - response.getDescriptionComponent().toLegacyText(), + GsonComponentSerializer.gson().serialize(BungeeComponentSerializer.get().deserialize(new BaseComponent[]{ response.getDescriptionComponent() })), response.getPlayers().getMax(), response.getPlayers().getOnline() ); @@ -174,6 +188,21 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List return false; } + @Override + public boolean isTransferred() { + return false; + } + + @Override + public CompletableFuture retrieveCookie(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture sendData(String s, byte[] bytes) { + throw new UnsupportedOperationException(); + } + @Override public Unsafe unsafe() { throw new UnsupportedOperationException(); diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java index 4191c8578..bc076d1cb 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java @@ -25,8 +25,11 @@ package org.geysermc.geyser.platform.bungeecord; +import io.netty.buffer.AdaptiveByteBufAllocator; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.protocol.ProtocolConstants; @@ -34,17 +37,21 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.bungeecord.command.GeyserBungeeCommandExecutor; +import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.bungee.BungeeCommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; import java.io.File; import java.io.IOException; @@ -54,20 +61,18 @@ import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; -import java.util.Map; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { - private GeyserCommandManager geyserCommandManager; + private CommandRegistry commandRegistry; private GeyserBungeeConfiguration geyserConfig; private GeyserBungeeInjector geyserInjector; - private GeyserBungeeLogger geyserLogger; + private final GeyserBungeeLogger geyserLogger = new GeyserBungeeLogger(getLogger()); private IGeyserPingPassthrough geyserBungeePingPassthrough; - private GeyserImpl geyser; @Override @@ -79,31 +84,61 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { public void onGeyserInitialize() { GeyserLocale.init(this); - // Copied from ViaVersion. - // https://github.com/ViaVersion/ViaVersion/blob/b8072aad86695cc8ec6f5e4103e43baf3abf6cc5/bungee/src/main/java/us/myles/ViaVersion/BungeePlugin.java#L43 + // TODO remove when this isn't an issue anymore + boolean adaptiveAllocatorUsed = System.getProperty("io.netty.allocator.type") == null && ByteBufAllocator.DEFAULT instanceof AdaptiveByteBufAllocator; + try { - ProtocolConstants.class.getField("MINECRAFT_1_20_3"); - } catch (NoSuchFieldException e) { - getLogger().warning(" / \\"); - getLogger().warning(" / \\"); - getLogger().warning(" / | \\"); - getLogger().warning(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", getProxy().getName())); - getLogger().warning(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps")); - getLogger().warning(" / o \\"); - getLogger().warning("/_____________\\"); + List supportedProtocols = ProtocolConstants.SUPPORTED_VERSION_IDS; + if (!supportedProtocols.contains(GameProtocol.getJavaProtocolVersion()) || adaptiveAllocatorUsed) { + geyserLogger.error(" / \\"); + geyserLogger.error(" / \\"); + geyserLogger.error(" / | \\"); + geyserLogger.error(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", getProxy().getName())); + geyserLogger.error(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps")); + geyserLogger.error(" / o \\"); + geyserLogger.error("/_____________\\"); + } + } catch (Throwable e) { + geyserLogger.warning("Unable to check the versions supported by this proxy! " + e.getMessage()); + } + + // See https://github.com/SpigotMC/BungeeCord/blob/e62fc6c2916a991e00177c580986d8b1a22fdb41/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L138 + if (Boolean.getBoolean("bungee.io_uring")) { + System.setProperty("Mcpl.io_uring", "true"); } if (!this.loadConfig()) { return; } - this.geyserLogger = new GeyserBungeeLogger(getLogger(), geyserConfig.isDebugMode()); + this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); this.geyser = GeyserImpl.load(PlatformType.BUNGEECORD, this); this.geyserInjector = new GeyserBungeeInjector(this); + + // Registration of listeners occurs only once + this.getProxy().getPluginManager().registerListener(this, new GeyserBungeeUpdateListener()); } @Override public void onEnable() { + if (geyser == null) { + return; // Config did not load properly! + } + + // After Geyser initialize for parity with other platforms. + var sourceConverter = new CommandSourceConverter<>( + CommandSender.class, + id -> getProxy().getPlayer(id), + () -> getProxy().getConsole(), + BungeeCommandSource::new + ); + CommandManager cloud = new BungeeCommandManager<>( + this, + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + this.commandRegistry = new CommandRegistry(geyser, cloud, false); // applying root permission would be a breaking change because we can't register permission defaults + // Big hack - Bungee does not provide us an event to listen to, so schedule a repeating // task that waits for a field to be filled which is set after the plugin enable // process is complete @@ -143,11 +178,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - } else { - // For consistency with other platforms - create command manager before GeyserImpl#start() - // This ensures the command events are called before the item/block ones are - this.geyserCommandManager = new GeyserCommandManager(geyser); - this.geyserCommandManager.init(); } // Force-disable query if enabled, or else Geyser won't enable @@ -182,16 +212,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } this.geyserInjector.initializeLocalChannel(this); - - this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor("geyser", this.geyser, this.geyserCommandManager.getCommands())); - for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { - Map commands = entry.getValue(); - if (commands.isEmpty()) { - continue; - } - - this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor(entry.getKey().description().id(), this.geyser, commands)); - } } @Override @@ -227,8 +247,8 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override @@ -251,6 +271,11 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { return Paths.get(getProxy().getName().equals("BungeeCord") ? "proxy.log.0" : "logs/latest.log"); } + @Override + public @NonNull String getServerPlatform() { + return getProxy().getName(); + } + @Nullable @Override public SocketAddress getSocketAddress() { @@ -293,7 +318,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); this.geyserConfig = FileUtils.loadConfig(configFile, GeyserBungeeConfiguration.class); } catch (IOException ex) { - getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); return false; } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java index c68839b20..0a89b5421 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java @@ -29,8 +29,8 @@ import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; @@ -40,7 +40,7 @@ public final class GeyserBungeeUpdateListener implements Listener { public void onPlayerJoin(final PostLoginEvent event) { if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { final ProxiedPlayer player = event.getPlayer(); - if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new BungeeCommandSource(player)); } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java index e3099f170..10ccc5bac 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java @@ -27,19 +27,22 @@ package org.geysermc.geyser.platform.bungeecord.command; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.GeyserLocale; import java.util.Locale; +import java.util.UUID; public class BungeeCommandSource implements GeyserCommandSource { - private final net.md_5.bungee.api.CommandSender handle; + private final CommandSender handle; - public BungeeCommandSource(net.md_5.bungee.api.CommandSender handle) { + public BungeeCommandSource(CommandSender handle) { this.handle = handle; // Ensure even Java players' languages are loaded GeyserLocale.loadGeyserLocale(this.locale()); @@ -72,12 +75,20 @@ public class BungeeCommandSource implements GeyserCommandSource { return !(handle instanceof ProxiedPlayer); } + @Override + public @Nullable UUID playerUuid() { + if (handle instanceof ProxiedPlayer player) { + return player.getUniqueId(); + } + return null; + } + @Override public String locale() { if (handle instanceof ProxiedPlayer player) { Locale locale = player.getLocale(); if (locale != null) { - // Locale can be null early on in the conneciton + // Locale can be null early on in the connection return GeyserLocale.formatLocale(locale.getLanguage() + "_" + locale.getCountry()); } } @@ -86,6 +97,12 @@ public class BungeeCommandSource implements GeyserCommandSource { @Override public boolean hasPermission(String permission) { - return handle.hasPermission(permission); + // Handle blank permissions ourselves, as bungeecord only handles empty ones + return permission.isBlank() || handle.hasPermission(permission); + } + + @Override + public Object handle() { + return handle; } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java deleted file mode 100644 index 2d02c9950..000000000 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.bungeecord.command; - -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.plugin.Command; -import net.md_5.bungee.api.plugin.TabExecutor; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -public class GeyserBungeeCommandExecutor extends Command implements TabExecutor { - private final GeyserCommandExecutor commandExecutor; - - public GeyserBungeeCommandExecutor(String name, GeyserImpl geyser, Map commands) { - super(name); - - this.commandExecutor = new GeyserCommandExecutor(geyser, commands); - } - - @Override - public void execute(CommandSender sender, String[] args) { - BungeeCommandSource commandSender = new BungeeCommandSource(sender); - GeyserSession session = this.commandExecutor.getGeyserSession(commandSender); - - if (args.length > 0) { - GeyserCommand command = this.commandExecutor.getCommand(args[0]); - if (command != null) { - if (!sender.hasPermission(command.permission())) { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.locale()); - - commandSender.sendMessage(ChatColor.RED + message); - return; - } - if (command.isBedrockOnly() && session == null) { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.locale()); - - commandSender.sendMessage(ChatColor.RED + message); - return; - } - command.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); - } else { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", commandSender.locale()); - commandSender.sendMessage(ChatColor.RED + message); - } - } else { - this.commandExecutor.getCommand("help").execute(session, commandSender, new String[0]); - } - } - - @Override - public Iterable onTabComplete(CommandSender sender, String[] args) { - if (args.length == 1) { - return commandExecutor.tabComplete(new BungeeCommandSource(sender)); - } else { - return Collections.emptyList(); - } - } -} diff --git a/bootstrap/mod/build.gradle.kts b/bootstrap/mod/build.gradle.kts index 7651a2df2..c43f123ec 100644 --- a/bootstrap/mod/build.gradle.kts +++ b/bootstrap/mod/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("geyser.modded-conventions") +} + architectury { common("neoforge", "fabric") } @@ -6,10 +10,18 @@ loom { mixin.defaultRefmapName.set("geyser-refmap.json") } +afterEvaluate { + // We don't need these + tasks.named("remapModrinthJar").configure { + enabled = false + } +} + dependencies { api(projects.core) compileOnly(libs.mixin) + compileOnly(libs.mixinextras) // Only here to suppress "unknown enum constant EnvType.CLIENT" warnings. DO NOT USE! compileOnly(libs.fabric.loader) -} \ No newline at end of file +} diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index eb28e0e93..5b66e0f37 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -1,5 +1,6 @@ plugins { - application + id("geyser.modded-conventions") + id("geyser.modrinth-uploading-conventions") } architectury { @@ -7,56 +8,41 @@ architectury { fabric() } -val includeTransitive: Configuration = configurations.getByName("includeTransitive") - dependencies { modImplementation(libs.fabric.loader) modApi(libs.fabric.api) api(project(":mod", configuration = "namedElements")) - shadow(project(path = ":mod", configuration = "transformProductionFabric")) { - isTransitive = false - } - shadow(projects.core) { isTransitive = false } + shadowBundle(project(path = ":mod", configuration = "transformProductionFabric")) + shadowBundle(projects.core) includeTransitive(projects.core) // These are NOT transitively included, and instead shadowed + relocated. // Avoids fabric complaining about non-SemVer versioning - // TODO: re-evaluate after loom 1.6 (https://github.com/FabricMC/fabric-loom/pull/1075) - shadow(libs.protocol.connection) { isTransitive = false } - shadow(libs.protocol.common) { isTransitive = false } - shadow(libs.protocol.codec) { isTransitive = false } - shadow(libs.mcauthlib) { isTransitive = false } - shadow(libs.raknet) { isTransitive = false } - shadow(libs.netty.codec.haproxy) { isTransitive = false } - shadow("org.cloudburstmc:nbt:3.0.2.Final") { isTransitive = false } - shadow("io.netty:netty-codec-dns:4.1.103.Final") { isTransitive = false } - shadow("io.netty:netty-resolver-dns-classes-macos:4.1.103.Final") { isTransitive = false } - - // Consequences of shading + relocating mcauthlib: shadow/relocate mcpl! - shadow(libs.mcprotocollib) { isTransitive = false } + shadowBundle(libs.protocol.connection) + shadowBundle(libs.protocol.common) + shadowBundle(libs.protocol.codec) + shadowBundle(libs.raknet) + shadowBundle(libs.mcprotocollib) // Since we also relocate cloudburst protocol: shade erosion common - shadow(libs.erosion.common) { isTransitive = false } + shadowBundle(libs.erosion.common) - // Permissions - modImplementation(libs.fabric.permissions) - include(libs.fabric.permissions) + // Let's shade in our own api/common module + shadowBundle(projects.api) + shadowBundle(projects.common) + + modImplementation(libs.cloud.fabric) + include(libs.cloud.fabric) + include(libs.fabric.permissions.api) } -application { - mainClass.set("org.geysermc.geyser.platform.fabric.GeyserFabricMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.fabric.GeyserFabricMain" } -relocate("org.cloudburstmc.nbt") relocate("org.cloudburstmc.netty") relocate("org.cloudburstmc.protocol") -relocate("io.netty.handler.codec.dns") -relocate("io.netty.handler.codec.haproxy") -relocate("io.netty.resolver.dns.macos") -relocate("com.github.steveice10.mc.protocol") -relocate("com.github.steveice10.mc.auth") -relocate("com.github.steveice10.packetlib") tasks { remapJar { @@ -70,7 +56,8 @@ tasks { modrinth { loaders.add("fabric") + uploadFile.set(tasks.getByPath("remapModrinthJar")) dependencies { required.project("fabric-api") } -} \ No newline at end of file +} diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java index 81e329c03..96a60322b 100644 --- a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java +++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java @@ -25,17 +25,24 @@ package org.geysermc.geyser.platform.fabric; -import me.lucko.fabric.api.permissions.v0.Permissions; import net.fabricmc.api.EnvType; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.world.entity.player.Player; +import net.minecraft.server.level.ServerPlayer; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.platform.mod.GeyserModUpdateListener; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.platform.mod.command.ModCommandSource; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.fabric.FabricServerCommandManager; public class GeyserFabricBootstrap extends GeyserModBootstrap implements ModInitializer { @@ -45,28 +52,47 @@ public class GeyserFabricBootstrap extends GeyserModBootstrap implements ModInit @Override public void onInitialize() { - if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER) { + if (isServer()) { // Set as an event, so we can get the proper IP and port if needed ServerLifecycleEvents.SERVER_STARTED.register((server) -> { this.setServer(server); onGeyserEnable(); }); + } else { + ClientLifecycleEvents.CLIENT_STOPPING.register(($)-> { + onGeyserShutdown(); + }); } // These are only registered once - ServerLifecycleEvents.SERVER_STOPPING.register((server) -> onGeyserShutdown()); + ServerLifecycleEvents.SERVER_STOPPING.register((server) -> { + if (isServer()) { + onGeyserShutdown(); + } else { + onGeyserDisable(); + } + }); + ServerPlayConnectionEvents.JOIN.register((handler, $, $$) -> GeyserModUpdateListener.onPlayReady(handler.getPlayer())); this.onGeyserInitialize(); + + var sourceConverter = CommandSourceConverter.layered( + CommandSourceStack.class, + id -> getServer().getPlayerList().getPlayer(id), + ServerPlayer::createCommandSourceStack, + () -> getServer().createCommandSourceStack(), // NPE if method reference is used, since server is not available yet + ModCommandSource::new + ); + CommandManager cloud = new FabricServerCommandManager<>( + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + this.setCommandRegistry(new CommandRegistry(GeyserImpl.getInstance(), cloud, false)); // applying root permission would be a breaking change because we can't register permission defaults } @Override - public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) { - return Permissions.check(source, permissionNode); - } - - @Override - public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) { - return Permissions.check(source, permissionNode, permissionLevel); + public boolean isServer() { + return FabricLoader.getInstance().getEnvironmentType().equals(EnvType.SERVER); } } diff --git a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json index 6bd217433..b042bb252 100644 --- a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json +++ b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json @@ -23,9 +23,8 @@ "geyser.mixins.json" ], "depends": { - "fabricloader": ">=0.15.2", - "fabric": "*", - "minecraft": ">=1.20.4", - "fabric-permissions-api-v0": "*" + "fabricloader": ">=0.17.2", + "fabric-api": "*", + "minecraft": ">=1.21.9" } } diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts index 2a414e6dd..310e8c3f6 100644 --- a/bootstrap/mod/neoforge/build.gradle.kts +++ b/bootstrap/mod/neoforge/build.gradle.kts @@ -1,17 +1,20 @@ plugins { - application + id("geyser.modded-conventions") + id("geyser.modrinth-uploading-conventions") } -// This is provided by "org.cloudburstmc.math.mutable" too, so yeet. -// NeoForge's class loader is *really* annoying. -provided("org.cloudburstmc.math", "api") - architectury { platformSetupLoomIde() neoForge() } -val includeTransitive: Configuration = configurations.getByName("includeTransitive") +// This is provided by "org.cloudburstmc.math.mutable" too, so yeet. +// NeoForge's class loader is *really* annoying. +provided("org.cloudburstmc.math", "api") +provided("com.google.errorprone", "error_prone_annotations") + +// Jackson shipped by Minecraft is too old, so we shade & relocate our newer version +relocate("com.fasterxml.jackson") dependencies { // See https://github.com/google/guava/issues/6618 @@ -24,16 +27,30 @@ dependencies { neoForge(libs.neoforge.minecraft) api(project(":mod", configuration = "namedElements")) - shadow(project(path = ":mod", configuration = "transformProductionNeoForge")) { - isTransitive = false - } - shadow(project(path = ":core")) { isTransitive = false } + shadowBundle(project(path = ":mod", configuration = "transformProductionNeoForge")) + shadowBundle(projects.core) + // Minecraft (1.21.2+) includes jackson. But an old version! + shadowBundle(libs.jackson.core) + shadowBundle(libs.jackson.databind) + shadowBundle(libs.jackson.dataformat.yaml) + shadowBundle(libs.jackson.annotations) + + // Let's shade in our own api + shadowBundle(projects.api) + + // cannot be shaded, since neoforge will complain if floodgate-neoforge tries to provide this + include(projects.common) + + // Include all transitive deps of core via JiJ includeTransitive(projects.core) + + modImplementation(libs.cloud.neoforge) + include(libs.cloud.neoforge) } -application { - mainClass.set("org.geysermc.geyser.platform.forge.GeyserNeoForgeMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.neoforge.GeyserNeoForgeMain" } tasks { @@ -44,8 +61,14 @@ tasks { remapModrinthJar { archiveBaseName.set("geyser-neoforge") } + + shadowJar { + // Without this, jackson's service files are not relocated + mergeServiceFiles() + } } modrinth { loaders.add("neoforge") -} \ No newline at end of file + uploadFile.set(tasks.getByPath("remapModrinthJar")) +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java index 67cad1683..803a06b6e 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java @@ -26,36 +26,64 @@ package org.geysermc.geyser.platform.neoforge; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.world.entity.player.Player; -import net.neoforged.api.distmarker.Dist; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.GameShuttingDownEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent; import net.neoforged.neoforge.event.server.ServerStoppingEvent; -import org.checkerframework.checker.nullness.qual.NonNull; +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.platform.mod.GeyserModUpdateListener; +import org.geysermc.geyser.platform.mod.command.ModCommandSource; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.neoforge.NeoForgeServerCommandManager; + +import java.util.Objects; @Mod(ModConstants.MOD_ID) public class GeyserNeoForgeBootstrap extends GeyserModBootstrap { - private final GeyserNeoForgePermissionHandler permissionHandler = new GeyserNeoForgePermissionHandler(); + public GeyserNeoForgeBootstrap(ModContainer container) { + super(new GeyserNeoForgePlatform(container)); - public GeyserNeoForgeBootstrap() { - super(new GeyserNeoForgePlatform()); - - if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) { + if (isServer()) { // Set as an event so we can get the proper IP and port if needed NeoForge.EVENT_BUS.addListener(this::onServerStarted); + } else { + NeoForge.EVENT_BUS.addListener(this::onClientStopping); } NeoForge.EVENT_BUS.addListener(this::onServerStopping); NeoForge.EVENT_BUS.addListener(this::onPlayerJoin); - NeoForge.EVENT_BUS.addListener(this.permissionHandler::onPermissionGather); + + NeoForge.EVENT_BUS.addListener(EventPriority.HIGHEST, this::onPermissionGather); this.onGeyserInitialize(); + + var sourceConverter = CommandSourceConverter.layered( + CommandSourceStack.class, + id -> getServer().getPlayerList().getPlayer(id), + ServerPlayer::createCommandSourceStack, + () -> getServer().createCommandSourceStack(), + ModCommandSource::new + ); + CommandManager cloud = new NeoForgeServerCommandManager<>( + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + GeyserNeoForgeCommandRegistry registry = new GeyserNeoForgeCommandRegistry(getGeyser(), cloud); + this.setCommandRegistry(registry); + // An auxiliary listener for registering undefined permissions belonging to commands. See javadocs for more info. + NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, registry::onPermissionGatherForUndefined); } private void onServerStarted(ServerStartedEvent event) { @@ -64,20 +92,39 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap { } private void onServerStopping(ServerStoppingEvent event) { + if (isServer()) { + this.onGeyserShutdown(); + } else { + this.onGeyserDisable(); + } + } + + private void onClientStopping(GameShuttingDownEvent ignored) { this.onGeyserShutdown(); } private void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) { - GeyserModUpdateListener.onPlayReady(event.getEntity()); + if (event.getEntity() instanceof ServerPlayer player) { + GeyserModUpdateListener.onPlayReady(player); + } } @Override - public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) { - return this.permissionHandler.hasPermission(source, permissionNode); + public boolean isServer() { + return FMLLoader.getCurrent().getDist().isDedicatedServer(); } - @Override - public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) { - return this.permissionHandler.hasPermission(source, permissionNode, permissionLevel); + private void onPermissionGather(PermissionGatherEvent.Nodes event) { + getGeyser().eventBus().fire( + (GeyserRegisterPermissionsEvent) (permission, defaultValue) -> { + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(defaultValue, "permission default for " + permission); + + if (permission.isBlank()) { + return; + } + PermissionUtils.register(permission, defaultValue, event); + } + ); } } diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java new file mode 100644 index 000000000..a8854d5d9 --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.neoforge; + +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.neoforge.PermissionNotRegisteredException; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class GeyserNeoForgeCommandRegistry extends CommandRegistry { + + /** + * Permissions with an undefined permission default. Use Set to not register the same fallback more than once. + * NeoForge requires that all permissions are registered, and cloud-neoforge follows that. + * This is unlike most platforms, on which we wouldn't register a permission if no default was provided. + */ + private final Set undefinedPermissions = new HashSet<>(); + + public GeyserNeoForgeCommandRegistry(GeyserImpl geyser, CommandManager cloud) { + super(geyser, cloud); + } + + @Override + protected void register(GeyserCommand command, Map commands) { + super.register(command, commands); + + // FIRST STAGE: Collect all permissions that may have undefined defaults. + if (!command.permission().isBlank() && command.permissionDefault() == null) { + // Permission requirement exists but no default value specified. + undefinedPermissions.add(command.permission()); + } + } + + @Override + protected void onRegisterPermissions(GeyserRegisterPermissionsEvent event) { + super.onRegisterPermissions(event); + + // SECOND STAGE + // Now that we are aware of all commands, we can eliminate some incorrect assumptions. + // Example: two commands may have the same permission, but only of them defines a permission default. + undefinedPermissions.removeAll(permissionDefaults.keySet()); + } + + /** + * Registers permissions with possibly undefined defaults. + * Should be subscribed late to allow extensions and mods to register a desired permission default first. + */ + void onPermissionGatherForUndefined(PermissionGatherEvent.Nodes event) { + // THIRD STAGE + for (String permission : undefinedPermissions) { + if (PermissionUtils.register(permission, TriState.NOT_SET, event)) { + // The permission was not already registered + geyser.getLogger().debug("Registered permission " + permission + " with fallback default value of NOT_SET"); + } + } + } + + @Override + public boolean hasPermission(GeyserCommandSource source, String permission) { + // NeoForgeServerCommandManager will throw this exception if the permission is not registered to the server. + // We can't realistically ensure that every permission is registered (calls by API users), so we catch this. + // This works for our calls, but not for cloud's internal usage. For that case, see above. + try { + return super.hasPermission(source, permission); + } catch (PermissionNotRegisteredException e) { + return false; + } + } +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java index 623f68d3a..8348ca160 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java @@ -54,10 +54,10 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo { private final List mods; public GeyserNeoForgeDumpInfo(MinecraftServer server) { - this.platformName = FMLLoader.launcherHandlerName(); - this.platformVersion = FMLLoader.versionInfo().neoForgeVersion(); - this.minecraftVersion = FMLLoader.versionInfo().mcVersion(); - this.dist = FMLLoader.getDist(); + this.platformName = server.getServerModName(); + this.platformVersion = FMLLoader.getCurrent().getVersionInfo().neoForgeVersion(); + this.minecraftVersion = FMLLoader.getCurrent().getVersionInfo().mcVersion(); + this.dist = FMLLoader.getCurrent().getDist(); this.serverIP = server.getLocalIp() == null ? "unknown" : server.getLocalIp(); this.serverPort = server.getPort(); this.onlineMode = server.usesAuthentication(); @@ -81,4 +81,4 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo { public String version; public String url; } -} \ No newline at end of file +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java deleted file mode 100644 index 0a5f8f052..000000000 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.neoforge; - -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.entity.player.Player; -import net.neoforged.neoforge.server.permission.PermissionAPI; -import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; -import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKey; -import net.neoforged.neoforge.server.permission.nodes.PermissionNode; -import net.neoforged.neoforge.server.permission.nodes.PermissionType; -import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.Constants; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.command.GeyserCommandManager; - -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Map; - -public class GeyserNeoForgePermissionHandler { - - private static final Constructor PERMISSION_NODE_CONSTRUCTOR; - - static { - try { - @SuppressWarnings("rawtypes") - Constructor constructor = PermissionNode.class.getDeclaredConstructor( - String.class, - PermissionType.class, - PermissionNode.PermissionResolver.class, - PermissionDynamicContextKey[].class - ); - constructor.setAccessible(true); - PERMISSION_NODE_CONSTRUCTOR = constructor; - } catch (NoSuchMethodException e) { - throw new RuntimeException("Unable to construct PermissionNode!", e); - } - } - - private final Map> permissionNodes = new HashMap<>(); - - public void onPermissionGather(PermissionGatherEvent.Nodes event) { - this.registerNode(Constants.UPDATE_PERMISSION, event); - - GeyserCommandManager commandManager = GeyserImpl.getInstance().commandManager(); - for (Map.Entry entry : commandManager.commands().entrySet()) { - Command command = entry.getValue(); - - // Don't register aliases - if (!command.name().equals(entry.getKey())) { - continue; - } - - this.registerNode(command.permission(), event); - } - - for (Map commands : commandManager.extensionCommands().values()) { - for (Map.Entry entry : commands.entrySet()) { - Command command = entry.getValue(); - - // Don't register aliases - if (!command.name().equals(entry.getKey())) { - continue; - } - - this.registerNode(command.permission(), event); - } - } - } - - public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) { - PermissionNode node = this.permissionNodes.get(permissionNode); - if (node == null) { - GeyserImpl.getInstance().getLogger().warning("Unable to find permission node " + permissionNode); - return false; - } - - return PermissionAPI.getPermission((ServerPlayer) source, node); - } - - public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) { - if (!source.isPlayer()) { - return true; - } - assert source.getPlayer() != null; - boolean permission = this.hasPermission(source.getPlayer(), permissionNode); - if (!permission) { - return source.getPlayer().hasPermissions(permissionLevel); - } - - return true; - } - - private void registerNode(String node, PermissionGatherEvent.Nodes event) { - PermissionNode permissionNode = this.createNode(node); - - // NeoForge likes to crash if you try and register a duplicate node - if (!event.getNodes().contains(permissionNode)) { - event.addNodes(permissionNode); - this.permissionNodes.put(node, permissionNode); - } - } - - @SuppressWarnings("unchecked") - private PermissionNode createNode(String node) { - // The typical constructors in PermissionNode require a - // mod id, which means our permission nodes end up becoming - // geyser_neoforge. instead of just . We work around - // this by using reflection to access the constructor that - // doesn't require a mod id or ResourceLocation. - try { - return (PermissionNode) PERMISSION_NODE_CONSTRUCTOR.newInstance( - node, - PermissionTypes.BOOLEAN, - (PermissionNode.PermissionResolver) (player, playerUUID, context) -> false, - new PermissionDynamicContextKey[0] - ); - } catch (Exception e) { - throw new RuntimeException("Unable to create permission node " + node, e); - } - } -} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java index 63abe4a4a..c1e0b67e0 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java @@ -26,20 +26,28 @@ package org.geysermc.geyser.platform.neoforge; import net.minecraft.server.MinecraftServer; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; import net.neoforged.fml.loading.FMLPaths; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; public class GeyserNeoForgePlatform implements GeyserModPlatform { + private final ModContainer container; + + public GeyserNeoForgePlatform(ModContainer container) { + this.container = container; + } + @Override public @NonNull PlatformType platformType() { return PlatformType.NEOFORGE; @@ -62,11 +70,20 @@ public class GeyserNeoForgePlatform implements GeyserModPlatform { @Override public boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap) { - return false; // No Floodgate mod for NeoForge yet + if (ModList.get().isLoaded("floodgate")) { + Path floodgateDataFolder = FMLPaths.CONFIGDIR.get().resolve("floodgate"); + bootstrap.getGeyserConfig().loadFloodgate(bootstrap, floodgateDataFolder); + return true; + } + return false; } @Override public @Nullable InputStream resolveResource(@NonNull String resource) { - return GeyserBootstrap.class.getClassLoader().getResourceAsStream(resource); + try { + return container.getModInfo().getOwningFile().getFile().getContents().openFile(resource); + } catch (IOException e) { + return null; + } } } diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java new file mode 100644 index 000000000..6ed5bc608 --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.neoforge; + +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.platform.neoforge.mixin.PermissionNodeMixin; + +import java.util.Objects; + +/** + * Common logic for handling the more complicated way we have to register permission on NeoForge + */ +public class PermissionUtils { + + private PermissionUtils() { + //no + } + + /** + * Registers the given permission and its default value to the event. If the permission has the same name as one + * that has already been registered to the event, it will not be registered. In other words, it will not override. + * + * @param permission the permission to register + * @param permissionDefault the permission's default value. See {@link GeyserRegisterPermissionsEvent#register(String, TriState)} for TriState meanings. + * @param event the registration event + * @return true if the permission was registered + */ + public static boolean register(String permission, TriState permissionDefault, PermissionGatherEvent.Nodes event) { + // NeoForge likes to crash if you try and register a duplicate node + if (event.getNodes().stream().noneMatch(n -> n.getNodeName().equals(permission))) { + PermissionNode node = createNode(permission, permissionDefault); + event.addNodes(node); + return true; + } + return false; + } + + private static PermissionNode createNode(String node, TriState permissionDefault) { + return PermissionNodeMixin.geyser$construct( + node, + PermissionTypes.BOOLEAN, + (player, playerUUID, context) -> switch (permissionDefault) { + case TRUE -> true; + case FALSE -> false; + case NOT_SET -> { + if (player != null) { + yield player.createCommandSourceStack().hasPermission(Objects.requireNonNull(player.level()).getServer().operatorUserPermissionLevel()); + } + yield false; // NeoForge javadocs say player is null in the case of an offline player. + } + } + ); + } +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java new file mode 100644 index 000000000..a43acd58a --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.neoforge.mixin; + +import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKey; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(value = PermissionNode.class, remap = false) // this is API - do not remap +public interface PermissionNodeMixin { + + /** + * Invokes the matching private constructor in {@link PermissionNode}. + *

+ * The typical constructors in PermissionNode require a mod id, which means our permission nodes + * would end up becoming {@code geyser_neoforge.} instead of just {@code }. + */ + @SuppressWarnings("rawtypes") // the varargs + @Invoker("") + static PermissionNode geyser$construct(String nodeName, PermissionType type, PermissionNode.PermissionResolver defaultResolver, PermissionDynamicContextKey... dynamics) { + throw new IllegalStateException(); + } +} diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml similarity index 79% rename from bootstrap/mod/neoforge/src/main/resources/META-INF/mods.toml rename to bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 2f9e9b12e..7eb124724 100644 --- a/bootstrap/mod/neoforge/src/main/resources/META-INF/mods.toml +++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -11,15 +11,17 @@ authors="GeyserMC" description="${description}" [[mixins]] config = "geyser.mixins.json" +[[mixins]] +config = "geyser_neoforge.mixins.json" [[dependencies.geyser_neoforge]] modId="neoforge" type="required" - versionRange="[20.4.48-beta,)" + versionRange="[21.9.14-beta,)" ordering="NONE" side="BOTH" [[dependencies.geyser_neoforge]] modId="minecraft" type="required" - versionRange="[1.20,1.21)" + versionRange="[1.21.9,)" ordering="NONE" - side="BOTH" \ No newline at end of file + side="BOTH" diff --git a/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json b/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json new file mode 100644 index 000000000..f1653051c --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.geysermc.geyser.platform.neoforge.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "PermissionNodeMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java index db966ec1a..2e3c4fad5 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java @@ -25,31 +25,21 @@ package org.geysermc.geyser.platform.mod; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.Commands; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.entity.player.Player; -import org.apache.logging.log4j.LogManager; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.mod.command.GeyserModCommandExecutor; import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform; import org.geysermc.geyser.platform.mod.world.GeyserModWorldManager; import org.geysermc.geyser.text.GeyserLocale; @@ -58,8 +48,8 @@ import org.geysermc.geyser.util.FileUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.SocketAddress; import java.nio.file.Path; -import java.util.Map; import java.util.UUID; @RequiredArgsConstructor @@ -70,16 +60,18 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { private final GeyserModPlatform platform; + @Getter private GeyserImpl geyser; private Path dataFolder; - @Setter + @Setter @Getter private MinecraftServer server; - private GeyserCommandManager geyserCommandManager; + @Setter + private CommandRegistry commandRegistry; private GeyserModConfiguration geyserConfig; private GeyserModInjector geyserInjector; - private GeyserModLogger geyserLogger; + private final GeyserModLogger geyserLogger = new GeyserModLogger(); private IGeyserPingPassthrough geyserPingPassthrough; private WorldManager geyserWorldManager; @@ -91,16 +83,17 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { if (!loadConfig()) { return; } - this.geyserLogger = new GeyserModLogger(geyserConfig.isDebugMode()); + this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); this.geyser = GeyserImpl.load(this.platform.platformType(), this); - - // Create command manager here, since the permission handler on neo needs it - this.geyserCommandManager = new GeyserCommandManager(geyser); - this.geyserCommandManager.init(); } public void onGeyserEnable() { + // "Disabling" a mod isn't possible; so if we fail to initialize we need to manually stop here + if (geyser == null) { + return; + } + if (GeyserImpl.getInstance().isReloading()) { if (!loadConfig()) { return; @@ -127,50 +120,8 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { // We want to do this late in the server startup process to allow other mods // To do their job injecting, then connect into *that* this.geyserInjector = new GeyserModInjector(server, this.platform); - this.geyserInjector.initializeLocalChannel(this); - - // Start command building - // Set just "geyser" as the help command - GeyserModCommandExecutor helpExecutor = new GeyserModCommandExecutor(geyser, - (GeyserCommand) geyser.commandManager().getCommands().get("help")); - LiteralArgumentBuilder builder = Commands.literal("geyser").executes(helpExecutor); - - // Register all subcommands as valid - for (Map.Entry command : geyser.commandManager().getCommands().entrySet()) { - GeyserModCommandExecutor executor = new GeyserModCommandExecutor(geyser, (GeyserCommand) command.getValue()); - builder.then(Commands.literal(command.getKey()) - .executes(executor) - // Could also test for Bedrock but depending on when this is called it may backfire - .requires(executor::testPermission) - // Allows parsing of arguments; e.g. for /geyser dump logs or the connectiontest command - .then(Commands.argument("args", StringArgumentType.greedyString()) - .executes(context -> executor.runWithArgs(context, StringArgumentType.getString(context, "args"))) - .requires(executor::testPermission))); - } - server.getCommands().getDispatcher().register(builder); - - // Register extension commands - for (Map.Entry> extensionMapEntry : geyser.commandManager().extensionCommands().entrySet()) { - Map extensionCommands = extensionMapEntry.getValue(); - if (extensionCommands.isEmpty()) { - continue; - } - - // Register help command for just "/" - GeyserModCommandExecutor extensionHelpExecutor = new GeyserModCommandExecutor(geyser, - (GeyserCommand) extensionCommands.get("help")); - LiteralArgumentBuilder extCmdBuilder = Commands.literal(extensionMapEntry.getKey().description().id()).executes(extensionHelpExecutor); - - for (Map.Entry command : extensionCommands.entrySet()) { - GeyserModCommandExecutor executor = new GeyserModCommandExecutor(geyser, (GeyserCommand) command.getValue()); - extCmdBuilder.then(Commands.literal(command.getKey()) - .executes(executor) - .requires(executor::testPermission) - .then(Commands.argument("args", StringArgumentType.greedyString()) - .executes(context -> executor.runWithArgs(context, StringArgumentType.getString(context, "args"))) - .requires(executor::testPermission))); - } - server.getCommands().getDispatcher().register(extCmdBuilder); + if (isServer()) { + this.geyserInjector.initializeLocalChannel(this); } } @@ -204,8 +155,8 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return commandRegistry; } @Override @@ -233,6 +184,7 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { return this.server.getServerVersion(); } + @SuppressWarnings("ConstantConditions") // Certain IDEA installations think that ip cannot be null @NonNull @Override public String getServerBindAddress() { @@ -241,10 +193,22 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { } @Override - public int getServerPort() { - return ((GeyserServerPortGetter) server).geyser$getServerPort(); + public SocketAddress getSocketAddress() { + return this.geyserInjector.getServerSocketAddress(); } + @Override + public int getServerPort() { + if (isServer()) { + return ((GeyserServerPortGetter) server).geyser$getServerPort(); + } else { + // Set in the IntegratedServerMixin + return geyserConfig.getRemote().port(); + } + } + + public abstract boolean isServer(); + @Override public boolean testFloodgatePluginPresent() { return this.platform.testFloodgatePluginPresent(this); @@ -256,10 +220,6 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { return this.platform.resolveResource(resource); } - public abstract boolean hasPermission(@NonNull Player source, @NonNull String permissionNode); - - public abstract boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel); - @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { try { @@ -273,9 +233,14 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { this.geyserConfig = FileUtils.loadConfig(configFile, GeyserModConfiguration.class); return true; } catch (IOException ex) { - LogManager.getLogger("geyser").error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); return false; } } + + @Override + public @NonNull String getServerPlatform() { + return server.getServerModName(); + } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java index 631a21510..89ca53544 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java @@ -28,8 +28,8 @@ package org.geysermc.geyser.platform.mod; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; -import net.minecraft.network.protocol.login.ClientboundGameProfilePacket; import net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket; +import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket; /** * Disables the compression packet (and the compression handlers from being added to the pipeline) for Geyser clients @@ -45,7 +45,7 @@ public class GeyserModCompressionDisabler extends ChannelOutboundHandlerAdapter Class msgClass = msg.getClass(); // Don't let any compression packet get through if (!ClientboundLoginCompressionPacket.class.isAssignableFrom(msgClass)) { - if (ClientboundGameProfilePacket.class.isAssignableFrom(msgClass)) { + if (ClientboundLoginFinishedPacket.class.isAssignableFrom(msgClass)) { // We're past the point that a compression packet can be sent, so we can safely yeet ourselves away ctx.channel().pipeline().remove(this); diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java index 06496293f..624eccb3f 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java @@ -93,8 +93,11 @@ public class GeyserModInjector extends GeyserInjector { protected void initChannel(@NonNull Channel ch) throws Exception { initChannel.invoke(childHandler, ch); + int index = ch.pipeline().names().indexOf("encoder"); + String baseName = index != -1 ? "encoder" : "outbound_config"; + if (bootstrap.getGeyserConfig().isDisableCompression()) { - ch.pipeline().addAfter("encoder", "geyser-compression-disabler", new GeyserModCompressionDisabler()); + ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserModCompressionDisabler()); } } }) diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java index 444b725e9..da66b32c3 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java @@ -37,10 +37,6 @@ public class GeyserModLogger implements GeyserLogger { private boolean debug; - public GeyserModLogger(boolean isDebug) { - debug = isDebug; - } - @Override public void severe(String message) { logger.fatal(message); @@ -88,6 +84,13 @@ public class GeyserModLogger implements GeyserLogger { } } + @Override + public void debug(String message, Object... arguments) { + if (debug) { + logger.info(String.format(message, arguments)); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java index 11ca0bc4f..6948bd1e1 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java @@ -25,17 +25,22 @@ package org.geysermc.geyser.platform.mod; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.world.entity.player.Player; -import org.geysermc.geyser.Constants; -import org.geysermc.geyser.platform.mod.command.ModCommandSender; +import net.minecraft.server.level.ServerPlayer; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; +import org.geysermc.geyser.platform.mod.command.ModCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; public final class GeyserModUpdateListener { - public static void onPlayReady(Player player) { - CommandSourceStack stack = player.createCommandSourceStack(); - if (GeyserModBootstrap.getInstance().hasPermission(stack, Constants.UPDATE_PERMISSION, 2)) { - VersionCheckUtils.checkForGeyserUpdate(() -> new ModCommandSender(stack)); + public static void onPlayReady(ServerPlayer player) { + // We could just not register the listener, but, this allows config reloading + if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + // Should be creating this in the supplier, but we need it for the permission check. + // Not a big deal currently because ModCommandSource doesn't load locale, so don't need to try to wait for it. + ModCommandSource source = new ModCommandSource(player.createCommandSourceStack()); + if (source.hasPermission(Permissions.CHECK_UPDATE)) { + VersionCheckUtils.checkForGeyserUpdate(() -> source); + } } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java index 12d690d83..2b2e3b736 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java @@ -25,21 +25,20 @@ package org.geysermc.geyser.platform.mod; +import com.mojang.serialization.JsonOps; +import io.netty.channel.ChannelFutureListener; import lombok.AllArgsConstructor; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.minecraft.network.Connection; -import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; import net.minecraft.network.protocol.status.ServerStatus; import net.minecraft.network.protocol.status.ServerStatusPacketListener; import net.minecraft.network.protocol.status.ServerboundStatusRequestPacket; +import net.minecraft.resources.RegistryOps; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerStatusPacketListenerImpl; -import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.ping.GeyserPingInfo; @@ -51,9 +50,6 @@ import java.util.Objects; @AllArgsConstructor public class ModPingPassthrough implements IGeyserPingPassthrough { - private static final GsonComponentSerializer GSON_SERIALIZER = GsonComponentSerializer.gson(); - private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection(); - private final MinecraftServer server; private final GeyserLogger logger; @@ -69,7 +65,7 @@ public class ModPingPassthrough implements IGeyserPingPassthrough { StatusInterceptor connection = new StatusInterceptor(); ServerStatusPacketListener statusPacketListener = new ServerStatusPacketListenerImpl(status, connection); - statusPacketListener.handleStatusRequest(new ServerboundStatusRequestPacket()); + statusPacketListener.handleStatusRequest(ServerboundStatusRequestPacket.INSTANCE); // mods like MiniMOTD (that inject into the above method) have now processed the response status = Objects.requireNonNull(connection.status, "status response"); } catch (Exception e) { @@ -79,11 +75,8 @@ public class ModPingPassthrough implements IGeyserPingPassthrough { } } - String jsonDescription = net.minecraft.network.chat.Component.Serializer.toJson(status.description()); - String legacyDescription = LEGACY_SERIALIZER.serialize(GSON_SERIALIZER.deserializeOr(jsonDescription, Component.empty())); - return new GeyserPingInfo( - legacyDescription, + ComponentSerialization.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, server.registryAccess()), status.description()).getOrThrow().toString(), status.players().map(ServerStatus.Players::max).orElse(1), status.players().map(ServerStatus.Players::online).orElse(0) ); @@ -101,11 +94,11 @@ public class ModPingPassthrough implements IGeyserPingPassthrough { } @Override - public void send(@NonNull Packet packet, @Nullable PacketSendListener packetSendListener, boolean bl) { + public void send(Packet packet, @Nullable ChannelFutureListener channelFutureListener, boolean bl) { if (packet instanceof ClientboundStatusResponsePacket statusResponse) { status = statusResponse.status(); } - super.send(packet, packetSendListener, bl); + super.send(packet, channelFutureListener, bl); } } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java deleted file mode 100644 index 694dc732e..000000000 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.mod.command; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.context.CommandContext; -import net.minecraft.commands.CommandSourceStack; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.platform.mod.GeyserModBootstrap; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.ChatColor; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Collections; - -public class GeyserModCommandExecutor extends GeyserCommandExecutor implements Command { - private final GeyserCommand command; - - public GeyserModCommandExecutor(GeyserImpl geyser, GeyserCommand command) { - super(geyser, Collections.singletonMap(command.name(), command)); - this.command = command; - } - - public boolean testPermission(CommandSourceStack source) { - return GeyserModBootstrap.getInstance().hasPermission(source, command.permission(), command.isSuggestedOpOnly() ? 2 : 0); - } - - @Override - public int run(CommandContext context) { - return runWithArgs(context, ""); - } - - public int runWithArgs(CommandContext context, String args) { - CommandSourceStack source = context.getSource(); - ModCommandSender sender = new ModCommandSender(source); - GeyserSession session = getGeyserSession(sender); - if (!testPermission(source)) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return 0; - } - - if (command.isBedrockOnly() && session == null) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.locale())); - return 0; - } - - command.execute(session, sender, args.split(" ")); - return 0; - } -} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java similarity index 66% rename from bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java index 17154ffd8..f34164b5a 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java @@ -25,24 +25,29 @@ package org.geysermc.geyser.platform.mod.command; +import com.google.gson.JsonElement; +import com.mojang.serialization.JsonOps; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.resources.RegistryOps; import net.minecraft.server.level.ServerPlayer; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.text.ChatColor; -import java.util.Objects; +import java.util.UUID; -public class ModCommandSender implements GeyserCommandSource { +public class ModCommandSource implements GeyserCommandSource { private final CommandSourceStack source; - public ModCommandSender(CommandSourceStack source) { + public ModCommandSource(CommandSourceStack source) { this.source = source; + // todo find locale? } @Override @@ -62,8 +67,8 @@ public class ModCommandSender implements GeyserCommandSource { @Override public void sendMessage(net.kyori.adventure.text.Component message) { if (source.getEntity() instanceof ServerPlayer player) { - String decoded = GsonComponentSerializer.gson().serialize(message); - player.displayClientMessage(Objects.requireNonNull(Component.Serializer.fromJson(decoded)), false); + JsonElement jsonComponent = GsonComponentSerializer.gson().serializeToTree(message); + player.displayClientMessage(ComponentSerialization.CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, player.registryAccess()), jsonComponent).getOrThrow(), false); return; } GeyserCommandSource.super.sendMessage(message); @@ -74,8 +79,24 @@ public class ModCommandSender implements GeyserCommandSource { return !(source.getEntity() instanceof ServerPlayer); } + @Override + public @Nullable UUID playerUuid() { + if (source.getEntity() instanceof ServerPlayer player) { + return player.getUUID(); + } + return null; + } + @Override public boolean hasPermission(String permission) { - return GeyserModBootstrap.getInstance().hasPermission(source, permission, source.getServer().getOperatorUserPermissionLevel()); + // Unlike other bootstraps; we delegate to cloud here too: + // On NeoForge; we'd have to keep track of all PermissionNodes - cloud already does that + // For Fabric, we won't need to include the Fabric Permissions API anymore - cloud already does that too :p + return GeyserImpl.getInstance().commandRegistry().hasPermission(this, permission); + } + + @Override + public Object handle() { + return source; } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java index 4db1165fc..ece2f730a 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java @@ -54,8 +54,10 @@ public class IntegratedServerMixin implements GeyserServerPortGetter { private void onOpenToLan(GameType gameType, boolean cheatsAllowed, int port, CallbackInfoReturnable cir) { if (cir.getReturnValueZ()) { // If the LAN is opened, starts Geyser. - GeyserModBootstrap.getInstance().setServer((MinecraftServer) (Object) this); - GeyserModBootstrap.getInstance().onGeyserEnable(); + GeyserModBootstrap instance = GeyserModBootstrap.getInstance(); + instance.setServer((MinecraftServer) (Object) this); + instance.getGeyserConfig().getRemote().setPort(port); + instance.onGeyserEnable(); // Ensure player locale has been loaded, in case it's different from Java system language GeyserLocale.loadGeyserLocale(this.minecraft.options.languageCode); // Give indication that Geyser is loaded diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java new file mode 100644 index 000000000..a2d76497e --- /dev/null +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.mod.mixin.server; + +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.SoundEvent; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockItem.class) +public class BlockPlaceMixin { + + @Inject(method = "place", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;playSound(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/BlockPos;Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FF)V")) + private void geyser$hijackPlaySound(BlockPlaceContext blockPlaceContext, CallbackInfoReturnable callbackInfoReturnable, + @Local BlockPos pos, @Local Player player, @Local(ordinal = 1) BlockState placedState) { + if (player == null) { + return; + } + + GeyserSession session = GeyserImpl.getInstance().connectionByUuid(player.getUUID()); + if (session == null) { + return; + } + + Vector3f position = Vector3f.from( + pos.getX(), + pos.getY(), + pos.getZ() + ); + + LevelSoundEventPacket placeBlockSoundPacket = new LevelSoundEventPacket(); + placeBlockSoundPacket.setSound(SoundEvent.PLACE); + placeBlockSoundPacket.setPosition(position); + placeBlockSoundPacket.setBabySound(false); + placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(Block.BLOCK_STATE_REGISTRY.getId(placedState))); + placeBlockSoundPacket.setIdentifier(":"); + session.sendUpstreamPacket(placeBlockSoundPacket); + session.setLastBlockPlacePosition(null); + session.setLastBlockPlaced(null); + } +} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java index 3b809d321..84e3303c9 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java @@ -30,7 +30,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.Services; import net.minecraft.server.WorldStem; import net.minecraft.server.dedicated.DedicatedServer; -import net.minecraft.server.level.progress.ChunkProgressListenerFactory; +import net.minecraft.server.level.progress.LevelLoadListener; import net.minecraft.server.packs.repository.PackRepository; import net.minecraft.world.level.storage.LevelStorageSource; import org.geysermc.geyser.platform.mod.GeyserServerPortGetter; @@ -40,8 +40,9 @@ import java.net.Proxy; @Mixin(DedicatedServer.class) public abstract class DedicatedServerMixin extends MinecraftServer implements GeyserServerPortGetter { - public DedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer dataFixer, Services services, ChunkProgressListenerFactory chunkProgressListenerFactory) { - super(thread, levelStorageAccess, packRepository, worldStem, proxy, dataFixer, services, chunkProgressListenerFactory); + + public DedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer dataFixer, Services services, LevelLoadListener levelLoadListener) { + super(thread, levelStorageAccess, packRepository, worldStem, proxy, dataFixer, services, levelLoadListener); } @Override diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java new file mode 100644 index 000000000..6ac51ba52 --- /dev/null +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.mod.mixin.server; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.cloudburstmc.math.vector.Vector3i; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.PistonCache; +import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValueType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Mixin(PistonBaseBlock.class) +public class PistonBaseBlockMixin { + + @Shadow + @Final + private boolean isSticky; + + @ModifyExpressionValue(method = "moveBlocks", + at = @At(value = "INVOKE", target = "Lcom/google/common/collect/Maps;newHashMap()Ljava/util/HashMap;") + ) + private HashMap geyser$onMapCreate(HashMap original, @Share("pushBlocks") LocalRef> localRef) { + localRef.set(original); + return original; + } + + @Inject(method = "moveBlocks", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/piston/PistonStructureResolver;getToDestroy()Ljava/util/List;") + ) + private void geyser$onBlocksMove(Level level, BlockPos blockPos, Direction direction, boolean isExtending, CallbackInfoReturnable cir, @Share("pushBlocks") LocalRef> localRef) { + PistonValueType type = isExtending ? PistonValueType.PUSHING : PistonValueType.PULLING; + boolean sticky = this.isSticky; + + Object2ObjectMap attachedBlocks = new Object2ObjectArrayMap<>(); + boolean blocksFilled = false; + + for (Map.Entry entry : GeyserImpl.getInstance().getSessionManager().getSessions().entrySet()) { + Player player = level.getPlayerByUUID(entry.getKey()); + //noinspection resource + if (player == null || !player.level().equals(level)) { + continue; + } + GeyserSession session = entry.getValue(); + + int dX = Math.abs(blockPos.getX() - player.getBlockX()) >> 4; + int dZ = Math.abs(blockPos.getZ() - player.getBlockZ()) >> 4; + if ((dX * dX + dZ * dZ) > session.getServerRenderDistance() * session.getServerRenderDistance()) { + // Ignore pistons outside the player's render distance + continue; + } + + // Trying to grab the blocks from the world like other platforms would result in the moving piston block + // being returned instead. + if (!blocksFilled) { + Map blocks = localRef.get(); + for (Map.Entry blockStateEntry : blocks.entrySet()) { + int blockStateId = Block.BLOCK_STATE_REGISTRY.getId(blockStateEntry.getValue()); + org.geysermc.geyser.level.block.type.BlockState state = org.geysermc.geyser.level.block.type.BlockState.of(blockStateId); + attachedBlocks.put(geyser$fromBlockPos(blockStateEntry.getKey()), state); + } + blocksFilled = true; + } + + org.geysermc.geyser.level.physics.Direction orientation = org.geysermc.geyser.level.physics.Direction.VALUES[direction.ordinal()]; + + Vector3i position = geyser$fromBlockPos(blockPos); + session.executeInEventLoop(() -> { + PistonCache pistonCache = session.getPistonCache(); + PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> + new PistonBlockEntity(session, position, orientation, sticky, !isExtending)); + blockEntity.setAction(type, attachedBlocks); + }); + } + } + + @Unique + private static Vector3i geyser$fromBlockPos(BlockPos pos) { + return Vector3i.from(pos.getX(), pos.getY(), pos.getZ()); + } + +} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java index 04c538632..5ede434c9 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java @@ -25,57 +25,30 @@ package org.geysermc.geyser.platform.mod.world; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; -import net.minecraft.nbt.ByteArrayTag; -import net.minecraft.nbt.ByteTag; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.DoubleTag; -import net.minecraft.nbt.EndTag; -import net.minecraft.nbt.FloatTag; -import net.minecraft.nbt.IntArrayTag; -import net.minecraft.nbt.IntTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.LongArrayTag; -import net.minecraft.nbt.LongTag; -import net.minecraft.nbt.ShortTag; -import net.minecraft.nbt.StringTag; -import net.minecraft.nbt.Tag; -import net.minecraft.nbt.TagVisitor; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.WritableBookItem; -import net.minecraft.world.item.WrittenBookItem; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.entity.BannerBlockEntity; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.LecternBlockEntity; +import net.minecraft.world.level.block.entity.DecoratedPotBlockEntity; import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkStatus; -import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; -import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; -import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtMapBuilder; -import org.cloudburstmc.nbt.NbtType; -import org.geysermc.erosion.util.LecternUtils; import org.geysermc.geyser.level.GeyserWorldManager; import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.util.BlockEntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; public class GeyserModWorldManager extends GeyserWorldManager { + private final MinecraftServer server; public GeyserModWorldManager(MinecraftServer server) { @@ -87,7 +60,7 @@ public class GeyserModWorldManager extends GeyserWorldManager { // If the protocol version of Geyser and the server are not the // same, fallback to the chunk cache. May be able to update this // in the future to use ViaVersion however, like Spigot does. - if (SharedConstants.getCurrentVersion().getProtocolVersion() != GameProtocol.getJavaProtocolVersion()) { + if (SharedConstants.getCurrentVersion().protocolVersion() != GameProtocol.getJavaProtocolVersion()) { return super.getBlockAt(session, x, y, z); } @@ -97,16 +70,17 @@ public class GeyserModWorldManager extends GeyserWorldManager { } Level level = player.level(); - if (y < level.getMinBuildHeight()) { + if (y < level.getMinY()) { return 0; } - ChunkAccess chunk = level.getChunkSource().getChunk(x >> 4, z >> 4, ChunkStatus.FULL, false); + // Only loads active chunks, and doesn't delegate to main thread + ChunkAccess chunk = ((ServerChunkCache) level.getChunkSource()).chunkMap.getChunkToSend(ChunkPos.asLong(x >> 4, z >> 4)); if (chunk == null) { return 0; } - int worldOffset = level.getMinBuildHeight() >> 4; + int worldOffset = level.getMinY() >> 4; int chunkOffset = (y >> 4) - worldOffset; if (chunkOffset < chunk.getSections().length) { LevelChunkSection section = chunk.getSections()[chunkOffset]; @@ -120,103 +94,7 @@ public class GeyserModWorldManager extends GeyserWorldManager { @Override public boolean hasOwnChunkCache() { - return SharedConstants.getCurrentVersion().getProtocolVersion() == GameProtocol.getJavaProtocolVersion(); - } - - @Override - public boolean shouldExpectLecternHandled(GeyserSession session) { - return true; - } - - @Override - public void sendLecternData(GeyserSession session, int x, int z, List blockEntityInfos) { - server.execute(() -> { - ServerPlayer player = getPlayer(session); - if (player == null) { - return; - } - - //noinspection resource - level() is just a getter - LevelChunk chunk = player.level().getChunk(x, z); - final int chunkBlockX = x << 4; - final int chunkBlockZ = z << 4; - //noinspection ForLoopReplaceableByForEach - avoid constructing iterator - for (int i = 0; i < blockEntityInfos.size(); i++) { - BlockEntityInfo blockEntityInfo = blockEntityInfos.get(i); - BlockEntity blockEntity = chunk.getBlockEntity(new BlockPos(chunkBlockX + blockEntityInfo.getX(), - blockEntityInfo.getY(), chunkBlockZ + blockEntityInfo.getZ())); - sendLecternData(session, blockEntity, true); - } - }); - } - - @Override - public void sendLecternData(GeyserSession session, int x, int y, int z) { - server.execute(() -> { - ServerPlayer player = getPlayer(session); - if (player == null) { - return; - } - //noinspection resource - level() is just a getter - BlockEntity blockEntity = player.level().getBlockEntity(new BlockPos(x, y, z)); - sendLecternData(session, blockEntity, false); - }); - } - - private void sendLecternData(GeyserSession session, BlockEntity blockEntity, boolean isChunkLoad) { - if (!(blockEntity instanceof LecternBlockEntity lectern)) { - return; - } - - int x = blockEntity.getBlockPos().getX(); - int y = blockEntity.getBlockPos().getY(); - int z = blockEntity.getBlockPos().getZ(); - - if (!lectern.hasBook()) { - if (!isChunkLoad) { - BlockEntityUtils.updateBlockEntity(session, LecternUtils.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z)); - } - return; - } - - ItemStack book = lectern.getBook(); - int pageCount = WrittenBookItem.getPageCount(book); - boolean hasBookPages = pageCount > 0; - NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x, y, z, hasBookPages ? pageCount : 1); - lecternTag.putInt("page", lectern.getPage() / 2); - NbtMapBuilder bookTag = NbtMap.builder() - .putByte("Count", (byte) book.getCount()) - .putShort("Damage", (short) 0) - .putString("Name", "minecraft:writable_book"); - List pages = new ArrayList<>(hasBookPages ? pageCount : 1); - if (hasBookPages && WritableBookItem.makeSureTagIsValid(book.getTag())) { - ListTag listTag = book.getTag().getList("pages", 8); - - for (int i = 0; i < listTag.size(); i++) { - String page = listTag.getString(i); - NbtMapBuilder pageBuilder = NbtMap.builder() - .putString("photoname", "") - .putString("text", page); - pages.add(pageBuilder.build()); - } - } else { - // Empty page - NbtMapBuilder pageBuilder = NbtMap.builder() - .putString("photoname", "") - .putString("text", ""); - pages.add(pageBuilder.build()); - } - - bookTag.putCompound("tag", NbtMap.builder().putList("pages", NbtType.COMPOUND, pages).build()); - lecternTag.putCompound("book", bookTag.build()); - NbtMap blockEntityTag = lecternTag.build(); - BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z)); - } - - @Override - public boolean hasPermission(GeyserSession session, String permission) { - ServerPlayer player = getPlayer(session); - return GeyserModBootstrap.getInstance().hasPermission(player, permission); + return SharedConstants.getCurrentVersion().protocolVersion() == GameProtocol.getJavaProtocolVersion(); } @Override @@ -224,128 +102,28 @@ public class GeyserModWorldManager extends GeyserWorldManager { return GameMode.byId(server.getDefaultGameType().getId()); } - @NonNull @Override - public CompletableFuture getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) { - CompletableFuture future = new CompletableFuture<>(); + public void getDecoratedPotData(GeyserSession session, Vector3i pos, Consumer> apply) { server.execute(() -> { ServerPlayer player = getPlayer(session); if (player == null) { - future.complete(null); return; } - BlockPos pos = new BlockPos(x, y, z); + BlockPos blockPos = new BlockPos(pos.getX(), pos.getY(), pos.getZ()); // Don't create a new block entity if invalid //noinspection resource - level() is just a getter - BlockEntity blockEntity = player.level().getChunkAt(pos).getBlockEntity(pos); - if (blockEntity instanceof BannerBlockEntity banner) { - // Potentially exposes other NBT data? But we need to get the NBT data for the banner patterns *and* - // the banner might have a custom name, both of which a Java client knows and caches - ItemStack itemStack = banner.getItem(); - var tag = OpenNbtTagVisitor.convert("", itemStack.getOrCreateTag()); - - future.complete(tag); - return; + BlockEntity blockEntity = player.level().getChunkAt(blockPos).getBlockEntity(blockPos); + if (blockEntity instanceof DecoratedPotBlockEntity pot) { + List sherds = pot.getDecorations().ordered() + .stream().map(item -> BuiltInRegistries.ITEM.getKey(item).toString()) + .toList(); + apply.accept(sherds); } - future.complete(null); }); - return future; } private ServerPlayer getPlayer(GeyserSession session) { return server.getPlayerList().getPlayer(session.getPlayerEntity().getUuid()); } - - // Future considerations: option to clone; would affect arrays - private static class OpenNbtTagVisitor implements TagVisitor { - private String currentKey; - private final com.github.steveice10.opennbt.tag.builtin.CompoundTag root; - private com.github.steveice10.opennbt.tag.builtin.Tag currentTag; - - OpenNbtTagVisitor(String key) { - root = new com.github.steveice10.opennbt.tag.builtin.CompoundTag(key); - } - - @Override - public void visitString(StringTag stringTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.StringTag(currentKey, stringTag.getAsString()); - } - - @Override - public void visitByte(ByteTag byteTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteTag(currentKey, byteTag.getAsByte()); - } - - @Override - public void visitShort(ShortTag shortTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.ShortTag(currentKey, shortTag.getAsShort()); - } - - @Override - public void visitInt(IntTag intTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.IntTag(currentKey, intTag.getAsInt()); - } - - @Override - public void visitLong(LongTag longTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.LongTag(currentKey, longTag.getAsLong()); - } - - @Override - public void visitFloat(FloatTag floatTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.FloatTag(currentKey, floatTag.getAsFloat()); - } - - @Override - public void visitDouble(DoubleTag doubleTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.DoubleTag(currentKey, doubleTag.getAsDouble()); - } - - @Override - public void visitByteArray(ByteArrayTag byteArrayTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteArrayTag(currentKey, byteArrayTag.getAsByteArray()); - } - - @Override - public void visitIntArray(IntArrayTag intArrayTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.IntArrayTag(currentKey, intArrayTag.getAsIntArray()); - } - - @Override - public void visitLongArray(LongArrayTag longArrayTag) { - currentTag = new com.github.steveice10.opennbt.tag.builtin.LongArrayTag(currentKey, longArrayTag.getAsLongArray()); - } - - @Override - public void visitList(ListTag listTag) { - var newList = new com.github.steveice10.opennbt.tag.builtin.ListTag(currentKey); - for (Tag tag : listTag) { - currentKey = ""; - tag.accept(this); - newList.add(currentTag); - } - currentTag = newList; - } - - @Override - public void visitCompound(@NonNull CompoundTag compoundTag) { - currentTag = convert(currentKey, compoundTag); - } - - private static com.github.steveice10.opennbt.tag.builtin.CompoundTag convert(String name, CompoundTag compoundTag) { - OpenNbtTagVisitor visitor = new OpenNbtTagVisitor(name); - for (String key : compoundTag.getAllKeys()) { - visitor.currentKey = key; - Tag tag = Objects.requireNonNull(compoundTag.get(key)); - tag.accept(visitor); - visitor.root.put(visitor.currentTag); - } - return visitor.root; - } - - @Override - public void visitEnd(@NonNull EndTag endTag) { - } - } } diff --git a/bootstrap/mod/src/main/resources/geyser.mixins.json b/bootstrap/mod/src/main/resources/geyser.mixins.json index 47b2f60f3..e820e654d 100644 --- a/bootstrap/mod/src/main/resources/geyser.mixins.json +++ b/bootstrap/mod/src/main/resources/geyser.mixins.json @@ -4,6 +4,8 @@ "package": "org.geysermc.geyser.platform.mod.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ + "server.BlockPlaceMixin", + "server.PistonBaseBlockMixin", "server.ServerConnectionListenerMixin" ], "server": [ diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index 41da1a0de..f9252084b 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -1,19 +1,34 @@ +plugins { + id("geyser.platform-conventions") + id("geyser.modrinth-uploading-conventions") + alias(libs.plugins.runpaper) +} + dependencies { api(projects.core) api(libs.erosion.bukkit.common) { isTransitive = false } + implementation(libs.erosion.bukkit.nms) { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21) + } + } + implementation(variantOf(libs.adapters.spigot) { classifier("all") // otherwise the unshaded jar is used without the shaded NMS implementations }) + implementation(variantOf(libs.adapters.paper) { + classifier("all") // otherwise the unshaded jar is used without the shaded NMS implementations + }) + implementation(libs.cloud.paper) implementation(libs.commodore) implementation(libs.adventure.text.serializer.bungeecord) compileOnly(libs.folia.api) - compileOnly(libs.paper.mojangapi) compileOnlyApi(libs.viaversion) } @@ -24,39 +39,45 @@ platformRelocate("com.fasterxml.jackson") platformRelocate("net.kyori", "net.kyori.adventure.text.logger.slf4j.ComponentLogger") platformRelocate("org.objectweb.asm") platformRelocate("me.lucko.commodore") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated platformRelocate("org.yaml") // Broken as of 1.20 - -// These dependencies are already present on the platform provided(libs.viaversion) -application { - mainClass.set("org.geysermc.geyser.platform.spigot.GeyserSpigotMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.spigot.GeyserSpigotMain" } tasks.withType { + + // Prevents Paper 1.20.5+ from remapping Geyser + manifest { + attributes["paperweight-mappings-namespace"] = "mojang" + } + archiveBaseName.set("Geyser-Spigot") dependencies { exclude(dependency("com.google.*:.*")) - // We cannot shade Netty, or else native libraries will not load // Needed because older Spigot builds do not provide the haproxy module - exclude(dependency("io.netty:netty-transport-classes-epoll:.*")) - exclude(dependency("io.netty:netty-transport-native-epoll:.*")) - exclude(dependency("io.netty:netty-transport-native-unix-common:.*")) - exclude(dependency("io.netty:netty-transport-classes-kqueue:.*")) - exclude(dependency("io.netty:netty-transport-native-kqueue:.*")) - exclude(dependency("io.netty:netty-handler:.*")) - exclude(dependency("io.netty:netty-common:.*")) - exclude(dependency("io.netty:netty-buffer:.*")) - exclude(dependency("io.netty:netty-resolver:.*")) - exclude(dependency("io.netty:netty-transport:.*")) - exclude(dependency("io.netty:netty-codec:.*")) - exclude(dependency("io.netty:netty-codec-dns:.*")) - exclude(dependency("io.netty:netty-resolver-dns:.*")) - exclude(dependency("io.netty:netty-resolver-dns-native-macos:.*")) + exclude("io.netty", libs.netty.codec.haproxy) // Commodore includes Brigadier exclude(dependency("com.mojang:.*")) } } + +modrinth { + uploadFile.set(tasks.getByPath("shadowJar")) + gameVersions.addAll("1.16.5", "1.17", "1.17.1", "1.18", "1.18.1", "1.18.2", "1.19", + "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6", + "1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8") + loaders.addAll("spigot", "paper") +} + +tasks { + runServer { + minecraftVersion(libs.versions.runpaperversion.get()) + } +} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java index 930f84cec..9ebd6519a 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java @@ -34,8 +34,8 @@ import java.util.logging.Logger; public final class GeyserPaperLogger extends GeyserSpigotLogger { private final ComponentLogger componentLogger; - public GeyserPaperLogger(Plugin plugin, Logger logger, boolean debug) { - super(logger, debug); + public GeyserPaperLogger(Plugin plugin, Logger logger) { + super(logger); componentLogger = plugin.getComponentLogger(); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java index 6a962f450..84a12b4ad 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java @@ -27,6 +27,8 @@ package org.geysermc.geyser.platform.spigot; import com.destroystokyo.paper.event.server.PaperServerListPingEvent; import com.destroystokyo.paper.network.StatusClient; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,6 +37,7 @@ import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.InetSocketAddress; /** @@ -42,7 +45,10 @@ import java.net.InetSocketAddress; * applied. */ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough { - private static final Constructor OLD_CONSTRUCTOR = ReflectedNames.getOldPaperPingConstructor(); + private static final Constructor EVENT_CONSTRUCTOR = ReflectedNames.paperServerListPingEventConstructor(); + // https://jd.papermc.io/paper/1.19.2/com/destroystokyo/paper/event/server/PaperServerListPingEvent.html + private static final boolean CHAT_PREVIEWS = EVENT_CONSTRUCTOR.getParameters()[2].getType() == boolean.class; + private static final Method MOTD_COMPONENT_GETTER = ReflectedNames.motdGetter(); private final GeyserSpigotLogger logger; @@ -55,18 +61,15 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough @Override public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) { try { - // We'd rather *not* use deprecations here, but unfortunately any Adventure class would be relocated at - // runtime because we still have to shade in our own Adventure class. For now. PaperServerListPingEvent event; - if (OLD_CONSTRUCTOR != null) { - // 1.19, removed in 1.19.4 - event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), - Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), - Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); + if (CHAT_PREVIEWS) { + event = EVENT_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), + MOTD_COMPONENT_GETTER.invoke(null), false, Bukkit.getOnlinePlayers().size(), + Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); } else { - event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress), - Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), - Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); + event = EVENT_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), + MOTD_COMPONENT_GETTER.invoke(null), Bukkit.getOnlinePlayers().size(), + Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); } Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) { @@ -81,7 +84,10 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough players = new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers()); } - return new GeyserPingInfo(event.getMotd(), players); + return new GeyserPingInfo( + GsonComponentSerializer.gson().serialize(LegacyComponentSerializer.legacySection().deserialize(event.getMotd())), + players + ); } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change logger.debug("Error while getting Paper ping passthrough: " + e); return null; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java index 2a6056df9..f0fa71ab2 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java @@ -96,19 +96,31 @@ public class GeyserSpigotCompressionDisabler extends ChannelOutboundHandlerAdapt private static Class findCompressionPacket() throws ClassNotFoundException { try { - return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression"); + // Mojmaps + return Class.forName("net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket"); } catch (ClassNotFoundException e) { - String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server"); - return Class.forName(prefix + ".PacketLoginOutSetCompression"); + try { + // Spigot mappings + return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression"); + } catch (ClassNotFoundException ex) { + String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server"); + return Class.forName(prefix + ".PacketLoginOutSetCompression"); + } } } private static Class findLoginSuccessPacket() throws ClassNotFoundException { try { - return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess"); + // Mojmaps + return Class.forName("net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket"); } catch (ClassNotFoundException e) { - String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server"); - return Class.forName(prefix + ".PacketLoginOutSuccess"); + try { + // Spigot mappings + return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess"); + } catch (ClassNotFoundException ex) { + String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server"); + return Class.forName(prefix + ".PacketLoginOutSuccess"); + } } } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java index ad31131bd..3098ef0e0 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java @@ -25,7 +25,8 @@ package org.geysermc.geyser.platform.spigot; -import com.github.steveice10.mc.protocol.MinecraftProtocol; +import org.geysermc.mcprotocollib.protocol.MinecraftConstants; +import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; import com.viaversion.viaversion.bukkit.handlers.BukkitChannelInitializer; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; @@ -119,8 +120,11 @@ public class GeyserSpigotInjector extends GeyserInjector { protected void initChannel(@NonNull Channel ch) throws Exception { initChannel.invoke(childHandler, ch); + int index = ch.pipeline().names().indexOf("encoder"); + String baseName = index != -1 ? "encoder" : "outbound_config"; + if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserSpigotCompressionDisabler.ENABLED) { - ch.pipeline().addAfter("encoder", "geyser-compression-disabler", new GeyserSpigotCompressionDisabler()); + ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserSpigotCompressionDisabler()); } } }) @@ -173,9 +177,9 @@ public class GeyserSpigotInjector extends GeyserInjector { */ private void workAroundWeirdBug(GeyserBootstrap bootstrap) { MinecraftProtocol protocol = new MinecraftProtocol(); - LocalSession session = new LocalSession(bootstrap.getGeyserConfig().getRemote().address(), - bootstrap.getGeyserConfig().getRemote().port(), this.serverSocketAddress, - InetAddress.getLoopbackAddress().getHostAddress(), protocol, protocol.createHelper()); + LocalSession session = new LocalSession(this.serverSocketAddress, InetAddress.getLoopbackAddress().getHostAddress(), protocol, Runnable::run); + session.setFlag(MinecraftConstants.CLIENT_HOST, bootstrap.getGeyserConfig().getRemote().address()); + session.setFlag(MinecraftConstants.CLIENT_PORT, bootstrap.getGeyserConfig().getRemote().port()); session.connect(); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java index fe56cba1c..231255fec 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.platform.spigot; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import org.geysermc.geyser.GeyserLogger; import java.util.logging.Level; import java.util.logging.Logger; -@AllArgsConstructor +@RequiredArgsConstructor public class GeyserSpigotLogger implements GeyserLogger { private final Logger logger; @Getter @Setter @@ -75,4 +75,11 @@ public class GeyserSpigotLogger implements GeyserLogger { info(message); } } + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + info(String.format(message, arguments)); + } + } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java index 4b1e42871..acd19ae18 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.platform.spigot; import lombok.AllArgsConstructor; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.server.ServerListPingEvent; @@ -52,7 +54,11 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough { try { ServerListPingEvent event = new GeyserPingEvent(inetSocketAddress.getAddress(), Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers()); Bukkit.getPluginManager().callEvent(event); - return new GeyserPingInfo(event.getMotd(), event.getMaxPlayers(), event.getNumPlayers()); + return new GeyserPingInfo( + GsonComponentSerializer.gson().serialize(LegacyComponentSerializer.legacySection().deserialize(event.getMotd())), + event.getMaxPlayers(), + event.getNumPlayers() + ); } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change logger.debug("Error while getting Bukkit ping passthrough: " + e); return null; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index 1f14a2f65..e3f52ee8e 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -30,36 +30,34 @@ import com.viaversion.viaversion.api.data.MappingData; import com.viaversion.viaversion.api.protocol.ProtocolPathEntry; import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import io.netty.buffer.ByteBuf; -import me.lucko.commodore.CommodoreProvider; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; -import org.bukkit.command.CommandMap; -import org.bukkit.command.PluginCommand; +import org.bukkit.command.CommandSender; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; -import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.adapters.paper.PaperAdapters; import org.geysermc.geyser.adapters.spigot.SpigotAdapters; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.spigot.command.GeyserBrigadierSupport; -import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandExecutor; -import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandManager; +import org.geysermc.geyser.platform.spigot.command.SpigotCommandRegistry; +import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource; import org.geysermc.geyser.platform.spigot.world.GeyserPistonListener; import org.geysermc.geyser.platform.spigot.world.GeyserSpigotBlockPlaceListener; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotLegacyNativeWorldManager; @@ -67,25 +65,25 @@ import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotNativeWorld import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.bukkit.BukkitCommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.paper.LegacyPaperCommandManager; import java.io.File; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.SocketAddress; import java.nio.file.Path; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.UUID; -import java.util.logging.Level; public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { - private GeyserSpigotCommandManager geyserCommandManager; + private CommandRegistry commandRegistry; private GeyserSpigotConfiguration geyserConfig; private GeyserSpigotInjector geyserInjector; - private GeyserSpigotLogger geyserLogger; + private final GeyserSpigotLogger geyserLogger = GeyserPaperLogger.supported() ? + new GeyserPaperLogger(this, getLogger()) : new GeyserSpigotLogger(getLogger()); private IGeyserPingPassthrough geyserSpigotPingPassthrough; private GeyserSpigotWorldManager geyserWorldManager; @@ -113,13 +111,12 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { // We depend on this as a fallback in certain scenarios BlockData.class.getMethod("getAsString"); } catch (ClassNotFoundException | NoSuchMethodException e) { - getLogger().severe("*********************************************"); - getLogger().severe(""); - getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.header")); - getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2")); - getLogger().severe(""); - getLogger().severe("*********************************************"); - Bukkit.getPluginManager().disablePlugin(this); + geyserLogger.error("*********************************************"); + geyserLogger.error(""); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.header")); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2")); + geyserLogger.error(""); + geyserLogger.error("*********************************************"); return; } @@ -127,13 +124,12 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { Class.forName("net.md_5.bungee.chat.ComponentSerializer"); } catch (ClassNotFoundException e) { if (!PaperAdventure.canSendMessageUsingComponent()) { // Prepare for Paper eventually removing Bungee chat - getLogger().severe("*********************************************"); - getLogger().severe(""); - getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.header", getServer().getName())); - getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper")); - getLogger().severe(""); - getLogger().severe("*********************************************"); - Bukkit.getPluginManager().disablePlugin(this); + geyserLogger.error("*********************************************"); + geyserLogger.error(""); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.header", getServer().getName())); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper")); + geyserLogger.error(""); + geyserLogger.error("*********************************************"); return; } } @@ -141,20 +137,34 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { try { Class.forName("io.netty.util.internal.ObjectPool$ObjectCreator"); } catch (ClassNotFoundException e) { - getLogger().severe("*********************************************"); - getLogger().severe(""); - getLogger().severe("This version of Spigot is using an outdated version of netty. Please use Paper instead!"); - getLogger().severe(""); - getLogger().severe("*********************************************"); - Bukkit.getPluginManager().disablePlugin(this); + geyserLogger.error("*********************************************"); + geyserLogger.error(""); + geyserLogger.error("This version of Spigot is using an outdated version of netty. Please use Paper instead!"); + geyserLogger.error(""); + geyserLogger.error("*********************************************"); return; } + try { + // Check spigot config for BungeeCord mode + if (Bukkit.getServer().spigot().getConfig().getBoolean("settings.bungeecord")) { + warnInvalidProxySetups("BungeeCord"); + return; + } + + // Now: Check for velocity mode - deliberately after checking bungeecord because this is a paper only option + if (Bukkit.getServer().spigot().getPaperConfig().getBoolean("proxies.velocity.enabled")) { + warnInvalidProxySetups("Velocity"); + return; + } + } catch (NoSuchMethodError e) { + // no-op + } + if (!loadConfig()) { return; } - this.geyserLogger = GeyserPaperLogger.supported() ? new GeyserPaperLogger(this, getLogger(), geyserConfig.isDebugMode()) - : new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); + this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); // Turn "(MC: 1.16.4)" into 1.16.4. @@ -165,31 +175,43 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { @Override public void onEnable() { - this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); - this.geyserCommandManager.init(); - - // Because Bukkit locks its command map upon startup, we need to - // add our plugin commands in onEnable, but populating the executor - // can happen at any time (later in #onGeyserEnable()) - CommandMap commandMap = GeyserSpigotCommandManager.getCommandMap(); - for (Extension extension : this.geyserCommandManager.extensionCommands().keySet()) { - // Thanks again, Bukkit - try { - Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); - constructor.setAccessible(true); - - PluginCommand pluginCommand = constructor.newInstance(extension.description().id(), this); - pluginCommand.setDescription("The main command for the " + extension.name() + " Geyser extension!"); - - commandMap.register(extension.description().id(), "geyserext", pluginCommand); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { - this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex); - } + // Disabling the plugin in onLoad() is not supported; we need to manually stop here and disable ourselves + if (geyser == null) { + Bukkit.getPluginManager().disablePlugin(this); + return; } + // Register commands after Geyser initialization, but before the server starts. + var sourceConverter = new CommandSourceConverter<>( + CommandSender.class, + Bukkit::getPlayer, + Bukkit::getConsoleSender, + SpigotCommandSource::new + ); + LegacyPaperCommandManager cloud; + try { + // LegacyPaperCommandManager works for spigot too, see https://cloud.incendo.org/minecraft/paper + cloud = new LegacyPaperCommandManager<>( + this, + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + + try { + // Commodore brigadier on Spigot/Paper 1.13 - 1.18.2 + // Paper-only brigadier on 1.19+ + cloud.registerBrigadier(); + } catch (BukkitCommandManager.BrigadierInitializationException e) { + geyserLogger.debug("Failed to initialize Brigadier support: " + e.getMessage()); + } + + this.commandRegistry = new SpigotCommandRegistry(geyser, cloud); + // Needs to be an anonymous inner class otherwise Bukkit complains about missing classes Bukkit.getPluginManager().registerEvents(new Listener() { - @EventHandler public void onServerLoaded(ServerLoadEvent event) { if (event.getType() == ServerLoadEvent.LoadType.RELOAD) { @@ -227,7 +249,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { } geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); - // Don't need to re-create the world manager/re-register commands/reinject when reloading + // Don't need to re-create the world manager/reinject when reloading if (GeyserImpl.getInstance().isReloading()) { return; } @@ -244,17 +266,29 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { if (Boolean.parseBoolean(System.getProperty("Geyser.UseDirectAdapters", "true"))) { try { - String name = Bukkit.getServer().getClass().getPackage().getName(); - String nmsVersion = name.substring(name.lastIndexOf('.') + 1); - SpigotAdapters.registerWorldAdapter(nmsVersion); + boolean isPaper = false; + try { + String name = Bukkit.getServer().getClass().getPackage().getName(); + String nmsVersion = name.substring(name.lastIndexOf('.') + 1); + SpigotAdapters.registerWorldAdapter(nmsVersion); + geyserLogger.debug("Using spigot NMS adapter for nms version: " + nmsVersion); + } catch (Exception e) { // Likely running on Paper 1.20.5+ + geyserLogger.debug("Unable to find spigot world manager: " + e.getMessage()); + //noinspection deprecation + int protocolVersion = Bukkit.getUnsafe().getProtocolVersion(); + PaperAdapters.registerClosestWorldAdapter(protocolVersion); + isPaper = true; + geyserLogger.debug("Using paper world adapter for protocol version: " + protocolVersion); + } + if (isViaVersion && isViaVersionNeeded()) { - this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this); + this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this, isPaper); } else { // No ViaVersion - this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this); + this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this, isPaper); } - geyserLogger.debug("Using NMS adapter: " + this.geyserWorldManager.getClass() + ", " + nmsVersion); - } catch (Exception e) { + geyserLogger.debug("Using world manager of type: " + this.geyserWorldManager.getClass().getSimpleName()); + } catch (Throwable e) { if (geyserConfig.isDebugMode()) { geyserLogger.debug("Error while attempting to find NMS adapter. Most likely, this can be safely ignored. :)"); e.printStackTrace(); @@ -270,79 +304,40 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.debug("Using default world manager."); } - PluginCommand geyserCommand = this.getCommand("geyser"); - Objects.requireNonNull(geyserCommand, "base command cannot be null"); - geyserCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands())); - - for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { - Map commands = entry.getValue(); - if (commands.isEmpty()) { - continue; - } - - PluginCommand command = this.getCommand(entry.getKey().description().id()); - if (command == null) { - continue; - } - - command.setExecutor(new GeyserSpigotCommandExecutor(this.geyser, commands)); - } - // Register permissions so they appear in, for example, LuckPerms' UI - // Re-registering permissions throws an error - for (Map.Entry entry : geyserCommandManager.commands().entrySet()) { - Command command = entry.getValue(); - if (command.aliases().contains(entry.getKey())) { - // Don't register aliases - continue; + // Re-registering permissions without removing it throws an error + PluginManager pluginManager = Bukkit.getPluginManager(); + geyser.eventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> { + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(def, "permission default for " + permission); + + if (permission.isBlank()) { + return; + } + PermissionDefault permissionDefault = switch (def) { + case TRUE -> PermissionDefault.TRUE; + case FALSE -> PermissionDefault.FALSE; + case NOT_SET -> PermissionDefault.OP; + }; + + Permission existingPermission = pluginManager.getPermission(permission); + if (existingPermission != null) { + geyserLogger.debug("permission " + permission + " with default " + + existingPermission.getDefault() + " is being overridden by " + permissionDefault); + + pluginManager.removePermission(permission); } - Bukkit.getPluginManager().addPermission(new Permission(command.permission(), - GeyserLocale.getLocaleStringLog(command.description()), - command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); - } - - // Register permissions for extension commands - for (Map.Entry> commandEntry : this.geyserCommandManager.extensionCommands().entrySet()) { - for (Map.Entry entry : commandEntry.getValue().entrySet()) { - Command command = entry.getValue(); - if (command.aliases().contains(entry.getKey())) { - // Don't register aliases - continue; - } - - if (command.permission().isBlank()) { - continue; - } - - // Avoid registering the same permission twice, e.g. for the extension help commands - if (Bukkit.getPluginManager().getPermission(command.permission()) != null) { - GeyserImpl.getInstance().getLogger().debug("Skipping permission " + command.permission() + " as it is already registered"); - continue; - } - - Bukkit.getPluginManager().addPermission(new Permission(command.permission(), - GeyserLocale.getLocaleStringLog(command.description()), - command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); - } - } - - Bukkit.getPluginManager().addPermission(new Permission(Constants.UPDATE_PERMISSION, - "Whether update notifications can be seen", PermissionDefault.OP)); + pluginManager.addPermission(new Permission(permission, permissionDefault)); + }); // Events cannot be unregistered - re-registering results in duplicate firings GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager); - Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); + pluginManager.registerEvents(blockPlaceListener, this); - Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); + pluginManager.registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); - Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigotUpdateListener(), this); - - boolean brigadierSupported = CommodoreProvider.isSupported(); - geyserLogger.debug("Brigadier supported? " + brigadierSupported); - if (brigadierSupported) { - GeyserBrigadierSupport.loadBrigadier(this, geyserCommand); - } + pluginManager.registerEvents(new GeyserSpigotUpdateListener(), this); } @Override @@ -378,8 +373,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override @@ -407,6 +402,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return this.minecraftVersion; } + @Override + public @NonNull String getServerPlatform() { + return Bukkit.getName(); + } + @Override public SocketAddress getSocketAddress() { return this.geyserInjector.getServerSocketAddress(); @@ -474,7 +474,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); this.geyserConfig = FileUtils.loadConfig(configFile, GeyserSpigotConfiguration.class); } catch (IOException ex) { - getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); Bukkit.getPluginManager().disablePlugin(this); return false; @@ -482,4 +482,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return true; } + + private void warnInvalidProxySetups(String platform) { + geyserLogger.error("*********************************************"); + geyserLogger.error(""); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy_backend", platform)); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.setup_guide", "https://geysermc.org/wiki/geyser/setup/")); + geyserLogger.error(""); + geyserLogger.error("*********************************************"); + } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java index 5e3c4def8..8a8a43460 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java @@ -29,8 +29,8 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; @@ -40,7 +40,7 @@ public final class GeyserSpigotUpdateListener implements Listener { public void onPlayerJoin(final PlayerJoinEvent event) { if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { final Player player = event.getPlayer(); - if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new SpigotCommandSource(player)); } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java index 9e0b14b11..fa7555ac6 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.platform.spigot; -import com.github.steveice10.mc.protocol.data.DefaultComponentSerializer; +import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer; import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java index 275fec657..5429850de 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java @@ -26,12 +26,12 @@ package org.geysermc.geyser.platform.spigot; import com.destroystokyo.paper.event.server.PaperServerListPingEvent; -import com.destroystokyo.paper.network.StatusClient; +import org.bukkit.Bukkit; import org.bukkit.event.server.ServerListPingEvent; -import org.bukkit.util.CachedServerIcon; import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.InetAddress; /** @@ -42,8 +42,9 @@ public final class ReflectedNames { static boolean checkPaperPingEvent() { try { Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent"); + paperServerListPingEventConstructor(); return true; - } catch (ClassNotFoundException e) { + } catch (Throwable ignored) { return false; } } @@ -52,18 +53,27 @@ public final class ReflectedNames { return getConstructor(ServerListPingEvent.class, InetAddress.class, String.class, boolean.class, int.class, int.class) != null; } - static @Nullable Constructor getOldPaperPingConstructor() { - if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class, - int.class, String.class, int.class, CachedServerIcon.class) != null) { - // @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers, - // @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon - // New constructor is present - return null; + // Ugly workaround that's necessary due to relocation of adventure components + static Method motdGetter() { + try { + return Bukkit.class.getMethod("motd"); + } catch (Throwable e) { + throw new RuntimeException("Could not find component motd method! Please report this issue.", e); } - // @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers, - // @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon - return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class, int.class, - String.class, int.class, CachedServerIcon.class); + } + + @SuppressWarnings("unchecked") + static Constructor paperServerListPingEventConstructor() { + var constructors = PaperServerListPingEvent.class.getConstructors(); + for (var constructor : constructors) { + // We want to get the constructor with the adventure component motd, but without referencing the + // component class as that's relocated + if (constructor.getParameters()[1].getType() != String.class) { + return (Constructor) constructor; + } + } + + throw new IllegalStateException("Could not find component motd method!"); } /** diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java deleted file mode 100644 index 61900174c..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.spigot.command; - -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import me.lucko.commodore.Commodore; -import me.lucko.commodore.CommodoreProvider; -import org.bukkit.Bukkit; -import org.bukkit.command.PluginCommand; -import org.geysermc.geyser.platform.spigot.GeyserSpigotPlugin; - -/** - * Needs to be a separate class so pre-1.13 loads correctly. - */ -public final class GeyserBrigadierSupport { - - public static void loadBrigadier(GeyserSpigotPlugin plugin, PluginCommand pluginCommand) { - // Enable command completions if supported - // This is beneficial because this is sent over the network and Bedrock can see it - Commodore commodore = CommodoreProvider.getCommodore(plugin); - LiteralArgumentBuilder builder = LiteralArgumentBuilder.literal("geyser"); - for (String command : plugin.getGeyserCommandManager().getCommands().keySet()) { - builder.then(LiteralArgumentBuilder.literal(command)); - } - commodore.register(pluginCommand, builder); - - try { - Class.forName("com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent"); - Bukkit.getServer().getPluginManager().registerEvents(new GeyserPaperCommandListener(), plugin); - plugin.getGeyserLogger().debug("Successfully registered AsyncPlayerSendCommandsEvent listener."); - } catch (ClassNotFoundException e) { - plugin.getGeyserLogger().debug("Not registering AsyncPlayerSendCommandsEvent listener."); - } - } - - private GeyserBrigadierSupport() { - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java deleted file mode 100644 index dcec045ab..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.spigot.command; - -import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; -import com.mojang.brigadier.tree.CommandNode; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; - -import java.net.InetSocketAddress; -import java.util.Iterator; -import java.util.Map; - -public final class GeyserPaperCommandListener implements Listener { - - @SuppressWarnings("UnstableApiUsage") - @EventHandler - public void onCommandSend(AsyncPlayerSendCommandsEvent event) { - // Documentation says to check (event.isAsynchronous() || !event.hasFiredAsync()), but as of Paper 1.18.2 - // event.hasFiredAsync is never true - if (event.isAsynchronous()) { - CommandNode geyserBrigadier = event.getCommandNode().getChild("geyser"); - if (geyserBrigadier != null) { - Player player = event.getPlayer(); - boolean isJavaPlayer = isProbablyJavaPlayer(player); - Map commands = GeyserImpl.getInstance().commandManager().getCommands(); - Iterator> it = geyserBrigadier.getChildren().iterator(); - - while (it.hasNext()) { - CommandNode subnode = it.next(); - Command command = commands.get(subnode.getName()); - if (command != null) { - if ((command.isBedrockOnly() && isJavaPlayer) || !player.hasPermission(command.permission())) { - // Remove this from the node as we don't have permission to use it - it.remove(); - } - } - } - } - } - } - - /** - * This early on, there is a rare chance that Geyser has yet to process the connection. We'll try to minimize that - * chance, though. - */ - private boolean isProbablyJavaPlayer(Player player) { - if (GeyserImpl.getInstance().connectionByUuid(player.getUniqueId()) != null) { - // For sure this is a Bedrock player - return false; - } - - if (GeyserImpl.getInstance().getConfig().isUseDirectConnection()) { - InetSocketAddress address = player.getAddress(); - if (address != null) { - return address.getPort() != 0; - } - } - return true; - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java deleted file mode 100644 index 6780bde17..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.spigot.command; - -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class GeyserSpigotCommandExecutor extends GeyserCommandExecutor implements TabExecutor { - - public GeyserSpigotCommandExecutor(GeyserImpl geyser, Map commands) { - super(geyser, commands); - } - - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String[] args) { - SpigotCommandSource commandSender = new SpigotCommandSource(sender); - GeyserSession session = getGeyserSession(commandSender); - - if (args.length > 0) { - GeyserCommand geyserCommand = getCommand(args[0]); - if (geyserCommand != null) { - if (!sender.hasPermission(geyserCommand.permission())) { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.locale()); - - commandSender.sendMessage(ChatColor.RED + message); - return true; - } - if (geyserCommand.isBedrockOnly() && session == null) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.locale())); - return true; - } - geyserCommand.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); - return true; - } else { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", commandSender.locale()); - commandSender.sendMessage(ChatColor.RED + message); - } - } else { - getCommand("help").execute(session, commandSender, new String[0]); - return true; - } - return true; - } - - @Override - public List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String[] args) { - if (args.length == 1) { - return tabComplete(new SpigotCommandSource(sender)); - } - return Collections.emptyList(); - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java similarity index 61% rename from bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java rename to bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java index 655d3be23..39496d2c6 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,16 +29,21 @@ import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.command.Command; import org.bukkit.command.CommandMap; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.incendo.cloud.CommandManager; import java.lang.reflect.Field; -public class GeyserSpigotCommandManager extends GeyserCommandManager { +public class SpigotCommandRegistry extends CommandRegistry { - private static final CommandMap COMMAND_MAP; + private final CommandMap commandMap; + + public SpigotCommandRegistry(GeyserImpl geyser, CommandManager cloud) { + super(geyser, cloud); - static { CommandMap commandMap = null; try { // Paper-only @@ -49,24 +54,28 @@ public class GeyserSpigotCommandManager extends GeyserCommandManager { Field cmdMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); cmdMapField.setAccessible(true); commandMap = (CommandMap) cmdMapField.get(Bukkit.getServer()); - } catch (NoSuchFieldException | IllegalAccessException ex) { - ex.printStackTrace(); + } catch (Exception ex) { + geyser.getLogger().error("Failed to get Spigot's CommandMap", ex); } } - COMMAND_MAP = commandMap; - } - - public GeyserSpigotCommandManager(GeyserImpl geyser) { - super(geyser); + this.commandMap = commandMap; } + @NonNull @Override - public String description(String command) { - Command cmd = COMMAND_MAP.getCommand(command.replace("/", "")); - return cmd != null ? cmd.getDescription() : ""; - } + public String description(@NonNull String command, @NonNull String locale) { + // check if the command is /geyser or an extension command so that we can localize the description + String description = super.description(command, locale); + if (!description.isBlank()) { + return description; + } - public static CommandMap getCommandMap() { - return COMMAND_MAP; + if (commandMap != null) { + Command cmd = commandMap.getCommand(command); + if (cmd != null) { + return cmd.getDescription(); + } + } + return ""; } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java index 365e9ad17..9f897b095 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java @@ -27,17 +27,22 @@ package org.geysermc.geyser.platform.spigot.command; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.platform.spigot.PaperAdventure; import org.geysermc.geyser.text.GeyserLocale; -public class SpigotCommandSource implements GeyserCommandSource { - private final org.bukkit.command.CommandSender handle; +import java.util.UUID; - public SpigotCommandSource(org.bukkit.command.CommandSender handle) { +public class SpigotCommandSource implements GeyserCommandSource { + private final CommandSender handle; + + public SpigotCommandSource(CommandSender handle) { this.handle = handle; // Ensure even Java players' languages are loaded GeyserLocale.loadGeyserLocale(locale()); @@ -65,9 +70,22 @@ public class SpigotCommandSource implements GeyserCommandSource { handle.spigot().sendMessage(BungeeComponentSerializer.get().serialize(message)); } + @Override + public Object handle() { + return handle; + } + @Override public boolean isConsole() { - return handle instanceof ConsoleCommandSender; + return handle instanceof ConsoleCommandSender || handle instanceof RemoteConsoleCommandSender; + } + + @Override + public @Nullable UUID playerUuid() { + if (handle instanceof Player player) { + return player.getUniqueId(); + } + return null; } @SuppressWarnings("deprecation") @@ -83,6 +101,7 @@ public class SpigotCommandSource implements GeyserCommandSource { @Override public boolean hasPermission(String permission) { - return handle.hasPermission(permission); + // Don't trust Spigot to handle blank permissions + return permission.isBlank() || handle.hasPermission(permission); } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java index 01b0be9f2..963f5bac3 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java @@ -25,10 +25,8 @@ package org.geysermc.geyser.platform.spigot.world; -import com.github.steveice10.mc.protocol.data.game.level.block.value.PistonValueType; -import org.cloudburstmc.math.vector.Vector3i; -import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; @@ -40,13 +38,17 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.BlockPistonEvent; import org.bukkit.event.block.BlockPistonExtendEvent; import org.bukkit.event.block.BlockPistonRetractEvent; +import org.cloudburstmc.math.vector.Vector3i; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PistonCache; import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValueType; import java.util.List; import java.util.Map; @@ -85,7 +87,7 @@ public class GeyserPistonListener implements Listener { PistonValueType type = isExtend ? PistonValueType.PUSHING : PistonValueType.PULLING; boolean sticky = event.isSticky(); - Object2IntMap attachedBlocks = new Object2IntArrayMap<>(); + Object2ObjectMap attachedBlocks = new Object2ObjectArrayMap<>(); boolean blocksFilled = false; for (Map.Entry entry : geyser.getSessionManager().getSessions().entrySet()) { @@ -108,10 +110,10 @@ public class GeyserPistonListener implements Listener { List blocks = isExtend ? ((BlockPistonExtendEvent) event).getBlocks() : ((BlockPistonRetractEvent) event).getBlocks(); for (Block block : blocks) { Location attachedLocation = block.getLocation(); - int blockId = worldManager.getBlockNetworkId(block); + BlockState state = BlockState.of(worldManager.getBlockNetworkId(block)); // Ignore blocks that will be destroyed - if (BlockStateValues.canPistonMoveBlock(blockId, isExtend)) { - attachedBlocks.put(getVector(attachedLocation), blockId); + if (BlockStateValues.canPistonMoveBlock(state, isExtend)) { + attachedBlocks.put(getVector(attachedLocation), state); } } blocksFilled = true; @@ -119,7 +121,7 @@ public class GeyserPistonListener implements Listener { int pistonBlockId = worldManager.getBlockNetworkId(event.getBlock()); // event.getDirection() is unreliable - Direction orientation = BlockStateValues.getPistonOrientation(pistonBlockId); + Direction orientation = BlockState.of(pistonBlockId).getValue(Properties.FACING); session.executeInEventLoop(() -> { PistonCache pistonCache = session.getPistonCache(); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java index 71aba11f9..7a4caabd7 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java @@ -33,7 +33,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockPlaceEvent; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; @@ -59,11 +59,11 @@ public class GeyserSpigotBlockPlaceListener implements Listener { event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ()))); } else { String javaBlockId = event.getBlockPlaced().getBlockData().getAsString(); - placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getOrDefault(javaBlockId, BlockStateValues.JAVA_AIR_ID))); + placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.get().getOrDefault(javaBlockId, Block.JAVA_AIR_ID))); } placeBlockSoundPacket.setIdentifier(":"); session.sendUpstreamPacket(placeBlockSoundPacket); session.setLastBlockPlacePosition(null); - session.setLastBlockPlacedId(null); + session.setLastBlockPlaced(null); } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java index 021db5ec1..fe2dda053 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java @@ -46,8 +46,8 @@ public class GeyserSpigotLegacyNativeWorldManager extends GeyserSpigotNativeWorl private final Int2IntMap oldToNewBlockId; - public GeyserSpigotLegacyNativeWorldManager(GeyserSpigotPlugin plugin) { - super(plugin); + public GeyserSpigotLegacyNativeWorldManager(GeyserSpigotPlugin plugin, boolean isPaper) { + super(plugin, isPaper); IntList allBlockStates = adapter.getAllBlockStates(); oldToNewBlockId = new Int2IntOpenHashMap(allBlockStates.size()); ProtocolVersion serverVersion = plugin.getServerProtocolVersion(); @@ -58,7 +58,7 @@ public class GeyserSpigotLegacyNativeWorldManager extends GeyserSpigotNativeWorl int newBlockId = oldBlockId; // protocolList should *not* be null; we checked for that before initializing this class for (int i = protocolList.size() - 1; i >= 0; i--) { - MappingData mappingData = protocolList.get(i).getProtocol().getMappingData(); + MappingData mappingData = protocolList.get(i).protocol().getMappingData(); if (mappingData != null) { newBlockId = mappingData.getNewBlockStateId(newBlockId); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java index 00212663c..96ae41933 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java @@ -26,27 +26,33 @@ package org.geysermc.geyser.platform.spigot.world.manager; import org.bukkit.Bukkit; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.adapters.WorldAdapter; +import org.geysermc.geyser.adapters.paper.PaperAdapters; import org.geysermc.geyser.adapters.spigot.SpigotAdapters; -import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter; -import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; public class GeyserSpigotNativeWorldManager extends GeyserSpigotWorldManager { - protected final SpigotWorldAdapter adapter; + protected final WorldAdapter adapter; - public GeyserSpigotNativeWorldManager(Plugin plugin) { + public GeyserSpigotNativeWorldManager(Plugin plugin, boolean isPaper) { super(plugin); - adapter = SpigotAdapters.getWorldAdapter(); + if (isPaper) { + adapter = PaperAdapters.getWorldAdapter(); + } else { + adapter = SpigotAdapters.getWorldAdapter(); + } } @Override public int getBlockAt(GeyserSession session, int x, int y, int z) { Player player = Bukkit.getPlayer(session.getPlayerEntity().getUsername()); if (player == null) { - return BlockStateValues.JAVA_AIR_ID; + return Block.JAVA_AIR_ID; } return adapter.getBlockAt(player.getWorld(), x, y, z); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java index 42f0d17f4..ea36dd1e7 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java @@ -25,56 +25,47 @@ package org.geysermc.geyser.platform.spigot.world.manager; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import org.bukkit.Bukkit; -import org.bukkit.Chunk; import org.bukkit.World; import org.bukkit.block.Block; +import org.bukkit.block.DecoratedPot; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.cloudburstmc.nbt.NbtMap; -import org.geysermc.erosion.bukkit.BukkitLecterns; +import org.cloudburstmc.math.vector.Vector3i; import org.geysermc.erosion.bukkit.BukkitUtils; -import org.geysermc.erosion.bukkit.PickBlockUtils; import org.geysermc.erosion.bukkit.SchedulerUtils; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.level.GameRule; import org.geysermc.geyser.level.WorldManager; -import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.util.BlockEntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; /** * The base world manager to use when there is no supported NMS revision */ public class GeyserSpigotWorldManager extends WorldManager { private final Plugin plugin; - private final BukkitLecterns lecterns; public GeyserSpigotWorldManager(Plugin plugin) { this.plugin = plugin; - this.lecterns = new BukkitLecterns(plugin); } @Override public int getBlockAt(GeyserSession session, int x, int y, int z) { Player bukkitPlayer; if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) { - return BlockStateValues.JAVA_AIR_ID; + return org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID; } World world = bukkitPlayer.getWorld(); if (!world.isChunkLoaded(x >> 4, z >> 4)) { // If the chunk isn't loaded, how could we even be here? - return BlockStateValues.JAVA_AIR_ID; + return org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID; } return getBlockNetworkId(world.getBlockAt(x, y, z)); @@ -85,9 +76,9 @@ public class GeyserSpigotWorldManager extends WorldManager { // Terrible behavior, but this is basically what's always been happening behind the scenes anyway. CompletableFuture blockData = new CompletableFuture<>(); Bukkit.getRegionScheduler().execute(this.plugin, block.getLocation(), () -> blockData.complete(block.getBlockData().getAsString())); - return BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(blockData.join(), BlockStateValues.JAVA_AIR_ID); + return BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.getOrDefault(blockData.join(), org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID); } - return BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(block.getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID); + return BlockRegistries.JAVA_BLOCK_STATE_IDENTIFIER_TO_ID.getOrDefault(block.getBlockData().getAsString(), org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID); // TODO could just make this a BlockState lookup? } @Override @@ -95,69 +86,6 @@ public class GeyserSpigotWorldManager extends WorldManager { return true; } - @Override - public void sendLecternData(GeyserSession session, int x, int y, int z) { - Player bukkitPlayer; - if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) { - return; - } - - Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z); - // Run as a task to prevent async issues - SchedulerUtils.runTask(this.plugin, () -> sendLecternData(session, block, false), block); - } - - public void sendLecternData(GeyserSession session, int x, int z, List blockEntityInfos) { - Player bukkitPlayer; - if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) { - return; - } - if (SchedulerUtils.FOLIA) { - Chunk chunk = getChunk(bukkitPlayer.getWorld(), x, z); - if (chunk == null) { - return; - } - Bukkit.getRegionScheduler().execute(this.plugin, bukkitPlayer.getWorld(), x, z, () -> - sendLecternData(session, chunk, blockEntityInfos)); - } else { - Bukkit.getScheduler().runTask(this.plugin, () -> { - Chunk chunk = getChunk(bukkitPlayer.getWorld(), x, z); - if (chunk == null) { - return; - } - sendLecternData(session, chunk, blockEntityInfos); - }); - } - } - - private @Nullable Chunk getChunk(World world, int x, int z) { - if (!world.isChunkLoaded(x, z)) { - return null; - } - return world.getChunkAt(x, z); - } - - private void sendLecternData(GeyserSession session, Chunk chunk, List blockEntityInfos) { - //noinspection ForLoopReplaceableByForEach - avoid constructing Iterator - for (int i = 0; i < blockEntityInfos.size(); i++) { - BlockEntityInfo info = blockEntityInfos.get(i); - Block block = chunk.getBlock(info.getX(), info.getY(), info.getZ()); - sendLecternData(session, block, true); - } - } - - private void sendLecternData(GeyserSession session, Block block, boolean isChunkLoad) { - NbtMap blockEntityTag = this.lecterns.getLecternData(block, isChunkLoad); - if (blockEntityTag != null) { - BlockEntityUtils.updateBlockEntity(session, blockEntityTag, BukkitUtils.getVector(block.getLocation())); - } - } - - @Override - public boolean shouldExpectLecternHandled(GeyserSession session) { - return true; - } - public boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { org.bukkit.GameRule bukkitGameRule = org.bukkit.GameRule.getByName(gameRule.getJavaID()); if (bukkitGameRule == null) { @@ -195,28 +123,19 @@ public class GeyserSpigotWorldManager extends WorldManager { return GameMode.byId(Bukkit.getDefaultGameMode().ordinal()); } - @Override - public boolean hasPermission(GeyserSession session, String permission) { - Player player = Bukkit.getPlayer(session.javaUuid()); - if (player != null) { - return player.hasPermission(permission); - } - return false; - } - - @Override - public @NonNull CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) { - CompletableFuture<@Nullable CompoundTag> future = new CompletableFuture<>(); + public void getDecoratedPotData(GeyserSession session, Vector3i pos, Consumer> apply) { Player bukkitPlayer; if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) { - future.complete(null); - return future; + return; } - Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z); - // Paper 1.19.3 complains about async access otherwise. - // java.lang.IllegalStateException: Tile is null, asynchronous access? - SchedulerUtils.runTask(this.plugin, () -> future.complete(PickBlockUtils.pickBlock(block)), block); - return future; + Block block = bukkitPlayer.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); + SchedulerUtils.runTask(this.plugin, () -> { + var state = BukkitUtils.getBlockState(block); + if (!(state instanceof DecoratedPot pot)) { + return; + } + apply.accept(pot.getShards().stream().map(material -> material.getKey().toString()).toList()); + }, block); } /** diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml index 6e81ccdb6..14e98f577 100644 --- a/bootstrap/spigot/src/main/resources/plugin.yml +++ b/bootstrap/spigot/src/main/resources/plugin.yml @@ -6,11 +6,3 @@ version: ${version} softdepend: ["ViaVersion", "floodgate"] api-version: 1.13 folia-supported: true -commands: - geyser: - description: The main command for Geyser. - usage: /geyser - permission: geyser.command -permissions: - geyser.command: - default: true diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts index eaf895108..6ec437654 100644 --- a/bootstrap/standalone/build.gradle.kts +++ b/bootstrap/standalone/build.gradle.kts @@ -1,7 +1,9 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer -val terminalConsoleVersion = "1.2.0" -val jlineVersion = "3.21.0" +plugins { + application + id("geyser.platform-conventions") +} dependencies { api(projects.core) @@ -12,7 +14,6 @@ dependencies { } implementation(libs.bundles.jline) - implementation(libs.bundles.log4j) } @@ -38,4 +39,6 @@ tasks.named("run") { dir.mkdirs() jvmArgs("-Dio.netty.leakDetection.level=PARANOID") workingDir = dir + + standardInput = System.`in` } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index 039004867..ea68d354c 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -32,17 +32,15 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedField; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.netty.util.ResourceLeakDetector; import lombok.Getter; -import net.minecrell.terminalconsole.TerminalConsoleAppender; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Logger; -import org.apache.logging.log4j.core.appender.ConsoleAppender; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; @@ -69,9 +67,10 @@ import java.util.stream.Collectors; public class GeyserStandaloneBootstrap implements GeyserBootstrap { - private GeyserCommandManager geyserCommandManager; + private StandaloneCloudCommandManager cloud; + private CommandRegistry commandRegistry; private GeyserStandaloneConfiguration geyserConfig; - private GeyserStandaloneLogger geyserLogger; + private final GeyserStandaloneLogger geyserLogger = new GeyserStandaloneLogger(); private IGeyserPingPassthrough geyserPingPassthrough; private GeyserStandaloneGUI gui; @Getter @@ -90,6 +89,14 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); // Can eat performance } + // Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator + if (System.getProperty("io.netty.allocator.type") == null) { + System.setProperty("io.netty.allocator.type", "pooled"); + } + + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + GeyserStandaloneLogger.setupStreams(); + GeyserStandaloneBootstrap bootstrap = new GeyserStandaloneBootstrap(); // Set defaults boolean useGuiOpts = bootstrap.useGui; @@ -173,19 +180,10 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { @Override public void onGeyserInitialize() { log4jLogger = (Logger) LogManager.getRootLogger(); - for (Appender appender : log4jLogger.getAppenders().values()) { - // Remove the appender that is not in use - // Prevents multiple appenders/double logging and removes harmless errors - if ((useGui && appender instanceof TerminalConsoleAppender) || (!useGui && appender instanceof ConsoleAppender)) { - log4jLogger.removeAppender(appender); - } - } - - this.geyserLogger = new GeyserStandaloneLogger(); if (useGui && gui == null) { gui = new GeyserStandaloneGUI(geyserLogger); - gui.redirectSystemStreams(); + gui.addGuiAppender(); gui.startUpdateThread(); } @@ -198,7 +196,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { public void onGeyserEnable() { try { File configFile = FileUtils.fileOrCopiedFromResource(new File(configFilename), "config.yml", - (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); + (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); geyserConfig = FileUtils.loadConfig(configFile, GeyserStandaloneConfiguration.class); handleArgsConfigOptions(); @@ -224,20 +222,32 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { geyser = GeyserImpl.load(PlatformType.STANDALONE, this); - geyserCommandManager = new GeyserCommandManager(geyser); - geyserCommandManager.init(); + boolean reloading = geyser.isReloading(); + if (!reloading) { + // Currently there would be no significant benefit of re-initializing commands. Also, we would have to unsubscribe CommandRegistry. + // Fire GeyserDefineCommandsEvent after PreInitEvent, before PostInitEvent, for consistency with other bootstraps. + cloud = new StandaloneCloudCommandManager(geyser); + commandRegistry = new CommandRegistry(geyser, cloud); + } GeyserImpl.start(); + if (!reloading) { + // Event must be fired after CommandRegistry has subscribed its listener. + // Also, the subscription for the Permissions class is created when Geyser is initialized. + cloud.fireRegisterPermissionsEvent(); + } else { + // This isn't ideal - but geyserLogger#start won't ever finish, leading to a reloading deadlock + geyser.setReloading(false); + } + if (gui != null) { - gui.enableCommands(geyser.getScheduledThread(), geyserCommandManager); + gui.enableCommands(geyser.getScheduledThread(), commandRegistry); } geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); - if (!useGui) { - geyserLogger.start(); // Throws an error otherwise - } + geyserLogger.start(); } /** @@ -250,15 +260,14 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { Class graphicsEnv = Class.forName("java.awt.GraphicsEnvironment"); Method isHeadless = graphicsEnv.getDeclaredMethod("isHeadless"); return (boolean) isHeadless.invoke(null); - } catch (Exception ignore) { } + } catch (Exception ignore) { + } return true; } @Override public void onGeyserDisable() { - // We can re-register commands on standalone, so why not - GeyserImpl.getInstance().commandManager().getCommands().clear(); geyser.disable(); } @@ -279,8 +288,8 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return commandRegistry; } @Override @@ -305,6 +314,11 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { return new GeyserStandaloneDumpInfo(this); } + @Override + public @NonNull String getServerPlatform() { + return PlatformType.STANDALONE.platformName(); + } + @NonNull @Override public String getServerBindAddress() { @@ -338,12 +352,12 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { // Get the ignored properties Set ignoredProperties = OBJECT_MAPPER.getSerializationConfig().getAnnotationIntrospector() - .findPropertyIgnoralByName(OBJECT_MAPPER.getSerializationConfig() ,beanDescription.getClassInfo()).getIgnored(); + .findPropertyIgnoralByName(OBJECT_MAPPER.getSerializationConfig(), beanDescription.getClassInfo()).getIgnored(); // Filter properties removing the ignored ones return properties.stream() - .filter(property -> !ignoredProperties.contains(property.getName())) - .collect(Collectors.toList()); + .filter(property -> !ignoredProperties.contains(property.getName())) + .collect(Collectors.toList()); } /** diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java index 3a34920ce..8739add8a 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java @@ -28,14 +28,40 @@ package org.geysermc.geyser.platform.standalone; import lombok.extern.slf4j.Slf4j; import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.io.IoBuilder; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.ChatColor; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; @Slf4j public class GeyserStandaloneLogger extends SimpleTerminalConsole implements GeyserLogger, GeyserCommandSource { + private static final Logger logger = LogManager.getLogger("GeyserConsole"); + + /** + * Sets up {@code System.out} and {@code System.err} to redirect to log4j. + */ + public static void setupStreams() { + System.setOut(IoBuilder.forLogger(logger).setLevel(Level.INFO).buildPrintStream()); + System.setErr(IoBuilder.forLogger(logger).setLevel(Level.ERROR).buildPrintStream()); + } + + @Override + protected LineReader buildReader(LineReaderBuilder builder) { + builder.completer((reader, line, candidates) -> { + var suggestions = GeyserImpl.getInstance().commandRegistry().suggestionsFor(this, line.line()); + for (var suggestion : suggestions.list()) { + candidates.add(new Candidate(suggestion.suggestion())); + } + }); + return super.buildReader(builder); + } @Override protected boolean isRunning() { @@ -44,7 +70,9 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey @Override protected void runCommand(String line) { - GeyserImpl.getInstance().commandManager().runCommand(this, line); + // don't block the terminal! + GeyserImpl geyser = GeyserImpl.getInstance(); + geyser.getScheduledThread().execute(() -> geyser.commandRegistry().runCommand(this, line)); } @Override @@ -87,6 +115,12 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey log.debug(ChatColor.GRAY + message); } + @Override + public void debug(String message, Object... arguments) { + // We can't use the debug call that would format for us as we're using Java's string formatting + log.debug(ChatColor.GRAY + String.format(message, arguments)); + } + @Override public void setDebug(boolean debug) { Configurator.setLevel(log.getName(), debug ? Level.DEBUG : Level.INFO); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java index b82d8cc94..b1acb585b 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java @@ -25,10 +25,17 @@ package org.geysermc.geyser.platform.standalone.gui; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.NullConfiguration; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.pattern.PatternFormatter; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; @@ -36,11 +43,16 @@ import javax.swing.*; import javax.swing.table.DefaultTableModel; import javax.swing.text.Document; import java.awt.*; -import java.awt.event.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -83,7 +95,8 @@ public class GeyserStandaloneGUI { // Remove Java UI look try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception ignored) { } + } catch (Exception ignored) { + } // Show a confirm dialog on close frame.addWindowListener(new WindowAdapter() { @@ -181,7 +194,8 @@ public class GeyserStandaloneGUI { openButton.addActionListener(e -> { try { Desktop.getDesktop().open(new File("./")); - } catch (IOException ignored) { } + } catch (IOException ignored) { + } }); fileMenu.add(openButton); @@ -245,41 +259,19 @@ public class GeyserStandaloneGUI { /** * Redirect the default io streams to the text pane */ - public void redirectSystemStreams() { - // Setup a new output stream to forward it to the text pane - OutputStream out = new OutputStream() { - @Override - public void write(final int b) { - appendConsole(String.valueOf((char) b)); - } - - @Override - public void write(byte @NonNull [] b, int off, int len) { - appendConsole(new String(b, off, len)); - } - - @Override - public void write(byte @NonNull[] b) { - write(b, 0, b.length); - } - }; - - // Override the system output streams - System.setOut(new PrintStream(out, true)); - System.setErr(new PrintStream(out, true)); - + public void addGuiAppender() { + new GUIAppender().start(); } /** - * Enable the command input box. + * Enables the command input box. * - * @param executor the executor for running commands off the GUI thread - * @param commandManager the command manager to delegate commands to + * @param executor the executor that commands will be run on + * @param registry the command registry containing all current commands */ - public void enableCommands(ScheduledExecutorService executor, GeyserCommandManager commandManager) { + public void enableCommands(ScheduledExecutorService executor, CommandRegistry registry) { // we don't want to block the GUI thread with the command execution - // todo: once cloud is used, an AsynchronousCommandExecutionCoordinator can be used to avoid this scheduler - commandListener.handler = cmd -> executor.schedule(() -> commandManager.runCommand(logger, cmd), 0, TimeUnit.SECONDS); + commandListener.dispatcher = cmd -> executor.execute(() -> registry.runCommand(logger, cmd)); commandInput.setEnabled(true); commandInput.requestFocusInWindow(); } @@ -344,14 +336,41 @@ public class GeyserStandaloneGUI { private class CommandListener implements ActionListener { - private Consumer handler; + private Consumer dispatcher; @Override public void actionPerformed(ActionEvent e) { - String command = commandInput.getText(); + // the headless variant of Standalone strips trailing whitespace for us - we need to manually + String command = commandInput.getText().stripTrailing(); appendConsole(command + "\n"); // show what was run in the console - handler.accept(command); // run the command + dispatcher.accept(command); // run the command commandInput.setText(""); // clear the input } } + + private class GUIAppender extends AbstractAppender { + private static final List FORMATTERS = PatternLayout.createPatternParser(new NullConfiguration()) + .parse( + "[%d{HH:mm:ss} %style{%highlight{%level}{FATAL=red, ERROR=red, WARN=yellow bright, INFO=cyan bright, DEBUG=green, TRACE=white}}] %minecraftFormatting{%msg}%n", + true, + false, + false + ); + + protected GUIAppender() { + super("GUIAppender", null, null, false, Property.EMPTY_ARRAY); + + ((Logger) LogManager.getRootLogger()).addAppender(this); + } + + @Override + public void append(LogEvent event) { + StringBuilder formatted = new StringBuilder(); + for (PatternFormatter formatter : FORMATTERS) { + formatter.format(event, formatted); + } + + appendConsole(formatted.toString()); + } + } } diff --git a/bootstrap/standalone/src/main/resources/log4j2.xml b/bootstrap/standalone/src/main/resources/log4j2.xml index 54f6f9528..0304e1924 100644 --- a/bootstrap/standalone/src/main/resources/log4j2.xml +++ b/bootstrap/standalone/src/main/resources/log4j2.xml @@ -4,9 +4,6 @@ - - - @@ -18,8 +15,10 @@ - + + + - \ No newline at end of file + diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts index 97e9d1f57..70bffe5bc 100644 --- a/bootstrap/velocity/build.gradle.kts +++ b/bootstrap/velocity/build.gradle.kts @@ -1,44 +1,33 @@ +plugins { + id("geyser.platform-conventions") + id("geyser.modrinth-uploading-conventions") + alias(libs.plugins.runvelocity) +} + dependencies { annotationProcessor(libs.velocity.api) api(projects.core) + compileOnly(libs.velocity.proxy) + compileOnly(libs.netty.transport.native.io.uring) + compileOnly(libs.netty.transport.native.kqueue) + compileOnlyApi(libs.velocity.api) + api(libs.cloud.velocity) } platformRelocate("com.fasterxml.jackson") platformRelocate("it.unimi.dsi.fastutil") platformRelocate("net.kyori.adventure.text.serializer.gson.legacyimpl") platformRelocate("org.yaml") - -exclude("com.google.*:*") - -// Needed because Velocity provides every dependency except netty-resolver-dns -exclude("io.netty:netty-transport-native-epoll:*") -exclude("io.netty:netty-transport-native-unix-common:*") -exclude("io.netty:netty-transport-native-kqueue:*") -exclude("io.netty:netty-handler:*") -exclude("io.netty:netty-common:*") -exclude("io.netty:netty-buffer:*") -exclude("io.netty:netty-resolver:*") -exclude("io.netty:netty-transport:*") -exclude("io.netty:netty-codec:*") -exclude("io.netty:netty-codec-haproxy:*") -exclude("org.slf4j:*") -exclude("org.ow2.asm:*") - -// Exclude all Kyori dependencies except the legacy NBT serializer -exclude("net.kyori:adventure-api:*") -exclude("net.kyori:examination-api:*") -exclude("net.kyori:examination-string:*") -exclude("net.kyori:adventure-text-serializer-gson:*") -exclude("net.kyori:adventure-text-serializer-legacy:*") -exclude("net.kyori:adventure-nbt:*") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated // These dependencies are already present on the platform provided(libs.velocity.api) -application { - mainClass.set("org.geysermc.geyser.platform.velocity.GeyserVelocityMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.velocity.GeyserVelocityMain" } tasks.withType { @@ -46,25 +35,21 @@ tasks.withType { dependencies { exclude(dependency("com.google.*:.*")) - // Needed because Velocity provides every dependency except netty-resolver-dns - exclude(dependency("io.netty:netty-transport-native-epoll:.*")) - exclude(dependency("io.netty:netty-transport-native-unix-common:.*")) - exclude(dependency("io.netty:netty-transport-native-kqueue:.*")) - exclude(dependency("io.netty:netty-handler:.*")) - exclude(dependency("io.netty:netty-common:.*")) - exclude(dependency("io.netty:netty-buffer:.*")) - exclude(dependency("io.netty:netty-resolver:.*")) - exclude(dependency("io.netty:netty-transport:.*")) - exclude(dependency("io.netty:netty-codec:.*")) - exclude(dependency("io.netty:netty-codec-haproxy:.*")) + exclude(dependency("io.netty:.*")) exclude(dependency("org.slf4j:.*")) exclude(dependency("org.ow2.asm:.*")) - // Exclude all Kyori dependencies except the legacy NBT serializer - exclude(dependency("net.kyori:adventure-api:.*")) - exclude(dependency("net.kyori:examination-api:.*")) - exclude(dependency("net.kyori:examination-string:.*")) - exclude(dependency("net.kyori:adventure-text-serializer-gson:.*")) - exclude(dependency("net.kyori:adventure-text-serializer-legacy:.*")) - exclude(dependency("net.kyori:adventure-nbt:.*")) + // Exclude all Kyori dependencies + exclude(dependency("net.kyori:.*:.*")) } -} \ No newline at end of file +} + +modrinth { + uploadFile.set(tasks.getByPath("shadowJar")) + loaders.addAll("velocity") +} + +tasks { + runVelocity { + version(libs.versions.runvelocityversion.get()) + } +} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java index 68a9eb40b..a33a89f4b 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java @@ -26,16 +26,38 @@ package org.geysermc.geyser.platform.velocity; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.proxy.network.TransportType; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoop; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.IoHandler; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SingleThreadIoEventLoop; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.epoll.EpollIoHandler; +import io.netty.channel.kqueue.KQueueIoHandler; import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalIoHandler; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.uring.IoUringIoHandler; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadAwareExecutor; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.network.netty.GeyserInjector; +import org.geysermc.geyser.network.netty.IoHandlerWrapper; import org.geysermc.geyser.network.netty.LocalServerChannelWrapper; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; import java.util.function.Supplier; public class GeyserVelocityInjector extends GeyserInjector { @@ -48,6 +70,13 @@ public class GeyserVelocityInjector extends GeyserInjector { @Override @SuppressWarnings("unchecked") protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception { + try { + Class.forName("io.netty.channel.MultiThreadIoEventLoopGroup"); + } catch (ClassNotFoundException e) { + bootstrap.getGeyserLogger().error("use-direct-connection disabled as Velocity is not up-to-date."); + return; + } + Field cm = proxy.getClass().getDeclaredField("cm"); cm.setAccessible(true); Object connectionManager = cm.get(proxy); @@ -63,36 +92,64 @@ public class GeyserVelocityInjector extends GeyserInjector { serverWriteMarkField.setAccessible(true); WriteBufferWaterMark serverWriteMark = (WriteBufferWaterMark) serverWriteMarkField.get(null); - EventLoopGroup bossGroup = (EventLoopGroup) connectionManagerClass.getMethod("getBossGroup").invoke(connectionManager); - Field workerGroupField = connectionManagerClass.getDeclaredField("workerGroup"); workerGroupField.setAccessible(true); - EventLoopGroup workerGroup = (EventLoopGroup) workerGroupField.get(connectionManager); + IoEventLoopGroup workerGroup = (IoEventLoopGroup) workerGroupField.get(connectionManager); + + var factory = LocalIoHandler.newFactory(); + var nativeFactory = getNativeHandlerFactory(); + var wrapperFactory = new IoHandlerFactory() { + @Override + public IoHandler newHandler(ThreadAwareExecutor ioExecutor) { + return new IoHandlerWrapper(factory.newHandler(ioExecutor), nativeFactory.newHandler(ioExecutor)); + } + }; + + EventLoopGroup wrapperGroup = new MultiThreadIoEventLoopGroup(factory) { + @Override + protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory("Geyser Backend Worker Group", Thread.MAX_PRIORITY); + } + + @Override + protected IoEventLoop newChild(Executor executor, IoHandlerFactory ioHandlerFactory, Object... args) { + return new SingleThreadIoEventLoop(workerGroup, executor, wrapperFactory); + } + }; // This method is what initializes the connection in Java Edition, after Netty is all set. Method initChannel = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class); initChannel.setAccessible(true); ChannelFuture channelFuture = (new ServerBootstrap() - .channel(LocalServerChannelWrapper.class) - .childHandler(new ChannelInitializer<>() { - @Override - protected void initChannel(@NonNull Channel ch) throws Exception { - initChannel.invoke(channelInitializer, ch); + .channel(LocalServerChannelWrapper.class) + .childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(@NonNull Channel ch) throws Exception { + initChannel.invoke(channelInitializer, ch); - if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) { - ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler", - new GeyserVelocityCompressionDisabler()); - } + if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) { + ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler", + new GeyserVelocityCompressionDisabler()); } - }) - .group(bossGroup, workerGroup) // Cannot be DefaultEventLoopGroup - .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur - .localAddress(LocalAddress.ANY)) - .bind() - .syncUninterruptibly(); + } + }) + .group(new MultiThreadIoEventLoopGroup(LocalIoHandler.newFactory()), wrapperGroup) // Cannot be DefaultEventLoopGroup + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur + .localAddress(LocalAddress.ANY)) + .bind() + .syncUninterruptibly(); this.localChannel = channelFuture; this.serverSocketAddress = channelFuture.channel().localAddress(); } + + private static IoHandlerFactory getNativeHandlerFactory() { + return switch (TransportType.bestType()) { + case NIO -> NioIoHandler.newFactory(); + case EPOLL -> EpollIoHandler.newFactory(); + case KQUEUE -> KQueueIoHandler.newFactory(); + case IO_URING -> IoUringIoHandler.newFactory(); + }; + } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java index 567870e7f..0331b825e 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java @@ -25,13 +25,13 @@ package org.geysermc.geyser.platform.velocity; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import org.geysermc.geyser.GeyserLogger; import org.slf4j.Logger; -@AllArgsConstructor +@RequiredArgsConstructor public class GeyserVelocityLogger implements GeyserLogger { private final Logger logger; @Getter @Setter @@ -73,4 +73,11 @@ public class GeyserVelocityLogger implements GeyserLogger { info(message); } } -} \ No newline at end of file + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + logger.info(String.format(message, arguments)); + } + } +} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java index 8944ea134..a61a65805 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java @@ -26,12 +26,16 @@ package org.geysermc.geyser.platform.velocity; import com.velocitypowered.api.event.proxy.ProxyPingEvent; +import com.velocitypowered.api.network.HandshakeIntent; +import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.ServerPing; +import com.velocitypowered.api.proxy.server.ServerPing.Version; import lombok.AllArgsConstructor; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; @@ -50,12 +54,14 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { try { event = server.getEventManager().fire(new ProxyPingEvent(new GeyserInboundConnection(inetSocketAddress), ServerPing.builder() .description(server.getConfiguration().getMotd()).onlinePlayers(server.getPlayerCount()) - .maximumPlayers(server.getConfiguration().getShowMaxPlayers()).build())).get(); + .maximumPlayers(server.getConfiguration().getShowMaxPlayers()) + .version(new Version(GameProtocol.getJavaProtocolVersion(), GameProtocol.getJavaMinecraftVersion())) + .build())).get(); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } return new GeyserPingInfo( - LegacyComponentSerializer.legacy('§').serialize(event.getPing().getDescriptionComponent()), + GsonComponentSerializer.gson().serialize(event.getPing().getDescriptionComponent()), event.getPing().getPlayers().map(ServerPing.Players::getMax).orElse(1), event.getPing().getPlayers().map(ServerPing.Players::getOnline).orElse(0) ); @@ -79,6 +85,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { return Optional.empty(); } + @Override + public Optional getRawVirtualHost() { + return Optional.empty(); + } + @Override public boolean isActive() { return false; @@ -88,6 +99,16 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { public ProtocolVersion getProtocolVersion() { return ProtocolVersion.MAXIMUM_VERSION; } + + @Override + public ProtocolState getProtocolState() { + return ProtocolState.STATUS; + } + + @Override + public HandshakeIntent getHandshakeIntent() { + return HandshakeIntent.STATUS; + } } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java index 347a47d63..a83a84d2c 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java @@ -26,7 +26,7 @@ package org.geysermc.geyser.platform.velocity; import com.google.inject.Inject; -import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ListenerBoundEvent; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; @@ -34,24 +34,30 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.network.ListenerType; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; +import io.netty.buffer.AdaptiveByteBufAllocator; +import io.netty.buffer.ByteBufAllocator; import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.velocity.command.GeyserVelocityCommandExecutor; +import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.velocity.VelocityCommandManager; import org.slf4j.Logger; import java.io.File; @@ -59,66 +65,90 @@ import java.io.IOException; import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Map; import java.util.UUID; @Plugin(id = "geyser", name = GeyserImpl.NAME + "-Velocity", version = GeyserImpl.VERSION, url = "https://geysermc.org", authors = "GeyserMC") public class GeyserVelocityPlugin implements GeyserBootstrap { - @Inject - private Logger logger; - @Inject - private ProxyServer proxyServer; - - @Inject - private CommandManager commandManager; - - private GeyserCommandManager geyserCommandManager; + private final ProxyServer proxyServer; + private final PluginContainer container; + private final GeyserVelocityLogger geyserLogger; private GeyserVelocityConfiguration geyserConfig; private GeyserVelocityInjector geyserInjector; - private GeyserVelocityLogger geyserLogger; private IGeyserPingPassthrough geyserPingPassthrough; - + private CommandRegistry commandRegistry; private GeyserImpl geyser; @Getter private final Path configFolder = Paths.get("plugins/" + GeyserImpl.NAME + "-Velocity/"); + @Inject + public GeyserVelocityPlugin(ProxyServer server, PluginContainer container, Logger logger) { + this.proxyServer = server; + this.container = container; + this.geyserLogger = new GeyserVelocityLogger(logger); + } + @Override public void onGeyserInitialize() { GeyserLocale.init(this); - if (!ProtocolVersion.isSupported(GameProtocol.getJavaProtocolVersion())) { - logger.error(" / \\"); - logger.error(" / \\"); - logger.error(" / | \\"); - logger.error(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", proxyServer.getVersion().getName())); - logger.error(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps")); - logger.error(" / o \\"); - logger.error("/_____________\\"); + // TODO remove when this isn't an issue anymore + boolean adaptiveAllocatorUsed = System.getProperty("io.netty.allocator.type") == null && ByteBufAllocator.DEFAULT instanceof AdaptiveByteBufAllocator; + + if (!ProtocolVersion.isSupported(GameProtocol.getJavaProtocolVersion()) || adaptiveAllocatorUsed) { + geyserLogger.error(" / \\"); + geyserLogger.error(" / \\"); + geyserLogger.error(" / | \\"); + geyserLogger.error(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", proxyServer.getVersion().getName())); + geyserLogger.error(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps")); + geyserLogger.error(" / o \\"); + geyserLogger.error("/_____________\\"); + } + + // Only use io_uring when velocity does as well + if (Boolean.getBoolean("velocity.enable-iouring-transport")) { + System.setProperty("Mcpl.io_uring", "true"); } if (!loadConfig()) { return; } - this.geyserLogger = new GeyserVelocityLogger(logger, geyserConfig.isDebugMode()); + this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this); this.geyserInjector = new GeyserVelocityInjector(proxyServer); + + // We need to register commands here, rather than in onGeyserEnable which is invoked during the appropriate ListenerBoundEvent. + // Reason: players can connect after a listener is bound, and a player join locks registration to the cloud CommandManager. + var sourceConverter = new CommandSourceConverter<>( + CommandSource.class, + id -> proxyServer.getPlayer(id).orElse(null), + proxyServer::getConsoleCommandSource, + VelocityCommandSource::new + ); + CommandManager cloud = new VelocityCommandManager<>( + container, + proxyServer, + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + this.commandRegistry = new CommandRegistry(geyser, cloud, false); // applying root permission would be a breaking change because we can't register permission defaults } @Override public void onGeyserEnable() { + // If e.g. the config failed to load, GeyserImpl was not loaded and we cannot start + if (geyser == null) { + return; + } if (GeyserImpl.getInstance().isReloading()) { if (!loadConfig()) { return; } this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - } else { - this.geyserCommandManager = new GeyserCommandManager(geyser); - this.geyserCommandManager.init(); } GeyserImpl.start(); @@ -129,22 +159,10 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer); } - // No need to re-register commands when reloading - if (GeyserImpl.getInstance().isReloading()) { - return; + // No need to re-register events + if (!GeyserImpl.getInstance().isReloading()) { + proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener()); } - - this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(geyser, geyserCommandManager.getCommands())); - for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { - Map commands = entry.getValue(); - if (commands.isEmpty()) { - continue; - } - - this.commandManager.register(entry.getKey().description().id(), new GeyserVelocityCommandExecutor(this.geyser, commands)); - } - - proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener()); } @Override @@ -175,8 +193,8 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override @@ -212,6 +230,11 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { return new GeyserVelocityDumpInfo(proxyServer); } + @Override + public @NonNull String getServerPlatform() { + return proxyServer.getVersion().getName(); + } + @Nullable @Override public SocketAddress getSocketAddress() { @@ -249,7 +272,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); this.geyserConfig = FileUtils.loadConfig(configFile, GeyserVelocityConfiguration.class); } catch (IOException ex) { - logger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); return false; } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java index 31e584612..c1c88b70d 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java @@ -28,8 +28,8 @@ package org.geysermc.geyser.platform.velocity; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.proxy.Player; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; @@ -39,7 +39,7 @@ public final class GeyserVelocityUpdateListener { public void onPlayerJoin(PostLoginEvent event) { if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { final Player player = event.getPlayer(); - if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new VelocityCommandSource(player)); } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java deleted file mode 100644 index c89c35b06..000000000 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.velocity.command; - -import com.velocitypowered.api.command.SimpleCommand; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.ChatColor; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class GeyserVelocityCommandExecutor extends GeyserCommandExecutor implements SimpleCommand { - - public GeyserVelocityCommandExecutor(GeyserImpl geyser, Map commands) { - super(geyser, commands); - } - - @Override - public void execute(Invocation invocation) { - GeyserCommandSource sender = new VelocityCommandSource(invocation.source()); - GeyserSession session = getGeyserSession(sender); - - if (invocation.arguments().length > 0) { - GeyserCommand command = getCommand(invocation.arguments()[0]); - if (command != null) { - if (!invocation.source().hasPermission(getCommand(invocation.arguments()[0]).permission())) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } - if (command.isBedrockOnly() && session == null) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.locale())); - return; - } - command.execute(session, sender, invocation.arguments().length > 1 ? Arrays.copyOfRange(invocation.arguments(), 1, invocation.arguments().length) : new String[0]); - } else { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", sender.locale()); - sender.sendMessage(ChatColor.RED + message); - } - } else { - getCommand("help").execute(session, sender, new String[0]); - } - } - - @Override - public List suggest(Invocation invocation) { - // Velocity seems to do the splitting a bit differently. This results in the same behaviour in bungeecord/spigot. - if (invocation.arguments().length == 0 || invocation.arguments().length == 1) { - return tabComplete(new VelocityCommandSource(invocation.source())); - } - return Collections.emptyList(); - } -} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java index 403e4cb20..f55f145ed 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java @@ -31,10 +31,12 @@ import com.velocitypowered.api.proxy.Player; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.GeyserLocale; import java.util.Locale; +import java.util.UUID; public class VelocityCommandSource implements GeyserCommandSource { @@ -58,7 +60,7 @@ public class VelocityCommandSource implements GeyserCommandSource { @Override public void sendMessage(@NonNull String message) { - handle.sendMessage(LegacyComponentSerializer.legacy('§').deserialize(message)); + handle.sendMessage(LegacyComponentSerializer.legacySection().deserialize(message)); } @Override @@ -72,6 +74,14 @@ public class VelocityCommandSource implements GeyserCommandSource { return handle instanceof ConsoleCommandSource; } + @Override + public @Nullable UUID playerUuid() { + if (handle instanceof Player player) { + return player.getUniqueId(); + } + return null; + } + @Override public String locale() { if (handle instanceof Player) { @@ -83,6 +93,12 @@ public class VelocityCommandSource implements GeyserCommandSource { @Override public boolean hasPermission(String permission) { - return handle.hasPermission(permission); + // Handle blank permissions ourselves, as velocity only handles empty ones + return permission.isBlank() || handle.hasPermission(permission); + } + + @Override + public Object handle() { + return handle; } } diff --git a/bootstrap/viaproxy/build.gradle.kts b/bootstrap/viaproxy/build.gradle.kts index 01c5b5b34..2b653aef8 100644 --- a/bootstrap/viaproxy/build.gradle.kts +++ b/bootstrap/viaproxy/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("geyser.platform-conventions") +} + dependencies { api(projects.core) @@ -8,12 +12,14 @@ platformRelocate("net.kyori") platformRelocate("org.yaml") platformRelocate("it.unimi.dsi.fastutil") platformRelocate("org.cloudburstmc.netty") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated // These dependencies are already present on the platform provided(libs.viaproxy) -application { - mainClass.set("org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain" } tasks.withType { diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java index ad249eb3b..afc46fa6a 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java @@ -26,15 +26,24 @@ package org.geysermc.geyser.platform.viaproxy; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import net.raphimc.vialegacy.api.LegacyProtocolVersion; -import net.raphimc.viaproxy.cli.options.Options; +import net.raphimc.viaproxy.ViaProxy; +import net.raphimc.viaproxy.protocoltranslator.viaproxy.ViaProxyConfig; import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; import java.io.File; import java.nio.file.Path; @JsonIgnoreProperties(ignoreUnknown = true) +@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final public class GeyserViaProxyConfiguration extends GeyserJacksonConfiguration { + private RemoteConfiguration remote = new RemoteConfiguration() { + @Override + public boolean isForwardHost() { + return super.isForwardHost() || !ViaProxy.getConfig().getWildcardDomainHandling().equals(ViaProxyConfig.WildcardDomainHandling.NONE); + } + }; + @Override public Path getFloodgateKeyPath() { return new File(GeyserViaProxyPlugin.ROOT_FOLDER, this.getFloodgateKeyFile()).toPath(); @@ -43,11 +52,16 @@ public class GeyserViaProxyConfiguration extends GeyserJacksonConfiguration { @Override public int getPingPassthroughInterval() { int interval = super.getPingPassthroughInterval(); - if (interval < 15 && Options.PROTOCOL_VERSION != null && Options.PROTOCOL_VERSION.olderThanOrEqualTo(LegacyProtocolVersion.r1_6_4)) { + if (interval < 15 && ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().olderThanOrEqualTo(LegacyProtocolVersion.r1_6_4)) { // <= 1.6.4 servers sometimes block incoming connections from an IP address if too many connections are made interval = 15; } return interval; } + @Override + public RemoteConfiguration getRemote() { + return this.remote; + } + } diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java index 08f3d5371..0bfc9d022 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.platform.viaproxy; import lombok.Getter; import net.raphimc.viaproxy.ViaProxy; -import net.raphimc.viaproxy.cli.options.Options; import net.raphimc.viaproxy.plugins.ViaProxyPlugin; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.text.AsteriskSerializer; @@ -49,8 +48,8 @@ public class GeyserViaProxyDumpInfo extends BootstrapDumpInfo { public GeyserViaProxyDumpInfo() { this.platformVersion = ViaProxy.VERSION; - this.onlineMode = Options.ONLINE_MODE; - if (Options.BIND_ADDRESS instanceof InetSocketAddress inetSocketAddress) { + this.onlineMode = ViaProxy.getConfig().isProxyOnlineMode(); + if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress inetSocketAddress) { this.serverIP = inetSocketAddress.getHostString(); this.serverPort = inetSocketAddress.getPort(); } else { diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java index 10f414b51..fdcfe2279 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java @@ -75,6 +75,13 @@ public class GeyserViaProxyLogger implements GeyserLogger, GeyserCommandSource { } } + @Override + public void debug(String message, Object... arguments) { + if (this.debug) { + this.debug(String.format(message, arguments)); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java index 675c92534..582a97d78 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.platform.viaproxy; -import net.raphimc.viaproxy.plugins.PluginManager; import org.geysermc.geyser.GeyserMain; public class GeyserViaProxyMain extends GeyserMain { @@ -39,7 +38,7 @@ public class GeyserViaProxyMain extends GeyserMain { } public String getPluginFolder() { - return PluginManager.PLUGINS_DIR.getName(); + return "plugins"; } } diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java index 47745df7d..678db0e53 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java @@ -24,48 +24,56 @@ */ package org.geysermc.geyser.platform.viaproxy; +import io.netty.channel.AbstractChannel; import net.lenni0451.lambdaevents.EventHandler; +import net.lenni0451.reflect.stream.RStream; import net.raphimc.vialegacy.api.LegacyProtocolVersion; import net.raphimc.viaproxy.ViaProxy; -import net.raphimc.viaproxy.cli.options.Options; import net.raphimc.viaproxy.plugins.PluginManager; import net.raphimc.viaproxy.plugins.ViaProxyPlugin; +import net.raphimc.viaproxy.plugins.events.Client2ProxyChannelInitializeEvent; import net.raphimc.viaproxy.plugins.events.ConsoleCommandEvent; import net.raphimc.viaproxy.plugins.events.ProxyStartEvent; import net.raphimc.viaproxy.plugins.events.ProxyStopEvent; import net.raphimc.viaproxy.plugins.events.ShouldVerifyOnlineModeEvent; +import net.raphimc.viaproxy.plugins.events.types.ITyped; import org.apache.logging.log4j.LogManager; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; +import org.geysermc.geyser.platform.viaproxy.listener.GeyserServerTransferListener; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.LoopbackUtil; -import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; -public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootstrap { +public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootstrap, EventRegistrar { public static final File ROOT_FOLDER = new File(PluginManager.PLUGINS_DIR, "Geyser"); private final GeyserViaProxyLogger logger = new GeyserViaProxyLogger(LogManager.getLogger("Geyser")); private GeyserViaProxyConfiguration config; private GeyserImpl geyser; - private GeyserCommandManager commandManager; + private StandaloneCloudCommandManager cloud; + private CommandRegistry commandRegistry; private IGeyserPingPassthrough pingPassthrough; @Override @@ -86,7 +94,9 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst @EventHandler private void onConsoleCommand(final ConsoleCommandEvent event) { final String command = event.getCommand().startsWith("/") ? event.getCommand().substring(1) : event.getCommand(); - if (this.getGeyserCommandManager().runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs()))) { + CommandRegistry registry = this.getCommandRegistry(); + if (registry.rootCommands().contains(command)) { + registry.runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs())); event.setCancelled(true); } } @@ -104,6 +114,27 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst } } + @EventHandler + private void onClient2ProxyChannelInitialize(Client2ProxyChannelInitializeEvent event) { + if (event.getType() != ITyped.Type.POST || event.isLegacyPassthrough()) { + return; + } + if (System.getProperty("geyser.viaproxy.disableIpPassthrough") != null) { // Temporary until Configurate branch is merged + return; + } + + final GeyserSession session = GeyserImpl.getInstance().onlineConnections().stream() + .filter(c -> c.getDownstream() != null) + .filter(c -> c.getDownstream().getSession().getLocalAddress().equals(event.getChannel().remoteAddress())) + .findAny().orElse(null); + if (session != null) { + final SocketAddress realAddress = session.getSocketAddress(); + if (event.getChannel() instanceof AbstractChannel) { + RStream.of(AbstractChannel.class, event.getChannel()).fields().by("remoteAddress").set(realAddress); + } + } + } + @EventHandler private void onProxyStart(final ProxyStartEvent event) { this.onGeyserEnable(); @@ -121,26 +152,42 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst } this.geyser = GeyserImpl.load(PlatformType.VIAPROXY, this); + this.geyser.eventBus().register(this, new GeyserServerTransferListener()); LoopbackUtil.checkAndApplyLoopback(this.logger); } @Override public void onGeyserEnable() { - if (GeyserImpl.getInstance().isReloading()) { + // If e.g. the config failed to load, GeyserImpl was not loaded and we cannot start + if (geyser == null) { + return; + } + boolean reloading = geyser.isReloading(); + if (reloading) { if (!this.loadConfig()) { return; } + } else { + // Only initialized once - documented in the Geyser-Standalone bootstrap + this.cloud = new StandaloneCloudCommandManager(geyser); + this.commandRegistry = new CommandRegistry(geyser, cloud); } - this.commandManager = new GeyserCommandManager(this.geyser); - this.commandManager.init(); - GeyserImpl.start(); - if (Options.PROTOCOL_VERSION != null && Options.PROTOCOL_VERSION.newerThanOrEqualTo(LegacyProtocolVersion.b1_8tob1_8_1)) { + if (!reloading) { + // Event must be fired after CommandRegistry has subscribed its listener. + // Also, the subscription for the Permissions class is created when Geyser is initialized (by GeyserImpl#start) + this.cloud.fireRegisterPermissionsEvent(); + } + + if (ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().newerThanOrEqualTo(LegacyProtocolVersion.b1_8tob1_8_1)) { // Only initialize the ping passthrough if the protocol version is above beta 1.7.3, as that's when the status protocol was added this.pingPassthrough = GeyserLegacyPingPassthrough.init(this.geyser); } + if (this.config.getRemote().authType() == AuthType.FLOODGATE) { + ViaProxy.getConfig().setPassthroughBungeecordPlayerInfo(true); + } } @Override @@ -164,8 +211,8 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.commandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override @@ -183,22 +230,27 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst return new GeyserViaProxyDumpInfo(); } - @NotNull + @Override + public @NonNull String getServerPlatform() { + return PlatformType.VIAPROXY.platformName(); + } + + @NonNull @Override public String getServerBindAddress() { - if (Options.BIND_ADDRESS instanceof InetSocketAddress socketAddress) { + if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress socketAddress) { return socketAddress.getHostString(); } else { - throw new IllegalStateException("Unsupported bind address type: " + Options.BIND_ADDRESS.getClass().getName()); + throw new IllegalStateException("Unsupported bind address type: " + ViaProxy.getConfig().getBindAddress().getClass().getName()); } } @Override public int getServerPort() { - if (Options.BIND_ADDRESS instanceof InetSocketAddress socketAddress) { + if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress socketAddress) { return socketAddress.getPort(); } else { - throw new IllegalStateException("Unsupported bind address type: " + Options.BIND_ADDRESS.getClass().getName()); + throw new IllegalStateException("Unsupported bind address type: " + ViaProxy.getConfig().getBindAddress().getClass().getName()); } } @@ -207,6 +259,7 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst return false; } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { try { final File configFile = FileUtils.fileOrCopiedFromResource(new File(ROOT_FOLDER, "config.yml"), "config.yml", s -> s.replaceAll("generateduuid", UUID.randomUUID().toString()), this); diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/listener/GeyserServerTransferListener.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/listener/GeyserServerTransferListener.java new file mode 100644 index 000000000..64b3cc56e --- /dev/null +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/listener/GeyserServerTransferListener.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.viaproxy.listener; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.net.HostAndPort; +import org.geysermc.event.PostOrder; +import org.geysermc.event.subscribe.Subscribe; +import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent; +import org.geysermc.geyser.api.event.java.ServerTransferEvent; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class GeyserServerTransferListener { + + private final Cache> cookieStorages = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(); + + @Subscribe(postOrder = PostOrder.FIRST) + private void onServerTransfer(final ServerTransferEvent event) { + this.cookieStorages.put(event.connection().xuid(), event.cookies()); + final GeyserSession geyserSession = (GeyserSession) event.connection(); + final HostAndPort hostAndPort = HostAndPort.fromString(geyserSession.getClientData().getServerAddress()).withDefaultPort(19132); + event.bedrockHost(hostAndPort.getHost()); + event.bedrockPort(hostAndPort.getPort()); + } + + @Subscribe(postOrder = PostOrder.FIRST) + private void onSessionLogin(final SessionLoginEvent event) { + final Map cookies = this.cookieStorages.asMap().remove(event.connection().xuid()); + if (cookies != null) { + event.cookies(cookies); + event.transferring(true); + } + } + +} diff --git a/bootstrap/viaproxy/src/main/resources/viaproxy.yml b/bootstrap/viaproxy/src/main/resources/viaproxy.yml index f42cda77b..89fc612cd 100644 --- a/bootstrap/viaproxy/src/main/resources/viaproxy.yml +++ b/bootstrap/viaproxy/src/main/resources/viaproxy.yml @@ -2,4 +2,4 @@ name: "${name}-ViaProxy" version: "${version}" author: "${author}" main: "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyPlugin" -min-version: "3.2.0" +min-version: "3.3.2" diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index b7aab2bf0..b87490880 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -12,6 +12,14 @@ repositories { } dependencies { + // This is for the LibsAccessor.kt hack + // this is OK as long as the same version catalog is used in the main build and build-logic + // see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + + // This is for applying plugins, and using the version from the libs.versions.toml + // Unfortunately they still need to be applied by their string name in the convention scripts. + implementation(libs.lombok) implementation(libs.indra) implementation(libs.shadow) implementation(libs.architectury.plugin) diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 63bde189b..bd4560d11 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -8,4 +8,4 @@ dependencyResolutionManagement { } } -rootProject.name = "build-logic" \ No newline at end of file +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/LibsAccessor.kt b/build-logic/src/main/kotlin/LibsAccessor.kt new file mode 100644 index 000000000..2a0c09eb6 --- /dev/null +++ b/build-logic/src/main/kotlin/LibsAccessor.kt @@ -0,0 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +val Project.libs: LibrariesForLibs + get() = rootProject.extensions.getByType() \ No newline at end of file diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt index 41e11344b..d78525996 100644 --- a/build-logic/src/main/kotlin/extensions.kt +++ b/build-logic/src/main/kotlin/extensions.kt @@ -42,9 +42,12 @@ fun Project.relocate(pattern: String) { } } -fun Project.exclude(group: String) { +// Excludes all dependencies from a module - except one +fun Project.exclude(group: String, lib: Provider) { tasks.named("shadowJar") { - exclude(group) + dependencies { + exclude { it.moduleGroup == group && it.moduleName != lib.get().module.name } + } } } @@ -58,10 +61,6 @@ fun Project.platformRelocate(pattern: String, exclusion: String = "") { val providedDependencies = mutableMapOf>() -fun getProvidedDependenciesForProject(projectName: String): MutableSet { - return providedDependencies.getOrDefault(projectName, emptySet()).toMutableSet() -} - fun Project.provided(pattern: String, name: String, excludedOn: Int = 0b110) { providedDependencies.getOrPut(project.name) { mutableSetOf() } .add("${calcExclusion(pattern, 0b100, excludedOn)}:${calcExclusion(name, 0b10, excludedOn)}") @@ -118,3 +117,12 @@ open class DownloadFilesTask : DefaultTask() { private fun calcExclusion(section: String, bit: Int, excludedOn: Int): String = if (excludedOn and bit > 0) section else "" +fun projectVersion(project: Project): String = + project.version.toString().replace("SNAPSHOT", "b" + buildNumber()) + +fun versionName(project: Project): String = + "Geyser-" + project.name.replaceFirstChar { it.uppercase() } + "-" + projectVersion(project) + +fun buildNumber(): Int = + (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1 + diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 3d41a3dd6..107068819 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -3,9 +3,10 @@ plugins { id("net.kyori.indra") } -dependencies { - compileOnly("org.checkerframework", "checker-qual", "3.19.0") -} +val rootProperties: Map = project.rootProject.properties +group = rootProperties["group"] as String + "." + rootProperties["id"] as String +version = rootProperties["version"] as String +description = rootProperties["description"] as String indra { github("GeyserMC", "Geyser") { @@ -20,18 +21,44 @@ indra { } } -tasks { - processResources { - // Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge - filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json", "viaproxy.yml", "META-INF/mods.toml")) { - expand( - "id" to "geyser", - "name" to "Geyser", - "version" to project.version, - "description" to project.description, - "url" to "https://geysermc.org", - "author" to "GeyserMC" - ) - } +dependencies { + compileOnly("org.checkerframework", "checker-qual", libs.checker.qual.get().version) +} + +repositories { + // mavenLocal() + + mavenCentral() + + // Floodgate, Cumulus etc. + maven("https://repo.opencollab.dev/main") + + // Paper, Velocity + maven("https://repo.papermc.io/repository/maven-public") + + // Spigot + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") { + mavenContent { snapshotsOnly() } } -} \ No newline at end of file + + // NeoForge + maven("https://maven.neoforged.net/releases") { + mavenContent { releasesOnly() } + } + + // Minecraft + maven("https://libraries.minecraft.net") { + name = "minecraft" + mavenContent { releasesOnly() } + } + + // ViaVersion + maven("https://repo.viaversion.com") { + name = "viaversion" + } + + // Jitpack for e.g. MCPL + maven("https://jitpack.io") { + content { includeGroupByRegex("com\\.github\\..*") } + } +} diff --git a/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts b/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts deleted file mode 100644 index e69de29bb..000000000 diff --git a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts index 91bde525e..903d82258 100644 --- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts @@ -2,13 +2,11 @@ import net.fabricmc.loom.task.RemapJarTask import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.maven plugins { - id("geyser.publish-conventions") + id("geyser.platform-conventions") id("architectury-plugin") id("dev.architectury.loom") - id("com.modrinth.minotaur") } // These are provided by Minecraft/modded platforms already, no need to include them @@ -26,26 +24,42 @@ provided("io.netty", "netty-transport-native-epoll") provided("io.netty", "netty-transport-native-unix-common") provided("io.netty", "netty-transport-classes-kqueue") provided("io.netty", "netty-transport-native-kqueue") +provided("io.netty", "netty-transport-native-io_uring") +provided("io.netty", "netty-transport-classes-io_uring") provided("io.netty", "netty-handler") provided("io.netty", "netty-common") provided("io.netty", "netty-buffer") provided("io.netty", "netty-resolver") provided("io.netty", "netty-transport") provided("io.netty", "netty-codec") -provided("io.netty", "netty-resolver-dns") -provided("io.netty", "netty-resolver-dns-native-macos") +provided("io.netty", "netty-codec-base") provided("org.ow2.asm", "asm") +// cloud-fabric/cloud-neoforge jij's all cloud depends already +provided("org.incendo", ".*") +provided("io.leangen.geantyref", "geantyref") + architectury { - minecraft = "1.20.4" + minecraft = libs.minecraft.get().version as String } loom { silentMojangMappingsLicense() } +indra { + javaVersions { + target(21) + } +} + configurations { create("includeTransitive").isTransitive = true + create("shadowBundle") { + isCanBeResolved = true + isCanBeConsumed = false + isTransitive = false + } } tasks { @@ -59,7 +73,7 @@ tasks { shadowJar { // Mirrors the example fabric project, otherwise tons of dependencies are shaded that shouldn't be - configurations = listOf(project.configurations.shadow.get()) + configurations = listOf(project.configurations.getByName("shadowBundle")) // The remapped shadowJar is the final desired mod jar archiveVersion.set(project.version.toString()) archiveClassifier.set("shaded") @@ -75,24 +89,22 @@ tasks { register("remapModrinthJar", RemapJarTask::class) { dependsOn(shadowJar) inputFile.set(shadowJar.get().archiveFile) - archiveVersion.set(project.version.toString() + "+build." + System.getenv("GITHUB_RUN_NUMBER")) + archiveVersion.set(versionName(project)) archiveClassifier.set("") } } afterEvaluate { - val providedDependencies = getProvidedDependenciesForProject(project.name) - - // These are shaded, no need to JiJ them - configurations["shadow"].dependencies.forEach {shadowed -> - println("Not including shadowed dependency: ${shadowed.group}:${shadowed.name}") - providedDependencies.add("${shadowed.group}:${shadowed.name}") - } + val providedDependencies = providedDependencies[project.name]!! + val shadedDependencies = configurations.getByName("shadowBundle").resolvedConfiguration.resolvedArtifacts.stream() + .map { dependency -> "${dependency.moduleVersion.id.module}" }.toList() // Now: Include all transitive dependencies that aren't excluded configurations["includeTransitive"].resolvedConfiguration.resolvedArtifacts.forEach { dep -> - if (!providedDependencies.contains("${dep.moduleVersion.id.group}:${dep.moduleVersion.id.name}") - and !providedDependencies.contains("${dep.moduleVersion.id.group}:.*")) { + val name = "${dep.moduleVersion.id.group}:${dep.moduleVersion.id.name}" + if (!shadedDependencies.contains(name) and !providedDependencies.contains(name) + and !providedDependencies.contains("${dep.moduleVersion.id.group}:.*") + ) { println("Including dependency via JiJ: ${dep.id}") dependencies.add("include", dep.moduleVersion.id.toString()) } else { @@ -102,29 +114,6 @@ afterEvaluate { } dependencies { - minecraft("com.mojang:minecraft:1.20.4") + minecraft(libs.minecraft) mappings(loom.officialMojangMappings()) } - -repositories { - maven("https://repo.opencollab.dev/maven-releases/") - maven("https://repo.opencollab.dev/maven-snapshots/") - maven("https://jitpack.io") - maven("https://oss.sonatype.org/content/repositories/snapshots/") - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") - maven("https://maven.neoforged.net/releases") -} - -modrinth { - token.set(System.getenv("MODRINTH_TOKEN")) // Even though this is the default value, apparently this prevents GitHub Actions caching the token? - projectId.set("wKkoqHrH") - versionNumber.set(project.version as String + "-" + System.getenv("GITHUB_RUN_NUMBER")) - versionType.set("beta") - changelog.set("A changelog can be found at https://github.com/GeyserMC/Geyser/commits") - - syncBodyFrom.set(rootProject.file("README.md").readText()) - - uploadFile.set(tasks.getByPath("remapModrinthJar")) - gameVersions.addAll("1.20.4") - failSilently.set(true) -} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts new file mode 100644 index 000000000..db2d3b850 --- /dev/null +++ b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("com.modrinth.minotaur") +} + +// Ensure that the readme is synched +tasks.modrinth.get().dependsOn(tasks.modrinthSyncBody) + +modrinth { + token.set(System.getenv("MODRINTH_TOKEN") ?: "") // Even though this is the default value, apparently this prevents GitHub Actions caching the token? + debugMode.set(System.getenv("MODRINTH_TOKEN") == null) + projectId.set("geyser") + versionName.set(versionName(project)) + versionNumber.set(projectVersion(project)) + versionType.set("beta") + changelog.set(System.getenv("CHANGELOG") ?: "") + gameVersions.addAll("1.21.9", libs.minecraft.get().version as String) + failSilently.set(true) + + syncBodyFrom.set(rootProject.file("README.md").readText()) +} diff --git a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts index 81d224906..7a342783b 100644 --- a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts @@ -1,4 +1,20 @@ plugins { - application id("geyser.publish-conventions") -} \ No newline at end of file + id("io.freefair.lombok") +} + +tasks { + processResources { + // Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge + filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json", "viaproxy.yml", "META-INF/neoforge.mods.toml")) { + expand( + "id" to "geyser", + "name" to "Geyser", + "version" to project.version, + "description" to project.description, + "url" to "https://geysermc.org", + "author" to "GeyserMC" + ) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index dfdff2187..7f700a2f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,41 +1,5 @@ plugins { - `java-library` // Ensure AP works in eclipse (no effect on other IDEs) eclipse - id("geyser.build-logic") - alias(libs.plugins.lombok) apply false + id("geyser.base-conventions") } - -allprojects { - group = properties["group"] as String + "." + properties["id"] as String - version = properties["version"] as String - description = properties["description"] as String -} - -val basePlatforms = setOf( - projects.bungeecord, - projects.spigot, - projects.standalone, - projects.velocity, - projects.viaproxy -).map { it.dependencyProject } - -val moddedPlatforms = setOf( - projects.fabric, - projects.neoforge, - projects.mod -).map { it.dependencyProject } - -subprojects { - apply { - plugin("java-library") - plugin("io.freefair.lombok") - plugin("geyser.build-logic") - } - - when (this) { - in basePlatforms -> plugins.apply("geyser.platform-conventions") - in moddedPlatforms -> plugins.apply("geyser.modded-conventions") - else -> plugins.apply("geyser.base-conventions") - } -} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index efba08c8d..166ffe9f5 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("geyser.publish-conventions") + id("io.freefair.lombok") } dependencies { diff --git a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java index 406204759..1a92f9c40 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java +++ b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -39,15 +39,19 @@ public enum DeviceOs { OSX("macOS"), AMAZON("Amazon"), GEARVR("Gear VR"), - HOLOLENS("Hololens"), + @Deprecated HOLOLENS("Hololens"), UWP("Windows"), WIN32("Windows x86"), DEDICATED("Dedicated"), - TVOS("Apple TV"), - PS4("PS4"), + @Deprecated TVOS("Apple TV"), + /** + * This is for all PlayStation platforms not just PS4 + */ + PS4("PlayStation"), NX("Switch"), - XBOX("Xbox One"), - WINDOWS_PHONE("Windows Phone"); + XBOX("Xbox"), + @Deprecated WINDOWS_PHONE("Windows Phone"), + LINUX("Linux"); private static final DeviceOs[] VALUES = values(); diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 054f4f0ae..5c7f34499 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,11 +1,16 @@ -import net.kyori.blossom.BlossomExtension - plugins { + // Allow blossom to mark sources root of templates + idea alias(libs.plugins.blossom) id("geyser.publish-conventions") + id("io.freefair.lombok") } dependencies { + constraints { + implementation(libs.raknet) // Ensure protocol does not override the RakNet version + } + api(projects.common) api(projects.api) @@ -20,39 +25,42 @@ dependencies { implementation(libs.websocket) api(libs.bundles.protocol) - implementation(libs.blockstateupdater) - api(libs.mcauthlib) + api(libs.minecraftauth) api(libs.mcprotocollib) { exclude("io.netty", "netty-all") - exclude("com.github.GeyserMC", "packetlib") - exclude("com.github.GeyserMC", "mcauthlib") + exclude("net.raphimc", "MinecraftAuth") } implementation(libs.raknet) { exclude("io.netty", "*") } - implementation(libs.netty.resolver.dns) - implementation(libs.netty.resolver.dns.native.macos) { artifact { classifier = "osx-x86_64" } } - implementation(libs.netty.codec.haproxy) // Network dependencies we are updating ourselves api(libs.netty.handler) + implementation(libs.netty.codec.haproxy) - implementation(libs.netty.transport.native.epoll) { artifact { classifier = "linux-x86_64" } } + api(libs.netty.transport.native.epoll) { artifact { classifier = "linux-x86_64" } } implementation(libs.netty.transport.native.epoll) { artifact { classifier = "linux-aarch_64" } } + // kqueue is macos only implementation(libs.netty.transport.native.kqueue) { artifact { classifier = "osx-x86_64" } } + api(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-x86_64" } } + implementation(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-aarch_64" } } // Adventure text serialization api(libs.bundles.adventure) + // command library + api(libs.cloud.core) + api(libs.erosion.common) { isTransitive = false } // Test testImplementation(libs.junit) + testImplementation(libs.mockito) // Annotation Processors compileOnly(projects.ap) @@ -62,11 +70,6 @@ dependencies { api(libs.events) } -configurations.api { - // This is still experimental - additionally, it could only really benefit standalone - exclude(group = "io.netty.incubator", module = "netty-incubator-transport-native-io_uring") -} - tasks.processResources { // This is solely for backwards compatibility for other programs that used this file before the switch to gradle. // It used to be generated by the maven Git-Commit-Id-Plugin @@ -75,7 +78,7 @@ tasks.processResources { expand( "branch" to info.branch, "buildNumber" to info.buildNumber, - "projectVersion" to project.version, + "projectVersion" to info.version, "commit" to info.commit, "commitAbbrev" to info.commitAbbrev, "commitMessage" to info.commitMessage, @@ -84,20 +87,26 @@ tasks.processResources { } } -configure { - val mainFile = "src/main/java/org/geysermc/geyser/GeyserImpl.java" - val info = GitInfo() - - replaceToken("\${version}", "${project.version} (${info.gitVersion})", mainFile) - replaceToken("\${gitVersion}", info.gitVersion, mainFile) - replaceToken("\${buildNumber}", info.buildNumber, mainFile) - replaceToken("\${branch}", info.branch, mainFile) - replaceToken("\${commit}", info.commit, mainFile) - replaceToken("\${repository}", info.repository, mainFile) +sourceSets { + main { + blossom { + val info = GitInfo() + javaSources { + property("version", info.version) + property("gitVersion", info.gitVersion) + property("buildNumber", info.buildNumber.toString()) + property("branch", info.branch) + property("commit", info.commit) + property("repository", info.repository) + property("devVersion", info.isDev.toString()) + } + } + } } -fun Project.buildNumber(): Int = - (System.getenv("GITHUB_RUN_NUMBER") ?: jenkinsBuildNumber())?.let { Integer.parseInt(it) } ?: -1 +fun isDevBuild(branch: String, repository: String): Boolean { + return branch != "master" || repository.equals("https://github.com/GeyserMC/Geyser", ignoreCase = true).not() +} inner class GitInfo { val branch: String @@ -111,40 +120,40 @@ inner class GitInfo { val commitMessage: String val repository: String + val isDev: Boolean + init { - // On Jenkins, a detached head is checked out, so indra cannot determine the branch. - // Fortunately, this environment variable is available. - branch = indraGit.branchName() ?: System.getenv("BRANCH_NAME") ?: "DEV" + branch = indraGit.branchName() ?: "DEV" val commit = indraGit.commit() this.commit = commit?.name ?: "0".repeat(40) commitAbbrev = commit?.name?.substring(0, 7) ?: "0".repeat(7) gitVersion = "git-${branch}-${commitAbbrev}" - version = "${project.version} ($gitVersion)" - buildNumber = buildNumber() val git = indraGit.git() commitMessage = git?.commit()?.message ?: "" repository = git?.repository?.config?.getString("remote", "origin", "url") ?: "" + + buildNumber = buildNumber() + isDev = isDevBuild(branch, repository) + val projectVersion = if (isDev) project.version else projectVersion(project) + version = "$projectVersion ($gitVersion)" } } -// todo remove this when we're not using Jenkins anymore -fun jenkinsBuildNumber(): String? = System.getenv("BUILD_NUMBER") - // Manual task to download the bedrock data files from the CloudburstMC/Data repository // Invoke with ./gradlew :core:downloadBedrockData --suffix=1_20_70 // Set suffix to the current Bedrock version tasks.register("downloadBedrockData") { urls = listOf( "https://raw.githubusercontent.com/CloudburstMC/Data/master/entity_identifiers.dat", - "https://raw.githubusercontent.com/CloudburstMC/Data/master/biome_definitions.dat", "https://raw.githubusercontent.com/CloudburstMC/Data/master/block_palette.nbt", "https://raw.githubusercontent.com/CloudburstMC/Data/master/creative_items.json", - "https://raw.githubusercontent.com/CloudburstMC/Data/master/runtime_item_states.json" + "https://raw.githubusercontent.com/CloudburstMC/Data/master/runtime_item_states.json", + "https://raw.githubusercontent.com/CloudburstMC/Data/master/stripped_biome_definitions.json" ) suffixedFiles = listOf("block_palette.nbt", "creative_items.json", "runtime_item_states.json") destinationDir = "$projectDir/src/main/resources/bedrock" -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/DoubleChestValue.java b/core/src/main/java-templates/org/geysermc/geyser/BuildData.java similarity index 66% rename from core/src/main/java/org/geysermc/geyser/level/block/DoubleChestValue.java rename to core/src/main/java-templates/org/geysermc/geyser/BuildData.java index 97c861df7..0e4d08bfe 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/DoubleChestValue.java +++ b/core/src/main/java-templates/org/geysermc/geyser/BuildData.java @@ -23,18 +23,20 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.level.block; +package org.geysermc.geyser; -/** - * This stores all values of double chests that are part of the Java block state. - * - * @param isFacingEast If true, then chest is facing east/west; if false, south/north - * @param isDirectionPositive If true, direction is positive (east/south); if false, direction is negative (west/north) - * @param isLeft If true, chest is the left of a pair; if false, chest is the right of a pair. - */ -public record DoubleChestValue( - boolean isFacingEast, - boolean isDirectionPositive, - boolean isLeft) { +// The constants are replaced before compilation +public class BuildData { + public static final String GIT_VERSION = "{{ gitVersion }}"; + public static final String VERSION = "{{ version }}"; + public static final String BUILD_NUMBER = "{{ buildNumber }}"; + public static final String BRANCH = "{{ branch }}"; + public static final String COMMIT = "{{ commit }}"; + public static final String REPOSITORY = "{{ repository }}"; + private static final String DEV = "{{ devVersion }}"; + + public static boolean isDevBuild() { + return Boolean.parseBoolean(DEV); + } } diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 5de8e6e6b..2b58e7e84 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -34,12 +34,9 @@ public final class Constants { public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/"; public static final String NEWS_PROJECT_NAME = "geyser"; - public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/"; - + public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://geysermc.org/download#floodgate"; public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download"; - public static final String UPDATE_PERMISSION = "geyser.update"; - - static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; + static final String SAVED_AUTH_CHAINS_FILE = "saved-auth-chains.json"; public static final String GEYSER_CUSTOM_NAMESPACE = "geyser_custom"; @@ -54,4 +51,4 @@ public final class Constants { } GLOBAL_API_WS_URI = wsUri; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java index a9414d9d0..c5b271593 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java @@ -27,7 +27,7 @@ package org.geysermc.geyser; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.GeyserWorldManager; @@ -82,11 +82,11 @@ public interface GeyserBootstrap { GeyserLogger getGeyserLogger(); /** - * Returns the current CommandManager + * Returns the current CommandRegistry * - * @return The current CommandManager + * @return The current CommandRegistry */ - GeyserCommandManager getGeyserCommandManager(); + CommandRegistry getCommandRegistry(); /** * Returns the current PingPassthrough manager @@ -149,6 +149,11 @@ public interface GeyserBootstrap { return Paths.get("logs/latest.log"); } + /** + * @return the name of the server platform Geyser is running on. + */ + @NonNull String getServerPlatform(); + /** * Get an InputStream for the given resource path. * Overridden on platforms that have different class loader properties. diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 8a25c3115..f5578d6a6 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -29,7 +29,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.steveice10.packetlib.tcp.TcpSession; import io.netty.channel.epoll.Epoll; import io.netty.util.NettyRuntime; import io.netty.util.concurrent.DefaultThreadFactory; @@ -42,7 +41,7 @@ import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; +import org.cloudburstmc.protocol.bedrock.util.EncryptionUtils; import org.geysermc.api.Geyser; import org.geysermc.cumulus.form.Form; import org.geysermc.cumulus.form.util.FormBuilder; @@ -54,38 +53,53 @@ import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.news.NewsItemAction; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.command.CommandSource; -import org.geysermc.geyser.api.event.EventBus; import org.geysermc.geyser.api.event.EventRegistrar; -import org.geysermc.geyser.api.event.lifecycle.*; +import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPostReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPreReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.api.util.MinecraftVersion; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.erosion.UnixSocketClientListener; import org.geysermc.geyser.event.GeyserEventBus; +import org.geysermc.geyser.event.type.SessionDisconnectEventImpl; import org.geysermc.geyser.extension.GeyserExtensionManager; import org.geysermc.geyser.impl.MinecraftVersionImpl; +import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.netty.GeyserServer; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.PendingMicrosoftAuthentication; +import org.geysermc.geyser.session.SessionDisconnectListener; import org.geysermc.geyser.session.SessionManager; +import org.geysermc.geyser.session.cache.RegistryCache; import org.geysermc.geyser.skin.FloodgateSkinUploader; import org.geysermc.geyser.skin.ProvidedSkins; import org.geysermc.geyser.skin.SkinProvider; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.*; +import org.geysermc.geyser.util.AssetUtils; +import org.geysermc.geyser.util.CodeOfConductManager; +import org.geysermc.geyser.util.CooldownUtils; +import org.geysermc.geyser.util.Metrics; +import org.geysermc.geyser.util.NewsHandler; +import org.geysermc.geyser.util.VersionCheckUtils; +import org.geysermc.geyser.util.WebUtils; import java.io.File; import java.io.FileWriter; @@ -96,16 +110,23 @@ import java.net.UnknownHostException; import java.nio.file.Path; import java.security.Key; import java.text.DecimalFormat; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @Getter -public class GeyserImpl implements GeyserApi { +public class GeyserImpl implements GeyserApi, EventRegistrar { public static final ObjectMapper JSON_MAPPER = new ObjectMapper() .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) @@ -114,29 +135,24 @@ public class GeyserImpl implements GeyserApi { .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); public static final String NAME = "Geyser"; - public static final String GIT_VERSION = "${gitVersion}"; - public static final String VERSION = "${version}"; + public static final String GIT_VERSION = BuildData.GIT_VERSION; + public static final String VERSION = BuildData.VERSION; - public static final String BUILD_NUMBER = "${buildNumber}"; - public static final String BRANCH = "${branch}"; - public static final String COMMIT = "${commit}"; - public static final String REPOSITORY = "${repository}"; + public static final String BUILD_NUMBER = BuildData.BUILD_NUMBER; + public static final String BRANCH = BuildData.BRANCH; + public static final String COMMIT = BuildData.COMMIT; + public static final String REPOSITORY = BuildData.REPOSITORY; + public static final boolean IS_DEV = BuildData.isDevBuild(); /** * Oauth client ID for Microsoft authentication */ public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88"; - private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; + private static final Pattern IP_REGEX = Pattern.compile("\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"); private final SessionManager sessionManager = new SessionManager(); - /** - * This is used in GeyserConnect to stop the bedrock server binding to a port - */ - @Setter - private static boolean shouldStartListener = true; - private FloodgateCipher cipher; private FloodgateSkinUploader skinUploader; private NewsHandler newsHandler; @@ -152,14 +168,14 @@ public class GeyserImpl implements GeyserApi { private final PlatformType platformType; private final GeyserBootstrap bootstrap; - private final EventBus eventBus; + private final GeyserEventBus eventBus; private final GeyserExtensionManager extensionManager; private Metrics metrics; private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; @Getter(AccessLevel.NONE) - private Map savedRefreshTokens; + private Map savedAuthChains; @Getter private static GeyserImpl instance; @@ -167,7 +183,8 @@ public class GeyserImpl implements GeyserApi { /** * Determines if we're currently reloading. Replaces per-bootstrap reload checks */ - private volatile boolean isReloading; + @Setter + private boolean isReloading; /** * Determines if Geyser is currently enabled. This is used to determine if {@link #disable()} should be called during {@link #shutdown()}. @@ -198,6 +215,9 @@ public class GeyserImpl implements GeyserApi { } public void initialize() { + // Setup encryption early so we don't start if we can't auth + EncryptionUtils.getMojangPublicKey(); + long startupTime = System.currentTimeMillis(); GeyserLogger logger = bootstrap.getGeyserLogger(); @@ -206,11 +226,22 @@ public class GeyserImpl implements GeyserApi { logger.info(""); logger.info(GeyserLocale.getLocaleStringLog("geyser.core.load", NAME, VERSION)); logger.info(""); + if (IS_DEV) { + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc")); + logger.info(""); + } logger.info("******************************************"); - /* Initialize registries */ - Registries.init(); - BlockRegistries.init(); + /* + First load the registries and then populate them. + Both the block registries and the common registries depend on each other, + so maintaining this order is crucial for Geyser to load. + */ + Registries.load(); + BlockRegistries.populate(); + Registries.populate(); + + RegistryCache.init(); /* Initialize translators */ EntityDefinitions.init(); @@ -234,6 +265,11 @@ public class GeyserImpl implements GeyserApi { CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks); }); + // Register our general permissions when possible + eventBus.subscribe(this, GeyserRegisterPermissionsEvent.class, Permissions::register); + // Replace disconnect messages whenever necessary + eventBus.subscribe(this, SessionDisconnectEventImpl.class, SessionDisconnectListener::onSessionDisconnect); + /* Call Registry events */ Registries.callRegistryEvents(); @@ -264,7 +300,10 @@ public class GeyserImpl implements GeyserApi { if (isReloading) { // If we're reloading, the default locale in the config might have changed. GeyserLocale.finalizeDefaultLocale(this); + } else { + CodeOfConductManager.load(); } + GeyserLogger logger = bootstrap.getGeyserLogger(); GeyserConfiguration config = bootstrap.getGeyserConfig(); @@ -338,22 +377,6 @@ public class GeyserImpl implements GeyserApi { } } - String broadcastPort = System.getProperty("geyserBroadcastPort", ""); - if (!broadcastPort.isEmpty()) { - int parsedPort; - try { - parsedPort = Integer.parseInt(broadcastPort); - if (parsedPort < 1 || parsedPort > 65535) { - throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!"); - } - } catch (NumberFormatException e) { - logger.error(String.format("Invalid broadcast port: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")")); - parsedPort = config.getBedrock().port(); - } - config.getBedrock().setBroadcastPort(parsedPort); - logger.info("Broadcast port set from system property: " + parsedPort); - } - if (platformType != PlatformType.VIAPROXY) { boolean floodgatePresent = bootstrap.testFloodgatePluginPresent(); if (config.getRemote().authType() == AuthType.FLOODGATE && !floodgatePresent) { @@ -368,9 +391,29 @@ public class GeyserImpl implements GeyserApi { } } + // Now that the Bedrock port may have been changed, also check the broadcast port (configurable on all platforms) + String broadcastPort = System.getProperty("geyserBroadcastPort", ""); + if (!broadcastPort.isEmpty()) { + try { + int parsedPort = Integer.parseInt(broadcastPort); + if (parsedPort < 1 || parsedPort > 65535) { + throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!"); + } + config.getBedrock().setBroadcastPort(parsedPort); + logger.info("Broadcast port set from system property: " + parsedPort); + } catch (NumberFormatException e) { + logger.error(String.format("Invalid broadcast port from system property: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")")); + } + } + + // It's set to 0 only if no system property or manual config value was set + if (config.getBedrock().broadcastPort() == 0) { + config.getBedrock().setBroadcastPort(config.getBedrock().port()); + } + String remoteAddress = config.getRemote().address(); // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. - if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { + if (!IP_REGEX.matcher(remoteAddress).matches() && !remoteAddress.equalsIgnoreCase("localhost")) { String[] record = WebUtils.findSrvRecord(this, remoteAddress); if (record != null) { int remotePort = Integer.parseInt(record[2]); @@ -380,9 +423,6 @@ public class GeyserImpl implements GeyserApi { } } - // Ensure that PacketLib does not create an event loop for handling packets; we'll do that ourselves - TcpSession.USE_EVENT_LOOP_FOR_PACKETS = false; - pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout()); this.newsHandler = new NewsHandler(BRANCH, this.buildNumber()); @@ -396,7 +436,7 @@ public class GeyserImpl implements GeyserApi { } CooldownUtils.setDefaultShowCooldown(config.getShowCooldown()); - DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether + BedrockDimension.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether Integer bedrockThreadCount = Integer.getInteger("Geyser.BedrockNetworkThreads"); if (bedrockThreadCount == null) { @@ -404,24 +444,27 @@ public class GeyserImpl implements GeyserApi { bedrockThreadCount = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); } - if (shouldStartListener) { - this.geyserServer = new GeyserServer(this, bedrockThreadCount); - this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port())) - .whenComplete((avoid, throwable) -> { - if (throwable == null) { - logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().address(), - String.valueOf(config.getBedrock().port()))); + this.geyserServer = new GeyserServer(this, bedrockThreadCount); + this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port())) + .whenComplete((avoid, throwable) -> { + String address = config.getBedrock().address(); + String port = String.valueOf(config.getBedrock().port()); // otherwise we get commas + + if (throwable == null) { + if ("0.0.0.0".equals(address)) { + // basically just hide it in the log because some people get confused and try to change it + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start.ip_suppressed", port)); } else { - String address = config.getBedrock().address(); - int port = config.getBedrock().port(); - logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port))); - if (!"0.0.0.0".equals(address)) { - logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN)); - logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN)); - } + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", address, port)); } - }).join(); - } + } else { + logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, port)); + if (!"0.0.0.0".equals(address)) { + logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN)); + logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN)); + } + } + }).join(); if (config.getRemote().authType() == AuthType.FLOODGATE) { try { @@ -442,9 +485,22 @@ public class GeyserImpl implements GeyserApi { metrics.addCustomChart(new Metrics.SingleLineChart("players", sessionManager::size)); // Prevent unwanted words best we can metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().authType().toString().toLowerCase(Locale.ROOT))); - metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::platformName)); + + Map> platformTypeMap = new HashMap<>(); + Map serverPlatform = new HashMap<>(); + serverPlatform.put(bootstrap.getServerPlatform(), 1); + platformTypeMap.put(platformType().platformName(), serverPlatform); + + metrics.addCustomChart(new Metrics.DrilldownPie("platform", () -> { + // By the end, we should return, for example: + // Geyser-Spigot => (Paper, 1) + return platformTypeMap; + })); + metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", GeyserLocale::getDefaultLocale)); metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserImpl.VERSION)); + metrics.addCustomChart(new Metrics.SimplePie("javaHaProxyProtocol", () -> String.valueOf(config.getRemote().isUseProxyProtocol()))); + metrics.addCustomChart(new Metrics.SimplePie("bedrockHaProxyProtocol", () -> String.valueOf(config.getBedrock().isEnableProxyProtocol()))); metrics.addCustomChart(new Metrics.AdvancedPie("playerPlatform", () -> { Map valueMap = new HashMap<>(); for (GeyserSession session : sessionManager.getAllSessions()) { @@ -478,7 +534,7 @@ public class GeyserImpl implements GeyserApi { if (minecraftVersion != null) { Map> versionMap = new HashMap<>(); Map platformMap = new HashMap<>(); - platformMap.put(platformType.platformName(), 1); + platformMap.put(bootstrap.getServerPlatform(), 1); versionMap.put(minecraftVersion, platformMap); metrics.addCustomChart(new Metrics.DrilldownPie("minecraftServerVersion", () -> { @@ -527,37 +583,37 @@ public class GeyserImpl implements GeyserApi { if (config.getRemote().authType() == AuthType.ONLINE) { // May be written/read to on multiple threads from each GeyserSession as well as writing the config - savedRefreshTokens = new ConcurrentHashMap<>(); + savedAuthChains = new ConcurrentHashMap<>(); - File tokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); - if (tokensFile.exists()) { + File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); + if (authChainsFile.exists()) { TypeReference> type = new TypeReference<>() { }; - Map refreshTokenFile = null; + Map authChainFile = null; try { - refreshTokenFile = JSON_MAPPER.readValue(tokensFile, type); + authChainFile = JSON_MAPPER.readValue(authChainsFile, type); } catch (IOException e) { logger.error("Cannot load saved user tokens!", e); } - if (refreshTokenFile != null) { + if (authChainFile != null) { List validUsers = config.getSavedUserLogins(); boolean doWrite = false; - for (Map.Entry entry : refreshTokenFile.entrySet()) { + for (Map.Entry entry : authChainFile.entrySet()) { String user = entry.getKey(); if (!validUsers.contains(user)) { // Perform a write to this file to purge the now-unused name doWrite = true; continue; } - savedRefreshTokens.put(user, entry.getValue()); + savedAuthChains.put(user, entry.getValue()); } if (doWrite) { - scheduleRefreshTokensWrite(); + scheduleAuthChainsWrite(); } } } } else { - savedRefreshTokens = null; + savedAuthChains = null; } newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); @@ -638,18 +694,14 @@ public class GeyserImpl implements GeyserApi { bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.kick.done")); } - scheduledThread.shutdown(); - geyserServer.shutdown(); - if (skinUploader != null) { - skinUploader.close(); - } - newsHandler.shutdown(); + runIfNonNull(scheduledThread, ScheduledExecutorService::shutdown); + runIfNonNull(geyserServer, GeyserServer::shutdown); + runIfNonNull(skinUploader, FloodgateSkinUploader::close); + runIfNonNull(newsHandler, NewsHandler::shutdown); + runIfNonNull(erosionUnixListener, UnixSocketClientListener::close); - if (this.erosionUnixListener != null) { - this.erosionUnixListener.close(); - } - - Registries.RESOURCE_PACKS.get().clear(); + ResourcePackLoader.clear(); + CodeOfConductManager.getInstance().save(); this.setEnabled(false); } @@ -659,7 +711,6 @@ public class GeyserImpl implements GeyserApi { if (isEnabled) { this.disable(); } - this.commandManager().getCommands().clear(); // Disable extensions, fire the shutdown event this.eventBus.fire(new GeyserShutdownEvent(this.extensionManager, this.eventBus)); @@ -684,9 +735,10 @@ public class GeyserImpl implements GeyserApi { * * @return true if the version number is not 'DEV'. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isProductionEnvironment() { // First is if Blossom runs, second is if Blossom doesn't run - //noinspection ConstantConditions,MismatchedStringCase - changes in production + //noinspection ConstantConditions - changes in production return !("git-local/dev-0000000".equals(GeyserImpl.GIT_VERSION) || "${gitVersion}".equals(GeyserImpl.GIT_VERSION)); } @@ -696,9 +748,12 @@ public class GeyserImpl implements GeyserApi { return this.extensionManager; } + /** + * @return the current CommandRegistry in use. The instance may change over the lifecycle of the Geyser runtime. + */ @NonNull - public GeyserCommandManager commandManager() { - return this.bootstrap.getGeyserCommandManager(); + public CommandRegistry commandRegistry() { + return this.bootstrap.getCommandRegistry(); } @Override @@ -713,7 +768,7 @@ public class GeyserImpl implements GeyserApi { @Override @NonNull - public EventBus eventBus() { + public GeyserEventBus eventBus() { return this.eventBus; } @@ -753,11 +808,7 @@ public class GeyserImpl implements GeyserApi { @Override public @NonNull List supportedBedrockVersions() { - ArrayList versions = new ArrayList<>(); - for (BedrockCodec codec : GameProtocol.SUPPORTED_BEDROCK_CODECS) { - versions.add(new MinecraftVersionImpl(codec.getMinecraftVersion(), codec.getProtocolVersion())); - } - return Collections.unmodifiableList(versions); + return Collections.unmodifiableList(GameProtocol.SUPPORTED_BEDROCK_VERSIONS); } @Override @@ -770,6 +821,7 @@ public class GeyserImpl implements GeyserApi { return 0; } + //noinspection DataFlowIssue return Integer.parseInt(BUILD_NUMBER); } @@ -807,11 +859,11 @@ public class GeyserImpl implements GeyserApi { } @Nullable - public String refreshTokenFor(@NonNull String bedrockName) { - return savedRefreshTokens.get(bedrockName); + public String authChainFor(@NonNull String bedrockName) { + return savedAuthChains.get(bedrockName); } - public void saveRefreshToken(@NonNull String bedrockName, @NonNull String refreshToken) { + public void saveAuthChain(@NonNull String bedrockName, @NonNull String authChain) { if (!getConfig().getSavedUserLogins().contains(bedrockName)) { // Do not save this login return; @@ -819,20 +871,26 @@ public class GeyserImpl implements GeyserApi { // We can safely overwrite old instances because MsaAuthenticationService#getLoginResponseFromRefreshToken // refreshes the token for us - if (!Objects.equals(refreshToken, savedRefreshTokens.put(bedrockName, refreshToken))) { - scheduleRefreshTokensWrite(); + if (!Objects.equals(authChain, savedAuthChains.put(bedrockName, authChain))) { + scheduleAuthChainsWrite(); } } - private void scheduleRefreshTokensWrite() { + private void runIfNonNull(T nullable, Consumer consumer) { + if (nullable != null) { + consumer.accept(nullable); + } + } + + private void scheduleAuthChainsWrite() { scheduledThread.execute(() -> { // Ensure all writes are handled on the same thread - File savedTokens = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + File savedAuthChains = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); TypeReference> type = new TypeReference<>() { }; - try (FileWriter writer = new FileWriter(savedTokens)) { + try (FileWriter writer = new FileWriter(savedAuthChains)) { JSON_MAPPER.writerFor(type) .withDefaultPrettyPrinter() - .writeValue(writer, savedRefreshTokens); + .writeValue(writer, this.savedAuthChains); } catch (IOException e) { getLogger().error("Unable to write saved refresh tokens!", e); } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index aa79e3630..5452d6d29 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -29,7 +29,9 @@ import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import java.util.UUID; public interface GeyserLogger extends GeyserCommandSource { @@ -102,6 +104,15 @@ public interface GeyserLogger extends GeyserCommandSource { debug(String.valueOf(object)); } + /** + * Logs and formats a message to console if debug mode is enabled, + * with the provided arguments. + * + * @param message the message to log + * @param arguments the arguments to replace in the message + */ + void debug(String message, Object... arguments); + /** * Sets if the logger should print debug messages * @@ -109,6 +120,15 @@ public interface GeyserLogger extends GeyserCommandSource { */ void setDebug(boolean debug); + /** + * A method to debug information specific to a session. + */ + default void debug(GeyserSession session, String message, Object... arguments) { + if (isDebug()) { + debug("(" + session.bedrockUsername() + ") " + message, arguments); + } + } + /** * If debug is enabled for this logger */ @@ -129,6 +149,11 @@ public interface GeyserLogger extends GeyserCommandSource { return true; } + @Override + default @Nullable UUID playerUuid() { + return null; + } + @Override default boolean hasPermission(String permission) { return true; diff --git a/core/src/main/java/org/geysermc/geyser/Permissions.java b/core/src/main/java/org/geysermc/geyser/Permissions.java new file mode 100644 index 000000000..b65a5af7a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/Permissions.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser; + +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; + +import java.util.HashMap; +import java.util.Map; + +/** + * Permissions related to Geyser + */ +public final class Permissions { + private static final Map PERMISSIONS = new HashMap<>(); + + public static final String CHECK_UPDATE = register("geyser.update"); + public static final String SERVER_SETTINGS = register("geyser.settings.server"); + public static final String SETTINGS_GAMERULES = register("geyser.settings.gamerules"); + + private Permissions() { + //no + } + + private static String register(String permission) { + return register(permission, TriState.NOT_SET); + } + + @SuppressWarnings("SameParameterValue") + private static String register(String permission, TriState permissionDefault) { + PERMISSIONS.put(permission, permissionDefault); + return permission; + } + + public static void register(GeyserRegisterPermissionsEvent event) { + for (Map.Entry permission : PERMISSIONS.entrySet()) { + event.register(permission.getKey(), permission.getValue()); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java new file mode 100644 index 000000000..75ea81ff9 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.protocol.bedrock.data.command.CommandData; +import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint; +import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData; +import org.cloudburstmc.protocol.bedrock.data.command.CommandOverloadData; +import org.cloudburstmc.protocol.bedrock.data.command.CommandParam; +import org.cloudburstmc.protocol.bedrock.data.command.CommandParamData; +import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.event.EventRegistrar; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand; +import org.geysermc.geyser.command.defaults.AdvancementsCommand; +import org.geysermc.geyser.command.defaults.ConnectionTestCommand; +import org.geysermc.geyser.command.defaults.CustomOptionsCommand; +import org.geysermc.geyser.command.defaults.DumpCommand; +import org.geysermc.geyser.command.defaults.ExtensionsCommand; +import org.geysermc.geyser.command.defaults.HelpCommand; +import org.geysermc.geyser.command.defaults.ListCommand; +import org.geysermc.geyser.command.defaults.OffhandCommand; +import org.geysermc.geyser.command.defaults.PingCommand; +import org.geysermc.geyser.command.defaults.QuickActionsCommand; +import org.geysermc.geyser.command.defaults.ReloadCommand; +import org.geysermc.geyser.command.defaults.SettingsCommand; +import org.geysermc.geyser.command.defaults.StatisticsCommand; +import org.geysermc.geyser.command.defaults.StopCommand; +import org.geysermc.geyser.command.defaults.VersionCommand; +import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl; +import org.geysermc.geyser.extension.command.GeyserExtensionCommand; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.Command.Builder; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.internal.CommandNode; +import org.incendo.cloud.parser.standard.EnumParser; +import org.incendo.cloud.parser.standard.IntegerParser; +import org.incendo.cloud.parser.standard.LiteralParser; +import org.incendo.cloud.parser.standard.StringArrayParser; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.Suggestions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static org.geysermc.geyser.command.GeyserCommand.DEFAULT_ROOT_COMMAND; + +/** + * Registers all built-in and extension commands to the given Cloud CommandManager. + *

+ * Fires {@link GeyserDefineCommandsEvent} upon construction. + *

+ * Subscribes to {@link GeyserRegisterPermissionsEvent} upon construction. + * A new instance of this class (that registers the same permissions) shouldn't be created until the previous + * instance is unsubscribed from the event. + */ +public class CommandRegistry implements EventRegistrar { + + private static final String GEYSER_ROOT_PERMISSION = "geyser.command"; + + public final static boolean STANDALONE_COMMAND_MANAGER = GeyserImpl.getInstance().getPlatformType() == PlatformType.STANDALONE || + GeyserImpl.getInstance().getPlatformType() == PlatformType.VIAPROXY; + + protected final GeyserImpl geyser; + private final CommandManager cloud; + private final boolean applyRootPermission; + + /** + * Map of Geyser subcommands to their Commands + */ + private final Map commands = new Object2ObjectOpenHashMap<>(13); + + /** + * Map of Extensions to maps of their subcommands + */ + private final Map> extensionCommands = new Object2ObjectOpenHashMap<>(0); + + /** + * Map of root commands (that are for extensions) to Extensions + */ + private final Map extensionRootCommands = new Object2ObjectOpenHashMap<>(0); + + /** + * Map containing only permissions that have been registered with a default value + */ + protected final Map permissionDefaults = new Object2ObjectOpenHashMap<>(13); + + /** + * Creates a new CommandRegistry. Does apply a root permission. If undesired, use the other constructor. + */ + public CommandRegistry(GeyserImpl geyser, CommandManager cloud) { + this(geyser, cloud, true); + } + + /** + * Creates a new CommandRegistry + * + * @param geyser the Geyser instance + * @param cloud the cloud command manager to register commands to + * @param applyRootPermission true if this registry should apply a permission to Geyser and Extension root commands. + * This currently exists because we want to retain the root command permission for Spigot, + * but don't want to add it yet to platforms like Velocity where we cannot natively + * specify a permission default. Doing so will break setups as players would suddenly not + * have the required permission to execute any Geyser commands. + */ + public CommandRegistry(GeyserImpl geyser, CommandManager cloud, boolean applyRootPermission) { + this.geyser = geyser; + this.cloud = cloud; + this.applyRootPermission = applyRootPermission; + + // register our custom exception handlers + ExceptionHandlers.register(cloud); + + // begin command registration + HelpCommand help = new HelpCommand(DEFAULT_ROOT_COMMAND, "help", "geyser.commands.help.desc", "geyser.command.help", this.commands); + registerBuiltInCommand(help); + buildRootCommand(GEYSER_ROOT_PERMISSION, help); // build root and delegate to help + + registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list")); + registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload")); + registerBuiltInCommand(new OffhandCommand("offhand", "geyser.commands.offhand.desc", "geyser.command.offhand")); + registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump")); + registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version")); + registerBuiltInCommand(new SettingsCommand("settings", "geyser.commands.settings.desc", "geyser.command.settings")); + registerBuiltInCommand(new StatisticsCommand("statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); + registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); + registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); + registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); + registerBuiltInCommand(new PingCommand("ping", "geyser.commands.ping.desc", "geyser.command.ping")); + registerBuiltInCommand(new CustomOptionsCommand("options", "geyser.commands.options.desc", "geyser.command.options")); + registerBuiltInCommand(new QuickActionsCommand("quickactions", "geyser.commands.quickactions.desc", "geyser.command.quickactions")); + + if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { + registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); + } + + if (!this.geyser.extensionManager().extensions().isEmpty()) { + registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions")); + } + + GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) { + + @Override + public void register(@NonNull Command command) { + if (!(command instanceof GeyserExtensionCommand extensionCommand)) { + throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?"); + } + + registerExtensionCommand(extensionCommand.extension(), extensionCommand); + } + }; + this.geyser.eventBus().fire(defineCommandsEvent); + + // Stuff that needs to be done on a per-extension basis + for (Map.Entry> entry : this.extensionCommands.entrySet()) { + Extension extension = entry.getKey(); + + // Register this extension's root command + extensionRootCommands.put(extension.rootCommand(), extension); + + // Register help commands for all extensions with commands + String id = extension.description().id(); + HelpCommand extensionHelp = new HelpCommand( + extension.rootCommand(), + "help", + "geyser.commands.exthelp.desc", + "geyser.command.exthelp." + id, + entry.getValue()); // commands it provides help for + + registerExtensionCommand(extension, extensionHelp); + buildRootCommand("geyser.extension." + id + ".command", extensionHelp); + } + + // Wait for the right moment (depends on the platform) to register permissions. + geyser.eventBus().subscribe(this, GeyserRegisterPermissionsEvent.class, this::onRegisterPermissions); + } + + /** + * @return an immutable view of the root commands registered to this command registry + */ + @NonNull + public Collection rootCommands() { + return cloud.rootCommands(); + } + + /** + * For internal Geyser commands + */ + private void registerBuiltInCommand(GeyserCommand command) { + register(command, this.commands); + } + + private void registerExtensionCommand(@NonNull Extension extension, @NonNull GeyserCommand command) { + register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>())); + } + + protected void register(GeyserCommand command, Map commands) { + String root = command.rootCommand(); + String name = command.name(); + if (commands.containsKey(name)) { + throw new IllegalArgumentException("Command with root=%s, name=%s already registered".formatted(root, name)); + } + + command.register(cloud); + commands.put(name, command); + geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", root + " " + name)); + + for (String alias : command.aliases()) { + commands.put(alias, command); + } + + String permission = command.permission(); + TriState defaultValue = command.permissionDefault(); + if (!permission.isBlank() && defaultValue != null) { + + TriState existingDefault = permissionDefaults.get(permission); + // Extensions might be using the same permission for two different commands + if (existingDefault != null && existingDefault != defaultValue) { + geyser.getLogger().debug("Overriding permission default %s:%s with %s".formatted(permission, existingDefault, defaultValue)); + } + + permissionDefaults.put(permission, defaultValue); + } + } + + /** + * Registers a root command to cloud that delegates to the given help command. + * The name of this root command is the root of the given help command. + * + * @param permission the permission of the root command. currently, it may or may not be + * applied depending on the platform. see below. + * @param help the help command to delegate to + */ + private void buildRootCommand(String permission, HelpCommand help) { + Builder builder = cloud.commandBuilder(help.rootCommand()); + + if (applyRootPermission) { + builder = builder.permission(permission); + permissionDefaults.put(permission, TriState.TRUE); + } + + cloud.command(builder.handler(context -> { + GeyserCommandSource source = context.sender(); + if (source.hasPermission(help.permission())) { + // Delegate to help if possible + help.execute(source); + } else if (STANDALONE_COMMAND_MANAGER && source instanceof GeyserSession session) { + // If we are on an appropriate platform, forward the command to the backend + session.sendCommandPacket(context.rawInput().input()); + } else { + source.sendLocaleString(ExceptionHandlers.PERMISSION_FAIL_LANG_KEY); + } + })); + } + + protected void onRegisterPermissions(GeyserRegisterPermissionsEvent event) { + for (Map.Entry permission : permissionDefaults.entrySet()) { + event.register(permission.getKey(), permission.getValue()); + } + } + + public boolean hasPermission(GeyserCommandSource source, String permission) { + // Handle blank permissions ourselves, as cloud only handles empty ones + return permission.isBlank() || cloud.hasPermission(source, permission); + } + + /** + * Returns the description of the given command + * + * @param command the root command node + * @param locale the ideal locale that the description should be in + * @return a description if found, otherwise an empty string. The locale is not guaranteed. + */ + @NonNull + public String description(@NonNull String command, @NonNull String locale) { + if (command.equals(DEFAULT_ROOT_COMMAND)) { + return GeyserLocale.getPlayerLocaleString("geyser.command.root.geyser", locale); + } + + Extension extension = extensionRootCommands.get(command); + if (extension != null) { + return GeyserLocale.getPlayerLocaleString("geyser.command.root.extension", locale, extension.name()); + } + return ""; + } + + /** + * Dispatches a command into cloud and handles any thrown exceptions. + * This method may or may not be blocking, depending on the {@link ExecutionCoordinator} in use by cloud. + */ + public void runCommand(@NonNull GeyserCommandSource source, @NonNull String command) { + cloud.commandExecutor().executeCommand(source, command); + } + + public Suggestions suggestionsFor(GeyserCommandSource source, String input) { + return cloud.suggestionFactory().suggestImmediately(source, input); + } + + public void export(GeyserSession session, List bedrockCommands, Set knownAliases) { + cloud.commandTree().rootNodes().forEach(commandTree -> { + var command = commandTree.command(); + // Command null happens if you register an extension command with custom Cloud parameters... + if (command == null || session.hasPermission(command.commandPermission().permissionString())) { + var rootComponent = commandTree.component(); + String name = rootComponent.name(); + if (!knownAliases.add(name)) { + // If the server already defined the command, let's not crash. + return; + } + + LinkedHashMap> values = new LinkedHashMap<>(); + for (String s : rootComponent.aliases()) { + values.put(s, EnumSet.of(CommandEnumConstraint.ALLOW_ALIASES)); + } + CommandEnumData aliases = new CommandEnumData(name + "Aliases", values, false); + + List data = new ArrayList<>(); + for (var node : commandTree.children()) { + List> params = createParamData(session, node); + params.forEach(param -> data.add(new CommandOverloadData(false, param.toArray(CommandParamData[]::new)))); + } + + CommandData bedrockCommand = new CommandData(name, rootComponent.description().textDescription(), + Set.of(CommandData.Flag.NOT_CHEAT), CommandPermission.ANY, aliases, + Collections.emptyList(), data.toArray(new CommandOverloadData[0])); + bedrockCommands.add(bedrockCommand); + } + }); + } + + private List> createParamData(GeyserSession session, CommandNode node) { + var command = node.command(); + if (command != null && !session.hasPermission(command.commandPermission().permissionString())) { + // Triggers with subcommands like Geyser dump, stop, etc. + return Collections.emptyList(); + } + + CommandParamData data = new CommandParamData(); + var component = node.component(); + data.setName(component.name()); + data.setOptional(component.optional()); + var suggestionProvider = component.suggestionProvider(); + if (suggestionProvider instanceof LiteralParser parser) { + Map> values = new LinkedHashMap<>(); + for (String alias : parser.aliases()) { + values.put(alias, Set.of()); + } + + data.setEnumData(new CommandEnumData(component.name(), values, false)); + } else if (suggestionProvider instanceof IntegerParser) { + data.setType(CommandParam.INT); + } else if (suggestionProvider instanceof EnumParser parser) { + LinkedHashMap> map = new LinkedHashMap<>(); + for (Enum e : parser.acceptedValues()) { + map.put(e.name().toLowerCase(Locale.ROOT), Set.of()); + } + + data.setEnumData(new CommandEnumData(component.name().toLowerCase(Locale.ROOT), map, false)); + } else if (component.parser() instanceof StringArrayParser) { + data.setType(CommandParam.TEXT); + } else { + data.setType(CommandParam.STRING); + } + + var children = node.children(); + if (children.isEmpty()) { + List list = new ArrayList<>(); // Must be mutable; parents will be added to list. + list.add(data); + return Collections.singletonList(list); // Safe to do; will be consumed in an addAll call. + } + List> collectiveData = new ArrayList<>(); + // If a node has multiple children, this will need to be represented + // by creating a new list/branch for each and cloning this node down each line. + for (var child : children) { + collectiveData.addAll(createParamData(session, child)); + } + collectiveData.forEach(list -> list.add(0, data)); + return collectiveData; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java b/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java new file mode 100644 index 000000000..1fa5871e0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.SenderMapper; + +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Converts {@link GeyserCommandSource}s to the server's command sender type (and back) in a lenient manner. + * + * @param senderType class of the server command sender type + * @param playerLookup function for looking up a player command sender by UUID + * @param consoleProvider supplier of the console command sender + * @param commandSourceLookup supplier of the platform implementation of the {@link GeyserCommandSource} + * @param server command sender type + */ +public record CommandSourceConverter(Class senderType, + Function playerLookup, + Supplier consoleProvider, + Function commandSourceLookup +) implements SenderMapper { + + /** + * Creates a new CommandSourceConverter for a server platform + * in which the player type is not a command sender type, and must be mapped. + * + * @param senderType class of the command sender type + * @param playerLookup function for looking up a player by UUID + * @param senderLookup function for converting a player to a command sender + * @param consoleProvider supplier of the console command sender + * @param commandSourceLookup supplier of the platform implementation of {@link GeyserCommandSource} + * @return a new CommandSourceConverter + * @param

server player type + * @param server command sender type + */ + public static CommandSourceConverter layered(Class senderType, + Function playerLookup, + Function senderLookup, + Supplier consoleProvider, + Function commandSourceLookup) { + Function lookup = uuid -> { + P player = playerLookup.apply(uuid); + if (player == null) { + return null; + } + return senderLookup.apply(player); + }; + return new CommandSourceConverter<>(senderType, lookup, consoleProvider, commandSourceLookup); + } + + @Override + public @NonNull GeyserCommandSource map(@NonNull S base) { + return commandSourceLookup.apply(base); + } + + @Override + public @NonNull S reverse(GeyserCommandSource source) throws IllegalArgumentException { + Object handle = source.handle(); + if (senderType.isInstance(handle)) { + return senderType.cast(handle); // one of the server platform implementations + } + + if (source.isConsole()) { + return consoleProvider.get(); // one of the loggers + } + + if (!(source instanceof GeyserSession)) { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); + if (logger.isDebug()) { + logger.debug("Falling back to UUID for command sender lookup for a command source that is not a GeyserSession: " + source); + Thread.dumpStack(); + } + } + + // Ideally lookup should only be necessary for GeyserSession + UUID uuid = source.playerUuid(); + if (uuid != null) { + return playerLookup.apply(uuid); + } + + throw new IllegalArgumentException("failed to find sender for name=%s, uuid=%s".formatted(source.name(), source.playerUuid())); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java new file mode 100644 index 000000000..444057709 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command; + +import io.leangen.geantyref.GenericTypeReflector; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.ChatColor; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.text.MinecraftLocale; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.exception.ArgumentParseException; +import org.incendo.cloud.exception.CommandExecutionException; +import org.incendo.cloud.exception.InvalidCommandSenderException; +import org.incendo.cloud.exception.InvalidSyntaxException; +import org.incendo.cloud.exception.NoPermissionException; +import org.incendo.cloud.exception.NoSuchCommandException; +import org.incendo.cloud.exception.handling.ExceptionController; + +import java.lang.reflect.Type; +import java.util.function.BiConsumer; + +/** + * Geyser's exception handlers for command execution with Cloud. + * Overrides Cloud's defaults so that messages can be customized to our liking: localization, etc. + */ +final class ExceptionHandlers { + + final static String PERMISSION_FAIL_LANG_KEY = "geyser.command.permission_fail"; + + private final ExceptionController controller; + + private ExceptionHandlers(ExceptionController controller) { + this.controller = controller; + } + + /** + * Clears the existing handlers that are registered to the given command manager, and repopulates them. + * + * @param manager the manager whose exception handlers will be modified + */ + static void register(CommandManager manager) { + new ExceptionHandlers(manager.exceptionController()).register(); + } + + private void register() { + // Yeet the default exception handlers that cloud provides so that we can perform localization. + controller.clearHandlers(); + + registerExceptionHandler(InvalidSyntaxException.class, + (ctx, e) -> ctx.sender().sendLocaleString("geyser.command.invalid_syntax", e.correctSyntax())); + + registerExceptionHandler(InvalidCommandSenderException.class, (ctx, e) -> { + // We currently don't use cloud sender type requirements anywhere. + // This can be implemented better in the future if necessary. + Type type = e.requiredSenderTypes().iterator().next(); // just grab the first + String typeString = GenericTypeReflector.getTypeName(type); + ctx.sender().sendLocaleString("geyser.command.invalid_sender", e.commandSender().getClass().getSimpleName(), typeString); + }); + + registerExceptionHandler(NoPermissionException.class, ExceptionHandlers::handleNoPermission); + + registerExceptionHandler(NoSuchCommandException.class, + (ctx, e) -> { + // Let backend server receive & handle the command + if (CommandRegistry.STANDALONE_COMMAND_MANAGER && ctx.sender() instanceof GeyserSession session) { + session.sendCommandPacket(ctx.rawInput().input()); + } else { + ctx.sender().sendLocaleString("geyser.command.not_found"); + } + }); + + registerExceptionHandler(ArgumentParseException.class, + (ctx, e) -> ctx.sender().sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage())); + + registerExceptionHandler(CommandExecutionException.class, + (ctx, e) -> handleUnexpectedThrowable(ctx.sender(), e.getCause())); + + registerExceptionHandler(Throwable.class, + (ctx, e) -> handleUnexpectedThrowable(ctx.sender(), e.getCause())); + } + + private void registerExceptionHandler(Class type, BiConsumer, E> handler) { + controller.registerHandler(type, context -> handler.accept(context.context(), context.exception())); + } + + private static void handleNoPermission(CommandContext context, NoPermissionException exception) { + GeyserCommandSource source = context.sender(); + + // Let backend server receive & handle the command + if (CommandRegistry.STANDALONE_COMMAND_MANAGER && source instanceof GeyserSession session) { + session.sendCommandPacket(context.rawInput().input()); + return; + } + + // custom handling if the source can't use the command because of additional requirements + if (exception.permissionResult() instanceof GeyserPermission.Result result) { + if (result.meta() == GeyserPermission.Result.Meta.NOT_BEDROCK) { + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.bedrock_only", source.locale())); + return; + } + if (result.meta() == GeyserPermission.Result.Meta.NOT_PLAYER) { + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.player_only", source.locale())); + return; + } + } else { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); + if (logger.isDebug()) { + logger.debug("Expected a GeyserPermission.Result for %s but instead got %s from %s".formatted(exception.currentChain(), exception.permissionResult(), exception.missingPermission())); + } + } + + // Result.NO_PERMISSION or generic permission failure + source.sendLocaleString(PERMISSION_FAIL_LANG_KEY); + } + + private static void handleUnexpectedThrowable(GeyserCommandSource source, Throwable throwable) { + source.sendMessage(MinecraftLocale.getLocaleString("command.failed", source.locale())); // java edition translation key + GeyserImpl.getInstance().getLogger().error("Exception while executing command handler", throwable); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java index 47d57e73f..3cc05ca0c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java @@ -25,65 +25,187 @@ package org.geysermc.geyser.command; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Accessors; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.CommandDescription; +import org.jetbrains.annotations.Contract; import java.util.Collections; import java.util.List; -@Accessors(fluent = true) -@Getter -@RequiredArgsConstructor -public abstract class GeyserCommand implements Command { +public abstract class GeyserCommand implements org.geysermc.geyser.api.command.Command { + public static final String DEFAULT_ROOT_COMMAND = "geyser"; + + /** + * The second literal of the command. Note: the first literal is {@link #rootCommand()}. + */ + @NonNull + private final String name; - protected final String name; /** * The description of the command - will attempt to be translated. */ - protected final String description; - protected final String permission; - - private List aliases = Collections.emptyList(); - - public abstract void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args); + @NonNull + private final String description; /** - * If false, hides the command from being shown on the Geyser Standalone GUI. - * - * @return true if the command can be run on the server console - */ - @Override - public boolean isExecutableOnConsole() { - return true; - } - - /** - * Used in the GUI to know what subcommands can be run - * - * @return a list of all possible subcommands, or empty if none. + * The permission node required to run the command, or blank if not required. */ @NonNull - @Override - public List subCommands() { - return Collections.emptyList(); + private final String permission; + + /** + * The default value of the permission node. + * A null value indicates that the permission node should not be registered whatsoever. + * See {@link GeyserRegisterPermissionsEvent#register(String, TriState)} for TriState meanings. + */ + @Nullable + private final TriState permissionDefault; + + /** + * True if this command can be executed by players + */ + private final boolean playerOnly; + + /** + * True if this command can only be run by bedrock players + */ + private final boolean bedrockOnly; + + /** + * The aliases of the command {@link #name}. This should not be modified after construction. + */ + protected List aliases = Collections.emptyList(); + + public GeyserCommand(@NonNull String name, @NonNull String description, + @NonNull String permission, @Nullable TriState permissionDefault, + boolean playerOnly, boolean bedrockOnly) { + + if (name.isBlank()) { + throw new IllegalArgumentException("Command cannot be null or blank!"); + } + if (permission.isBlank()) { + // Cloud treats empty permissions as available to everyone, but not blank permissions. + // When registering commands, we must convert ALL whitespace permissions into empty ones, + // because we cannot override permission checks that Cloud itself performs + permission = ""; + permissionDefault = null; + } + + this.name = name; + this.description = description; + this.permission = permission; + this.permissionDefault = permissionDefault; + + if (bedrockOnly && !playerOnly) { + throw new IllegalArgumentException("Command cannot be bedrockOnly if it is not playerOnly"); + } + + this.playerOnly = playerOnly; + this.bedrockOnly = bedrockOnly; } - public void setAliases(List aliases) { - this.aliases = aliases; + public GeyserCommand(@NonNull String name, @NonNull String description, @NonNull String permission, @Nullable TriState permissionDefault) { + this(name, description, permission, permissionDefault, false, false); + } + + @NonNull + @Override + public final String name() { + return name; + } + + @NonNull + @Override + public final String description() { + return description; + } + + @NonNull + @Override + public final String permission() { + return permission; + } + + @Nullable + public final TriState permissionDefault() { + return permissionDefault; + } + + @Override + public final boolean isPlayerOnly() { + return playerOnly; + } + + @Override + public final boolean isBedrockOnly() { + return bedrockOnly; + } + + @NonNull + @Override + public final List aliases() { + return Collections.unmodifiableList(aliases); } /** - * Used for permission defaults on server implementations. - * - * @return if this command is designated to be used only by server operators. + * @return the first (literal) argument of this command, which comes before {@link #name()}. */ - @Override - public boolean isSuggestedOpOnly() { - return false; + public String rootCommand() { + return DEFAULT_ROOT_COMMAND; } -} \ No newline at end of file + + /** + * Returns a {@link org.incendo.cloud.permission.Permission} that handles {@link #isBedrockOnly()}, {@link #isPlayerOnly()}, and {@link #permission()}. + * + * @param manager the manager to be used for permission node checking + * @return a permission that will properly restrict usage of this command + */ + public final GeyserPermission commandPermission(CommandManager manager) { + return new GeyserPermission(bedrockOnly, playerOnly, permission, manager); + } + + /** + * Creates a new command builder with {@link #rootCommand()}, {@link #name()}, and {@link #aliases()} built on it. + * A permission predicate that takes into account {@link #permission()}, {@link #isBedrockOnly()}, and {@link #isPlayerOnly()} + * is applied. The Applicable from {@link #meta()} is also applied to the builder. + */ + @Contract(value = "_ -> new", pure = true) + public final Command.Builder baseBuilder(CommandManager manager) { + return manager.commandBuilder(rootCommand()) + .literal(name, aliases.toArray(new String[0])) + .permission(commandPermission(manager)) + .apply(meta()); + } + + /** + * @return an Applicable that applies this command's description + */ + protected Command.Builder.Applicable meta() { + return builder -> builder + .commandDescription(CommandDescription.commandDescription(GeyserLocale.getLocaleStringLog(description))); // used in cloud-bukkit impl + } + + /** + * Registers this command to the given command manager. + * This method may be overridden to register more than one command. + *

+ * The default implementation is that {@link #baseBuilder(CommandManager)} with {@link #execute(CommandContext)} + * applied as the handler is registered to the manager. + */ + public void register(CommandManager manager) { + manager.command(baseBuilder(manager).handler(this::execute)); + } + + /** + * Executes this command + * @param context the context with which this command should be executed + */ + public abstract void execute(CommandContext context); +} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java deleted file mode 100644 index 37d2ef4fb..000000000 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.command; - -import lombok.AllArgsConstructor; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.session.GeyserSession; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * Represents helper functions for listening to {@code /geyser} or {@code /geyserext} commands. - */ -@AllArgsConstructor -public class GeyserCommandExecutor { - - protected final GeyserImpl geyser; - private final Map commands; - - public GeyserCommand getCommand(String label) { - return (GeyserCommand) commands.get(label); - } - - @Nullable - public GeyserSession getGeyserSession(GeyserCommandSource sender) { - if (sender.isConsole()) { - return null; - } - - for (GeyserSession session : geyser.getSessionManager().getSessions().values()) { - if (sender.name().equals(session.getPlayerEntity().getUsername())) { - return session; - } - } - return null; - } - - /** - * Determine which subcommands to suggest in the tab complete for the main /geyser command by a given command sender. - * - * @param sender The command sender to receive the tab complete suggestions. - * If the command sender is a bedrock player, an empty list will be returned as bedrock players do not get command argument suggestions. - * If the command sender is not a bedrock player, bedrock commands will not be shown. - * If the command sender does not have the permission for a given command, the command will not be shown. - * @return A list of command names to include in the tab complete - */ - public List tabComplete(GeyserCommandSource sender) { - if (getGeyserSession(sender) != null) { - // Bedrock doesn't get tab completions or argument suggestions - return Collections.emptyList(); - } - - List availableCommands = new ArrayList<>(); - - // Only show commands they have permission to use - for (Map.Entry entry : commands.entrySet()) { - Command geyserCommand = entry.getValue(); - if (sender.hasPermission(geyserCommand.permission())) { - if (geyserCommand.isBedrockOnly()) { - // Don't show commands the JE player can't run - continue; - } - - availableCommands.add(entry.getKey()); - } - } - - return availableCommands; - } -} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java deleted file mode 100644 index 72ed22381..000000000 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.command; - -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.command.CommandExecutor; -import org.geysermc.geyser.api.command.CommandSource; -import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent; -import org.geysermc.geyser.api.extension.Extension; -import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand; -import org.geysermc.geyser.command.defaults.AdvancementsCommand; -import org.geysermc.geyser.command.defaults.ConnectionTestCommand; -import org.geysermc.geyser.command.defaults.DumpCommand; -import org.geysermc.geyser.command.defaults.ExtensionsCommand; -import org.geysermc.geyser.command.defaults.HelpCommand; -import org.geysermc.geyser.command.defaults.ListCommand; -import org.geysermc.geyser.command.defaults.OffhandCommand; -import org.geysermc.geyser.command.defaults.ReloadCommand; -import org.geysermc.geyser.command.defaults.SettingsCommand; -import org.geysermc.geyser.command.defaults.StatisticsCommand; -import org.geysermc.geyser.command.defaults.StopCommand; -import org.geysermc.geyser.command.defaults.VersionCommand; -import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl; -import org.geysermc.geyser.extension.command.GeyserExtensionCommand; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -@RequiredArgsConstructor -public class GeyserCommandManager { - - @Getter - private final Map commands = new Object2ObjectOpenHashMap<>(12); - private final Map> extensionCommands = new Object2ObjectOpenHashMap<>(0); - - private final GeyserImpl geyser; - - public void init() { - registerBuiltInCommand(new HelpCommand(geyser, "help", "geyser.commands.help.desc", "geyser.command.help", "geyser", this.commands)); - registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list")); - registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload")); - registerBuiltInCommand(new OffhandCommand(geyser, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand")); - registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump")); - registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version")); - registerBuiltInCommand(new SettingsCommand(geyser, "settings", "geyser.commands.settings.desc", "geyser.command.settings")); - registerBuiltInCommand(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); - registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); - registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); - registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); - if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { - registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); - } - - if (!this.geyser.extensionManager().extensions().isEmpty()) { - registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions")); - } - - GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) { - - @Override - public void register(@NonNull Command command) { - if (!(command instanceof GeyserExtensionCommand extensionCommand)) { - throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?"); - } - - registerExtensionCommand(extensionCommand.extension(), extensionCommand); - } - }; - - this.geyser.eventBus().fire(defineCommandsEvent); - - // Register help commands for all extensions with commands - for (Map.Entry> entry : this.extensionCommands.entrySet()) { - String id = entry.getKey().description().id(); - registerExtensionCommand(entry.getKey(), new HelpCommand(this.geyser, "help", "geyser.commands.exthelp.desc", "geyser.command.exthelp." + id, id, entry.getValue())); - } - } - - /** - * For internal Geyser commands - */ - public void registerBuiltInCommand(GeyserCommand command) { - register(command, this.commands); - } - - public void registerExtensionCommand(@NonNull Extension extension, @NonNull Command command) { - register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>())); - } - - private void register(Command command, Map commands) { - commands.put(command.name(), command); - geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", command.name())); - - if (command.aliases().isEmpty()) { - return; - } - - for (String alias : command.aliases()) { - commands.put(alias, command); - } - } - - @NonNull - public Map commands() { - return Collections.unmodifiableMap(this.commands); - } - - @NonNull - public Map> extensionCommands() { - return Collections.unmodifiableMap(this.extensionCommands); - } - - public boolean runCommand(GeyserCommandSource sender, String command) { - Extension extension = null; - for (Extension loopedExtension : this.extensionCommands.keySet()) { - if (command.startsWith(loopedExtension.description().id() + " ")) { - extension = loopedExtension; - break; - } - } - - if (!command.startsWith("geyser ") && extension == null) { - return false; - } - - command = command.trim().replace(extension != null ? extension.description().id() + " " : "geyser ", ""); - String label; - String[] args; - - if (!command.contains(" ")) { - label = command.toLowerCase(Locale.ROOT); - args = new String[0]; - } else { - label = command.substring(0, command.indexOf(" ")).toLowerCase(Locale.ROOT); - String argLine = command.substring(command.indexOf(" ") + 1); - args = argLine.contains(" ") ? argLine.split(" ") : new String[] { argLine }; - } - - Command cmd = (extension != null ? this.extensionCommands.getOrDefault(extension, Collections.emptyMap()) : this.commands).get(label); - if (cmd == null) { - sender.sendMessage(GeyserLocale.getLocaleStringLog("geyser.commands.invalid")); - return false; - } - - if (cmd instanceof GeyserCommand) { - if (sender instanceof GeyserSession) { - ((GeyserCommand) cmd).execute((GeyserSession) sender, sender, args); - } else { - if (!cmd.isBedrockOnly()) { - ((GeyserCommand) cmd).execute(null, sender, args); - } else { - geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.command.bedrock_only")); - } - } - } - - return true; - } - - /** - * Returns the description of the given command - * - * @param command Command to get the description for - * @return Command description - */ - public String description(String command) { - return ""; - } - - @RequiredArgsConstructor - public static class CommandBuilder implements Command.Builder { - private final Extension extension; - private Class sourceType; - private String name; - private String description = ""; - private String permission = ""; - private List aliases; - private boolean suggestedOpOnly = false; - private boolean executableOnConsole = true; - private List subCommands; - private boolean bedrockOnly; - private CommandExecutor executor; - - @Override - public Command.Builder source(@NonNull Class sourceType) { - this.sourceType = sourceType; - return this; - } - - public CommandBuilder name(@NonNull String name) { - this.name = name; - return this; - } - - public CommandBuilder description(@NonNull String description) { - this.description = description; - return this; - } - - public CommandBuilder permission(@NonNull String permission) { - this.permission = permission; - return this; - } - - public CommandBuilder aliases(@NonNull List aliases) { - this.aliases = aliases; - return this; - } - - @Override - public Command.Builder suggestedOpOnly(boolean suggestedOpOnly) { - this.suggestedOpOnly = suggestedOpOnly; - return this; - } - - public CommandBuilder executableOnConsole(boolean executableOnConsole) { - this.executableOnConsole = executableOnConsole; - return this; - } - - public CommandBuilder subCommands(@NonNull List subCommands) { - this.subCommands = subCommands; - return this; - } - - public CommandBuilder bedrockOnly(boolean bedrockOnly) { - this.bedrockOnly = bedrockOnly; - return this; - } - - public CommandBuilder executor(@NonNull CommandExecutor executor) { - this.executor = executor; - return this; - } - - @NonNull - public GeyserExtensionCommand build() { - if (this.name == null || this.name.isBlank()) { - throw new IllegalArgumentException("Command cannot be null or blank!"); - } - - if (this.sourceType == null) { - throw new IllegalArgumentException("Source type was not defined for command " + this.name + " in extension " + this.extension.name()); - } - - return new GeyserExtensionCommand(this.extension, this.name, this.description, this.permission) { - - @SuppressWarnings("unchecked") - @Override - public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) { - Class sourceType = CommandBuilder.this.sourceType; - CommandExecutor executor = CommandBuilder.this.executor; - if (sourceType.isInstance(session)) { - executor.execute((T) session, this, args); - return; - } - - if (sourceType.isInstance(sender)) { - executor.execute((T) sender, this, args); - return; - } - - GeyserImpl.getInstance().getLogger().debug("Ignoring command " + this.name + " due to no suitable sender."); - } - - @NonNull - @Override - public List aliases() { - return CommandBuilder.this.aliases == null ? Collections.emptyList() : CommandBuilder.this.aliases; - } - - @Override - public boolean isSuggestedOpOnly() { - return CommandBuilder.this.suggestedOpOnly; - } - - @NonNull - @Override - public List subCommands() { - return CommandBuilder.this.subCommands == null ? Collections.emptyList() : CommandBuilder.this.subCommands; - } - - @Override - public boolean isBedrockOnly() { - return CommandBuilder.this.bedrockOnly; - } - - @Override - public boolean isExecutableOnConsole() { - return CommandBuilder.this.executableOnConsole; - } - }; - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java index 88d148b11..c14767496 100644 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java @@ -25,11 +25,16 @@ package org.geysermc.geyser.command; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.command.CommandSource; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import java.util.UUID; + /** * Implemented on top of any class that can send a command. * For example, it wraps around Spigot's CommandSender class. @@ -46,4 +51,29 @@ public interface GeyserCommandSource extends CommandSource { default void sendMessage(Component message) { sendMessage(LegacyComponentSerializer.legacySection().serialize(message)); } + + default void sendLocaleString(String key, Object... values) { + sendMessage(GeyserLocale.getPlayerLocaleString(key, locale(), values)); + } + + default void sendLocaleString(String key) { + sendMessage(GeyserLocale.getPlayerLocaleString(key, locale())); + } + + @Override + default @Nullable GeyserSession connection() { + UUID uuid = playerUuid(); + if (uuid == null) { + return null; + } + return GeyserImpl.getInstance().connectionByUuid(uuid); + } + + /** + * @return the underlying platform handle that this source represents. + * If such handle doesn't exist, this itself is returned. + */ + default Object handle() { + return this; + } } diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java b/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java new file mode 100644 index 000000000..1ee677e97 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command; + +import lombok.AllArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.key.CloudKey; +import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.permission.PermissionResult; +import org.incendo.cloud.permission.PredicatePermission; + +import static org.geysermc.geyser.command.GeyserPermission.Result.Meta; + +@AllArgsConstructor +public class GeyserPermission implements PredicatePermission { + + /** + * True if this permission requires the command source to be a bedrock player + */ + private final boolean bedrockOnly; + + /** + * True if this permission requires the command source to be any player + */ + private final boolean playerOnly; + + /** + * The permission node that the command source must have + */ + private final String permission; + + /** + * The command manager to delegate permission checks to + */ + private final CommandManager manager; + + @Override + public @NonNull Result testPermission(@NonNull GeyserCommandSource source) { + if (bedrockOnly) { + if (source.connection() == null) { + return new Result(Meta.NOT_BEDROCK); + } + // connection is present -> it is a player -> playerOnly is irrelevant + } else if (playerOnly) { + if (source.isConsole()) { + return new Result(Meta.NOT_PLAYER); // must be a player but is console + } + } + + if (permission.isBlank() || manager.hasPermission(source, permission)) { + return new Result(Meta.ALLOWED); + } + return new Result(Meta.NO_PERMISSION); + } + + @Override + public @NonNull CloudKey key() { + return CloudKey.cloudKey(permission); + } + + /** + * Basic implementation of cloud's {@link PermissionResult} that delegates to the more informative {@link Meta}. + */ + public final class Result implements PermissionResult { + + private final Meta meta; + + private Result(Meta meta) { + this.meta = meta; + } + + public Meta meta() { + return meta; + } + + @Override + public boolean allowed() { + return meta == Meta.ALLOWED; + } + + @Override + public @NonNull Permission permission() { + return GeyserPermission.this; + } + + /** + * More detailed explanation of whether the permission check passed. + */ + public enum Meta { + + /** + * The source must be a bedrock player, but is not. + */ + NOT_BEDROCK, + + /** + * The source must be a player, but is not. + */ + NOT_PLAYER, + + /** + * The source does not have a required permission node. + */ + NO_PERMISSION, + + /** + * The source meets all requirements. + */ + ALLOWED + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java index 466515b3f..be2ffd39c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java @@ -25,33 +25,32 @@ package org.geysermc.geyser.command.defaults; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class AdvancedTooltipsCommand extends GeyserCommand { + public AdvancedTooltipsCommand(String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session != null) { - String onOrOff = session.isAdvancedTooltips() ? "off" : "on"; - session.setAdvancedTooltips(!session.isAdvancedTooltips()); - session.sendMessage("§l§e" + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + " §r" + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale())); - session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); - } - } + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + String onOrOff = session.isAdvancedTooltips() ? "off" : "on"; + session.setAdvancedTooltips(!session.isAdvancedTooltips()); + session.sendMessage(ChatColor.BOLD + ChatColor.YELLOW + + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + + " " + ChatColor.RESET + + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale())); + session.getPlayerInventoryHolder().updateInventory(); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java index 28253433f..e82c0b66b 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java @@ -25,29 +25,23 @@ package org.geysermc.geyser.command.defaults; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class AdvancementsCommand extends GeyserCommand { + public AdvancementsCommand(String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session != null) { - session.getAdvancementsCache().buildAndShowMenuForm(); - } - } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.getAdvancementsCache().buildAndShowForm(); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java index 981c97595..d2066dba1 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java @@ -26,90 +26,82 @@ package org.geysermc.geyser.command.defaults; import com.fasterxml.jackson.databind.JsonNode; -import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.LoopbackUtil; import org.geysermc.geyser.util.WebUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Random; import java.util.concurrent.CompletableFuture; +import static org.incendo.cloud.parser.standard.IntegerParser.integerParser; +import static org.incendo.cloud.parser.standard.StringParser.stringParser; + public class ConnectionTestCommand extends GeyserCommand { + /* * The MOTD is temporarily changed during the connection test. * This allows us to check if we are pinging the correct Geyser instance */ public static String CONNECTION_TEST_MOTD = null; - private final GeyserImpl geyser; + private static final String ADDRESS = "address"; + private static final String PORT = "port"; + private final GeyserImpl geyser; private final Random random = new Random(); public ConnectionTestCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) { - // Only allow the console to create dumps on Geyser Standalone - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .required(ADDRESS, stringParser()) + .optional(PORT, integerParser(0, 65535)) + .handler(this::execute)); + } - if (args.length == 0) { - sender.sendMessage("Provide the server IP and port you are trying to test Bedrock connections for. Example: `test.geysermc.org:19132`"); - return; - } + @Override + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String ipArgument = context.get(ADDRESS); + Integer portArgument = context.getOrDefault(PORT, null); // null if port was not specified // Replace "<" and ">" symbols if they are present to avoid the common issue of people including them - String[] fullAddress = args[0].replace("<", "").replace(">", "").split(":", 2); - - // Still allow people to not supply a port and fallback to 19132 - int port; - if (fullAddress.length == 2) { - try { - port = Integer.parseInt(fullAddress[1]); - } catch (NumberFormatException e) { - // can occur if e.g. "/geyser connectiontest : is ran - sender.sendMessage("Not a valid port! Specify a valid numeric port."); - return; - } - } else { - port = geyser.getConfig().getBedrock().broadcastPort(); - } - String ip = fullAddress[0]; + final String ip = ipArgument.replace("<", "").replace(">", ""); + final int port = portArgument != null ? portArgument : geyser.getConfig().getBedrock().broadcastPort(); // default bedrock port // Issue: people commonly checking placeholders if (ip.equals("ip")) { - sender.sendMessage(ip + " is not a valid IP, and instead a placeholder. Please specify the IP to check."); + source.sendMessage(ip + " is not a valid IP, and instead a placeholder. Please specify the IP to check."); return; } // Issue: checking 0.0.0.0 won't work if (ip.equals("0.0.0.0")) { - sender.sendMessage("Please specify the IP that you would connect with. 0.0.0.0 in the config tells Geyser to the listen on the server's IPv4."); + source.sendMessage("Please specify the IP that you would connect with. 0.0.0.0 in the config tells Geyser to the listen on the server's IPv4."); return; } // Issue: people testing local ip if (ip.equals("localhost") || ip.startsWith("127.") || ip.startsWith("10.") || ip.startsWith("192.168.")) { - sender.sendMessage("This tool checks if connections from other networks are possible, so you cannot check a local IP."); + source.sendMessage("This tool checks if connections from other networks are possible, so you cannot check a local IP."); return; } // Issue: port out of bounds if (port <= 0 || port >= 65535) { - sender.sendMessage("The port you specified is invalid! Please specify a valid port."); + source.sendMessage("The port you specified is invalid! Please specify a valid port."); return; } @@ -118,37 +110,37 @@ public class ConnectionTestCommand extends GeyserCommand { // Issue: do the ports not line up? We only check this if players don't override the broadcast port - if they do, they (hopefully) know what they're doing if (config.getBedrock().broadcastPort() == config.getBedrock().port()) { if (port != config.getBedrock().port()) { - if (fullAddress.length == 2) { - sender.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration (" + if (portArgument != null) { + source.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration (" + config.getBedrock().port() + ")"); - sender.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config."); + source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config."); if (config.getBedrock().isCloneRemotePort()) { - sender.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead."); + source.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead."); } } else { - sender.sendMessage("You did not specify the port to check (add it with \":\"), " + + source.sendMessage("You did not specify the port to check (add it with \":\"), " + "and the default port 19132 does not match the port in your Geyser configuration (" + config.getBedrock().port() + ")!"); - sender.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`."); + source.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`."); } } } else { if (config.getBedrock().broadcastPort() != port) { - sender.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration (" + source.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration (" + config.getBedrock().broadcastPort() + "). "); - sender.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on."); - sender.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config."); + source.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on."); + source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config."); } } // Issue: is the `bedrock` `address` in the config different? if (!config.getBedrock().address().equals("0.0.0.0")) { - sender.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional."); + source.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional."); } // Issue: did someone turn on enable-proxy-protocol, and they didn't mean it? if (config.getBedrock().isEnableProxyProtocol()) { - sender.sendMessage("You have the `enable-proxy-protocol` setting enabled. " + + source.sendMessage("You have the `enable-proxy-protocol` setting enabled. " + "Unless you're deliberately using additional software that REQUIRES this setting, you may not need it enabled."); } @@ -157,14 +149,14 @@ public class ConnectionTestCommand extends GeyserCommand { // Issue: SRV record? String[] record = WebUtils.findSrvRecord(geyser, ip); if (record != null && !ip.equals(record[3]) && !record[2].equals(String.valueOf(port))) { - sender.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2] + source.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2] + ". If that fails, re-run this command with that address and port."); return; } // Issue: does Loopback need applying? if (LoopbackUtil.needsLoopback(GeyserImpl.getInstance().getLogger())) { - sender.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " + + source.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " + "See here for steps on how to resolve: " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/#using-geyser-on-the-same-computer"); } @@ -178,7 +170,7 @@ public class ConnectionTestCommand extends GeyserCommand { String connectionTestMotd = "Geyser Connection Test " + randomStr; CONNECTION_TEST_MOTD = connectionTestMotd; - sender.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait..."); + source.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait..."); JsonNode output; try { String hostname = URLEncoder.encode(ip, StandardCharsets.UTF_8); @@ -200,31 +192,31 @@ public class ConnectionTestCommand extends GeyserCommand { JsonNode pong = ping.get("pong"); String remoteMotd = pong.get("motd").asText(); if (!connectionTestMotd.equals(remoteMotd)) { - sender.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " + + source.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " + "Did you supply the correct IP and port of your server?"); - sendLinks(sender); + sendLinks(source); return; } if (ping.get("tcpFirst").asBoolean()) { - sender.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information."); - sendLinks(sender); + source.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information."); + sendLinks(source); return; } - sender.sendMessage("Your server is likely online and working as of " + when + "!"); - sendLinks(sender); + source.sendMessage("Your server is likely online and working as of " + when + "!"); + sendLinks(source); return; } - sender.sendMessage("Your server is likely unreachable from outside the network!"); + source.sendMessage("Your server is likely unreachable from outside the network!"); JsonNode message = output.get("message"); if (message != null && !message.asText().isEmpty()) { - sender.sendMessage("Got the error message: " + message.asText()); + source.sendMessage("Got the error message: " + message.asText()); } - sendLinks(sender); + sendLinks(source); } catch (Exception e) { - sender.sendMessage("An error occurred while trying to check your connection! Check the console for more information."); + source.sendMessage("An error occurred while trying to check your connection! Check the console for more information."); geyser.getLogger().error("Error while trying to check your connection!", e); } }); @@ -235,9 +227,4 @@ public class ConnectionTestCommand extends GeyserCommand { "https://wiki.geysermc.org/geyser/setup/"); sender.sendMessage("If that does not work, see " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/" + ", or contact us on Discord: " + "https://discord.gg/geysermc"); } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/CustomOptionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/CustomOptionsCommand.java new file mode 100644 index 000000000..32d151da5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/CustomOptionsCommand.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.defaults; + +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; + +public class CustomOptionsCommand extends GeyserCommand { + + public CustomOptionsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.openPauseScreenAdditions(); + if (!session.hasFormOpen()) { + context.sender().sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.options.fail", session.locale())); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java index b3fee375f..ff879b8a8 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java @@ -25,46 +25,75 @@ package org.geysermc.geyser.command.defaults; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.dump.DumpInfo; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.WebUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.suggestion.SuggestionProvider; import java.io.FileOutputStream; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; +import static org.incendo.cloud.parser.standard.StringArrayParser.stringArrayParser; + public class DumpCommand extends GeyserCommand { + private static final String ARGUMENTS = "args"; + private static final Iterable SUGGESTIONS = List.of("full", "offline", "logs"); + private final GeyserImpl geyser; private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String DUMP_URL = "https://dump.geysermc.org/"; public DumpCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - // Only allow the console to create dumps on Geyser Standalone - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .optional(ARGUMENTS, stringArrayParser(), SuggestionProvider.blockingStrings((ctx, input) -> { + // parse suggestions here + List inputs = new ArrayList<>(); + while (input.hasRemainingInput()) { + inputs.add(input.readStringSkipWhitespace()); + } + + if (inputs.size() <= 2) { + return SUGGESTIONS; // only `geyser dump` was typed (2 literals) + } + + // the rest of the input after `geyser dump` is for this argument + inputs = inputs.subList(2, inputs.size()); + + // don't suggest any words they have already typed + List suggestions = new ArrayList<>(); + SUGGESTIONS.forEach(suggestions::add); + suggestions.removeAll(inputs); + return suggestions; + })) + .handler(this::execute)); + } + + @Override + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String[] args = context.getOrDefault(ARGUMENTS, new String[0]); boolean showSensitive = false; boolean offlineDump = false; @@ -75,25 +104,28 @@ public class DumpCommand extends GeyserCommand { case "full" -> showSensitive = true; case "offline" -> offlineDump = true; case "logs" -> addLog = true; + default -> context.sender().sendMessage("Invalid geyser dump option " + arg + "! Fallback to no arguments."); } } } AsteriskSerializer.showSensitive = showSensitive; - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", source.locale())); String dumpData; try { + DumpInfo dump = new DumpInfo(geyser, addLog); + if (offlineDump) { DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); // Make arrays easier to read prettyPrinter.indentArraysWith(new DefaultIndenter(" ", "\n")); - dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(new DumpInfo(addLog)); + dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(dump); } else { - dumpData = MAPPER.writeValueAsString(new DumpInfo(addLog)); + dumpData = MAPPER.writeValueAsString(dump); } } catch (IOException e) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.collect_error_short"), e); return; } @@ -101,55 +133,47 @@ public class DumpCommand extends GeyserCommand { String uploadedDumpUrl; if (offlineDump) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.writing", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.writing", source.locale())); try { FileOutputStream outputStream = new FileOutputStream(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("dump.json").toFile()); outputStream.write(dumpData.getBytes()); outputStream.close(); } catch (IOException e) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.write_error", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.write_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.write_error_short"), e); return; } uploadedDumpUrl = "dump.json"; } else { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", source.locale())); - String response; + String response = null; JsonNode responseNode; try { response = WebUtils.post(DUMP_URL + "documents", dumpData); responseNode = MAPPER.readTree(response); } catch (IOException e) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.upload_error_short"), e); + if (e instanceof JsonParseException && response != null) { + geyser.getLogger().error("Failed to parse dump response! got: " + response); + } return; } if (!responseNode.has("key")) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", sender.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response)); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response)); return; } uploadedDumpUrl = DUMP_URL + responseNode.get("key").asText(); } - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", sender.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl); - if (!sender.isConsole()) { - geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.commands.dump.created", sender.name(), uploadedDumpUrl)); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", source.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl); + if (!source.isConsole()) { + geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.commands.dump.created", source.name(), uploadedDumpUrl)); } } - - @NonNull - @Override - public List subCommands() { - return Arrays.asList("offline", "full", "logs"); - } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java index df33437d9..24881f2ca 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java @@ -25,14 +25,14 @@ package org.geysermc.geyser.command.defaults; -import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.Comparator; import java.util.List; @@ -41,22 +41,23 @@ public class ExtensionsCommand extends GeyserCommand { private final GeyserImpl geyser; public ExtensionsCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.TRUE); this.geyser = geyser; } @Override - public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) { + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + // TODO: Pagination int page = 1; int maxPage = 1; - String header = GeyserLocale.getPlayerLocaleString("geyser.commands.extensions.header", sender.locale(), page, maxPage); - sender.sendMessage(header); + String header = GeyserLocale.getPlayerLocaleString("geyser.commands.extensions.header", source.locale(), page, maxPage); + source.sendMessage(header); this.geyser.extensionManager().extensions().stream().sorted(Comparator.comparing(Extension::name)).forEach(extension -> { String extensionName = (extension.isEnabled() ? ChatColor.GREEN : ChatColor.RED) + extension.name(); - sender.sendMessage("- " + extensionName + ChatColor.RESET + " v" + extension.description().version() + formatAuthors(extension.description().authors())); + source.sendMessage("- " + extensionName + ChatColor.RESET + " v" + extension.description().version() + formatAuthors(extension.description().authors())); }); } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java index c9671b089..9911863ab 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java @@ -25,61 +25,59 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.GeyserImpl; +import com.google.common.base.Predicates; import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Map; public class HelpCommand extends GeyserCommand { - private final GeyserImpl geyser; - private final String baseCommand; - private final Map commands; + private final String rootCommand; + private final Collection commands; - public HelpCommand(GeyserImpl geyser, String name, String description, String permission, - String baseCommand, Map commands) { - super(name, description, permission); - this.geyser = geyser; - this.baseCommand = baseCommand; - this.commands = commands; - - this.setAliases(Collections.singletonList("?")); + public HelpCommand(String rootCommand, String name, String description, String permission, Map commands) { + super(name, description, permission, TriState.TRUE); + this.rootCommand = rootCommand; + this.commands = commands.values(); + this.aliases = Collections.singletonList("?"); } - /** - * Sends the help menu to a command sender. Will not show certain commands depending on the command sender and session. - * - * @param session The Geyser session of the command sender, if it is a bedrock player. If null, bedrock-only commands will be hidden. - * @param sender The CommandSender to send the help message to. - * @param args Not used. - */ @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { + public String rootCommand() { + return rootCommand; + } + + @Override + public void execute(CommandContext context) { + execute(context.sender()); + } + + public void execute(GeyserCommandSource source) { + boolean bedrockPlayer = source.connection() != null; + + // todo: pagination int page = 1; int maxPage = 1; - String translationKey = this.baseCommand.equals("geyser") ? "geyser.commands.help.header" : "geyser.commands.extensions.header"; - String header = GeyserLocale.getPlayerLocaleString(translationKey, sender.locale(), page, maxPage); - sender.sendMessage(header); + String translationKey = this.rootCommand.equals(DEFAULT_ROOT_COMMAND) ? "geyser.commands.help.header" : "geyser.commands.extensions.header"; + String header = GeyserLocale.getPlayerLocaleString(translationKey, source.locale(), page, maxPage); + source.sendMessage(header); - this.commands.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { - Command cmd = entry.getValue(); - - // Standalone hack-in since it doesn't have a concept of permissions - if (geyser.getPlatformType() == PlatformType.STANDALONE || sender.hasPermission(cmd.permission())) { - // Only list commands the player can actually run - if (cmd.isBedrockOnly() && session == null) { - return; - } - - sender.sendMessage(ChatColor.YELLOW + "/" + baseCommand + " " + entry.getKey() + ChatColor.WHITE + ": " + - GeyserLocale.getPlayerLocaleString(cmd.description(), sender.locale())); - } - }); + this.commands.stream() + .distinct() // remove aliases + .filter(bedrockPlayer ? Predicates.alwaysTrue() : cmd -> !cmd.isBedrockOnly()) // remove bedrock only commands if not a bedrock player + .filter(cmd -> source.hasPermission(cmd.permission())) + .sorted(Comparator.comparing(Command::name)) + .forEachOrdered(cmd -> { + String description = GeyserLocale.getPlayerLocaleString(cmd.description(), source.locale()); + source.sendMessage(ChatColor.YELLOW + "/" + rootCommand + " " + cmd.name() + ChatColor.WHITE + ": " + description); + }); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java index 90446fbb6..5a76ab902 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java @@ -26,10 +26,12 @@ package org.geysermc.geyser.command.defaults; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.stream.Collectors; @@ -38,22 +40,18 @@ public class ListCommand extends GeyserCommand { private final GeyserImpl geyser; public ListCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - String message = GeyserLocale.getPlayerLocaleString("geyser.commands.list.message", sender.locale(), - geyser.getSessionManager().size(), - geyser.getSessionManager().getAllSessions().stream().map(GeyserSession::bedrockUsername).collect(Collectors.joining(" "))); + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); - sender.sendMessage(message); - } + String message = GeyserLocale.getPlayerLocaleString("geyser.commands.list.message", source.locale(), + geyser.getSessionManager().size(), + geyser.getSessionManager().getAllSessions().stream().map(GeyserSession::bedrockUsername).collect(Collectors.joining(" "))); - @Override - public boolean isSuggestedOpOnly() { - return true; + source.sendMessage(message); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java index 6188e6924..5f9061618 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java @@ -25,33 +25,23 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class OffhandCommand extends GeyserCommand { - public OffhandCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + public OffhandCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session == null) { - return; - } - + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); session.requestOffhandSwap(); } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java new file mode 100644 index 000000000..f39be0528 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.defaults; + +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; + +public class PingCommand extends GeyserCommand { + + public PingCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.ping.message", session.locale(), session.ping())); + } +} + diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/QuickActionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/QuickActionsCommand.java new file mode 100644 index 000000000..07f76e62a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/QuickActionsCommand.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.defaults; + +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; + +public class QuickActionsCommand extends GeyserCommand { + + public QuickActionsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.openQuickActions(); + if (!session.hasFormOpen()) { + context.sender().sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.quickactions.fail", session.locale())); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java index 987860238..e54b83ddf 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.concurrent.TimeUnit; @@ -39,27 +39,17 @@ public class ReloadCommand extends GeyserCommand { private final GeyserImpl geyser; public ReloadCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - return; - } - - String message = GeyserLocale.getPlayerLocaleString("geyser.commands.reload.message", sender.locale()); - - sender.sendMessage(message); + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.reload.message", source.locale())); geyser.getSessionManager().disconnectAll("geyser.commands.reload.kick"); //FIXME Without the tiny wait, players do not get kicked - same happens when Geyser tries to disconnect all sessions on shutdown geyser.getScheduledThread().schedule(geyser::reloadGeyser, 10, TimeUnit.MILLISECONDS); } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java index 7828cf1d2..a5734a69f 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java @@ -25,31 +25,24 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.SettingsUtils; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class SettingsCommand extends GeyserCommand { - public SettingsCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + + public SettingsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session != null) { - session.sendForm(SettingsUtils.buildForm(session)); - } - } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.sendForm(SettingsUtils.buildForm(session)); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java index 1ff12dea3..eebb9170c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java @@ -25,35 +25,28 @@ package org.geysermc.geyser.command.defaults; -import com.github.steveice10.mc.protocol.data.game.ClientCommand; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.ClientCommand; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class StatisticsCommand extends GeyserCommand { - public StatisticsCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + public StatisticsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session == null) return; + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); session.setWaitingForStatistics(true); - ServerboundClientCommandPacket ServerboundClientCommandPacket = new ServerboundClientCommandPacket(ClientCommand.STATS); - session.sendDownstreamGamePacket(ServerboundClientCommandPacket); - } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + ServerboundClientCommandPacket packet = new ServerboundClientCommandPacket(ClientCommand.STATS); + session.sendDownstreamGamePacket(packet); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java index 1cd3050c9..f6dc1610a 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.Collections; @@ -39,24 +38,13 @@ public class StopCommand extends GeyserCommand { private final GeyserImpl geyser; public StopCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; - - this.setAliases(Collections.singletonList("shutdown")); + this.aliases = Collections.singletonList("shutdown"); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } - + public void execute(CommandContext context) { geyser.getBootstrap().onGeyserShutdown(); } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java index eb2e8ff47..ef5535cb8 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java @@ -25,79 +25,85 @@ package org.geysermc.geyser.command.defaults; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; -import org.geysermc.geyser.Constants; +import com.fasterxml.jackson.databind.JsonNode; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.MinecraftVersion; import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.WebUtils; +import org.incendo.cloud.context.CommandContext; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import java.io.IOException; import java.util.List; public class VersionCommand extends GeyserCommand { + private static final String SUPPORTED_BEDROCK_RANGE; + private static final String SUPPORTED_JAVA_RANGE; + + static { + List bedrockVersions = GameProtocol.SUPPORTED_BEDROCK_VERSIONS; + if (bedrockVersions.size() > 1) { + SUPPORTED_BEDROCK_RANGE = bedrockVersions.get(0).versionString() + " - " + bedrockVersions.get(bedrockVersions.size() - 1).versionString(); + } else { + SUPPORTED_BEDROCK_RANGE = bedrockVersions.get(0).versionString(); + } + + List javaVersions = GameProtocol.getJavaVersions(); + if (javaVersions.size() > 1) { + SUPPORTED_JAVA_RANGE = javaVersions.get(0) + " - " + javaVersions.get(javaVersions.size() - 1); + } else { + SUPPORTED_JAVA_RANGE = javaVersions.get(0); + } + } + private final GeyserImpl geyser; public VersionCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - String bedrockVersions; - List supportedCodecs = GameProtocol.SUPPORTED_BEDROCK_CODECS; - if (supportedCodecs.size() > 1) { - bedrockVersions = supportedCodecs.get(0).getMinecraftVersion() + " - " + supportedCodecs.get(supportedCodecs.size() - 1).getMinecraftVersion(); - } else { - bedrockVersions = GameProtocol.SUPPORTED_BEDROCK_CODECS.get(0).getMinecraftVersion(); - } - String javaVersions; - List supportedJavaVersions = GameProtocol.getJavaVersions(); - if (supportedJavaVersions.size() > 1) { - javaVersions = supportedJavaVersions.get(0) + " - " + supportedJavaVersions.get(supportedJavaVersions.size() - 1); - } else { - javaVersions = supportedJavaVersions.get(0); - } + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.version", sender.locale(), - GeyserImpl.NAME, GeyserImpl.VERSION, javaVersions, bedrockVersions)); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.version", source.locale(), + GeyserImpl.NAME, GeyserImpl.VERSION, SUPPORTED_JAVA_RANGE, SUPPORTED_BEDROCK_RANGE)); // Disable update checking in dev mode and for players in Geyser Standalone - if (GeyserImpl.getInstance().isProductionEnvironment() && !(!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", sender.locale())); - try { - String buildXML = WebUtils.getBody("https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/" + - URLEncoder.encode(GeyserImpl.BRANCH, StandardCharsets.UTF_8) + "/lastSuccessfulBuild/api/xml?xpath=//buildNumber"); - if (buildXML.startsWith("")) { - int latestBuildNum = Integer.parseInt(buildXML.replaceAll("<(\\\\)?(/)?buildNumber>", "").trim()); - int buildNum = this.geyser.buildNumber(); - if (latestBuildNum == buildNum) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", sender.locale())); - } else { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.outdated", - sender.locale(), (latestBuildNum - buildNum), Constants.GEYSER_DOWNLOAD_LOCATION)); - } - } else { - throw new AssertionError("buildNumber missing"); - } - } catch (Exception e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.version.failed"), e); - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", sender.locale())); + if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { + return; + } + + if (GeyserImpl.IS_DEV) { + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.core.dev_build", source.locale(), "https://discord.gg/geysermc")); + return; + } + + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", source.locale())); + try { + int buildNumber = this.geyser.buildNumber(); + JsonNode response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest"); + int latestBuildNumber = response.get("build").asInt(); + + if (latestBuildNumber == buildNumber) { + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", source.locale())); + return; } + + source.sendMessage(GeyserLocale.getPlayerLocaleString( + "geyser.commands.version.outdated", + source.locale(), (latestBuildNumber - buildNumber), "https://geysermc.org/download" + )); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.version.failed"), e); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", source.locale())); } } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java new file mode 100644 index 000000000..59e8db211 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.standalone; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.util.Collections; +import java.util.Set; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final +public class PermissionConfiguration { + + @JsonProperty("default-permissions") + private Set defaultPermissions = Collections.emptySet(); + + @JsonProperty("default-denied-permissions") + private Set defaultDeniedPermissions = Collections.emptySet(); +} diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java new file mode 100644 index 000000000..87aab8045 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.standalone; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionCheckersEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.permission.PermissionChecker; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.internal.CommandRegistrationHandler; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class StandaloneCloudCommandManager extends CommandManager { + + private final GeyserImpl geyser; + + /** + * The checkers we use to test if a command source has a permission + */ + private final List permissionCheckers = new ArrayList<>(); + + /** + * Any permissions that all connections have + */ + private final Set basePermissions = new ObjectOpenHashSet<>(); + + /** + * Any permissions that all connections do not have + */ + private final Set baseDeniedPermissions = new ObjectOpenHashSet<>(); + + public StandaloneCloudCommandManager(GeyserImpl geyser) { + super(ExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler()); + // simpleCoordinator: execute commands immediately on the calling thread. + // nullCommandRegistrationHandler: cloud is not responsible for handling our CommandRegistry, which is fairly decoupled. + this.geyser = geyser; + + // allow any extensions to customize permissions + geyser.getEventBus().fire((GeyserRegisterPermissionCheckersEvent) permissionCheckers::add); + + // must still implement a basic permission system + try { + File permissionsFile = geyser.getBootstrap().getConfigFolder().resolve("permissions.yml").toFile(); + FileUtils.fileOrCopiedFromResource(permissionsFile, "permissions.yml", geyser.getBootstrap()); + PermissionConfiguration config = FileUtils.loadConfig(permissionsFile, PermissionConfiguration.class); + basePermissions.addAll(config.getDefaultPermissions()); + baseDeniedPermissions.addAll(config.getDefaultDeniedPermissions()); + } catch (Exception e) { + geyser.getLogger().error("Failed to load permissions.yml - proceeding without it", e); + } + } + + /** + * Fire a {@link GeyserRegisterPermissionsEvent} to determine any additions or removals to the base list of + * permissions. This should be called after any event listeners have been registered, such as that of {@link CommandRegistry}. + */ + public void fireRegisterPermissionsEvent() { + geyser.getEventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> { + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(def, "permission default for " + permission); + + if (permission.isBlank() || def == TriState.NOT_SET) { + return; + } + + GeyserImpl.getInstance().getLogger().debug("Registering permission %s with permission default %s", permission, def); + + if (def == TriState.TRUE) { + // The last caller gets to override earlier set defaults + baseDeniedPermissions.remove(permission); + basePermissions.add(permission); + } else { + basePermissions.remove(permission); + baseDeniedPermissions.add(permission); + } + }); + } + + @Override + public boolean hasPermission(@NonNull GeyserCommandSource sender, @NonNull String permission) { + // Note: the two GeyserCommandSources on Geyser-Standalone are GeyserLogger and GeyserSession + // GeyserLogger#hasPermission always returns true + // GeyserSession#hasPermission delegates to this method, + // which is why this method doesn't just call GeyserCommandSource#hasPermission + if (sender.isConsole()) { + return true; + } + + // An empty or blank permission is treated as a lack of permission requirement + if (permission.isBlank()) { + return true; + } + + for (PermissionChecker checker : permissionCheckers) { + Boolean result = checker.hasPermission(sender, permission).toBoolean(); + if (result != null) { + return result; + } + // undefined - try the next checker to see if it has a defined value + } + + if (baseDeniedPermissions.contains(permission)) { + return false; + } + + // fallback to our list of default permissions + // note that a PermissionChecker may in fact override any values set here by returning FALSE + return basePermissions.contains(permission); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index d12ab79e9..88bb98171 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -72,8 +72,10 @@ public interface GeyserConfiguration { boolean isDebugMode(); + @Deprecated boolean isAllowThirdPartyCapes(); + @Deprecated boolean isAllowThirdPartyEars(); String getShowCooldown(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index b3b7e8cd4..81ac824e4 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -40,9 +40,11 @@ import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.network.CIDRMatcher; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.WebUtils; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -92,7 +94,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private boolean debugMode = false; @JsonProperty("allow-third-party-capes") - private boolean allowThirdPartyCapes = true; + private boolean allowThirdPartyCapes = false; @JsonProperty("show-cooldown") private String showCooldown = "title"; @@ -233,7 +235,18 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration List matchers = this.whitelistedIPsMatchers; if (matchers == null) { synchronized (this) { - this.whitelistedIPsMatchers = matchers = proxyProtocolWhitelistedIPs.stream() + // Check if proxyProtocolWhitelistedIPs contains URLs we need to fetch and parse by line + List whitelistedCIDRs = new ArrayList<>(); + for (String ip: proxyProtocolWhitelistedIPs) { + if (!ip.startsWith("http")) { + whitelistedCIDRs.add(ip); + continue; + } + + WebUtils.getLineStream(ip).forEach(whitelistedCIDRs::add); + } + + this.whitelistedIPsMatchers = matchers = whitelistedCIDRs.stream() .map(CIDRMatcher::new) .collect(Collectors.toList()); } diff --git a/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java index 6a56c536a..7851fadfd 100644 --- a/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java +++ b/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java @@ -27,8 +27,8 @@ package org.geysermc.geyser.dump; import lombok.AllArgsConstructor; import lombok.Getter; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.text.AsteriskSerializer; import java.util.List; diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java index 818607314..aeff8767b 100644 --- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java +++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java @@ -34,12 +34,12 @@ import com.google.common.io.Files; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.Getter; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.FloodgateInfoHolder; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.MinecraftVersion; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; @@ -55,13 +55,9 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.UnknownHostException; +import java.nio.charset.Charset; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.stream.Collectors; @Getter @@ -78,6 +74,7 @@ public class DumpInfo { private final GeyserConfiguration config; private final Floodgate floodgate; private final Object2IntMap userPlatforms; + private final int connectionAttempts; private final HashInfo hashInfo; private final RamInfo ramInfo; private LogsInfo logsInfo; @@ -85,17 +82,17 @@ public class DumpInfo { private final FlagsInfo flagsInfo; private final List extensionInfo; - public DumpInfo(boolean addLog) { + public DumpInfo(GeyserImpl geyser, boolean addLog) { this.versionInfo = new VersionInfo(); this.cpuCount = Runtime.getRuntime().availableProcessors(); this.cpuName = CpuUtils.tryGetProcessorName(); this.systemLocale = Locale.getDefault(); - this.systemEncoding = System.getProperty("file.encoding"); + this.systemEncoding = Charset.defaultCharset().displayName(); this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY); - this.config = GeyserImpl.getInstance().getConfig(); + this.config = geyser.getConfig(); this.floodgate = new Floodgate(); String md5Hash = "unknown"; @@ -111,7 +108,7 @@ public class DumpInfo { //noinspection UnstableApiUsage sha256Hash = byteSource.hash(Hashing.sha256()).toString(); } catch (Exception e) { - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (this.config.isDebugMode()) { e.printStackTrace(); } } @@ -120,16 +117,22 @@ public class DumpInfo { this.ramInfo = new RamInfo(); if (addLog) { - this.logsInfo = new LogsInfo(); + this.logsInfo = new LogsInfo(geyser); } this.userPlatforms = new Object2IntOpenHashMap<>(); - for (GeyserSession session : GeyserImpl.getInstance().getSessionManager().getAllSessions()) { + for (GeyserSession session : geyser.getSessionManager().getAllSessions()) { DeviceOs device = session.getClientData().getDeviceOs(); userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1); } - this.bootstrapInfo = GeyserImpl.getInstance().getBootstrap().getDumpInfo(); + if (geyser.getGeyserServer() != null) { + this.connectionAttempts = geyser.getGeyserServer().getConnectionAttempts(); + } else { + this.connectionAttempts = 0; // Fallback if Geyser failed to fully startup + } + + this.bootstrapInfo = geyser.getBootstrap().getDumpInfo(); this.flagsInfo = new FlagsInfo(); @@ -176,9 +179,8 @@ public class DumpInfo { NetworkInfo() { if (AsteriskSerializer.showSensitive) { - try { + try (Socket socket = new Socket()) { // This is the most reliable for getting the main local IP - Socket socket = new Socket(); socket.connect(new InetSocketAddress("geysermc.org", 80)); this.internalIP = socket.getLocalAddress().getHostAddress(); } catch (IOException e1) { @@ -223,9 +225,9 @@ public class DumpInfo { private final int javaProtocol; MCInfo() { - this.bedrockVersions = GameProtocol.SUPPORTED_BEDROCK_CODECS.stream().map(BedrockCodec::getMinecraftVersion).toList(); - this.bedrockProtocols = GameProtocol.SUPPORTED_BEDROCK_CODECS.stream().map(BedrockCodec::getProtocolVersion).toList(); - this.defaultBedrockProtocol = GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion(); + this.bedrockVersions = GameProtocol.SUPPORTED_BEDROCK_VERSIONS.stream().map(MinecraftVersion::versionString).toList(); + this.bedrockProtocols = GameProtocol.SUPPORTED_BEDROCK_PROTOCOLS; + this.defaultBedrockProtocol = GameProtocol.DEFAULT_BEDROCK_PROTOCOL; this.javaVersions = GameProtocol.getJavaVersions(); this.javaProtocol = GameProtocol.getJavaProtocolVersion(); } @@ -246,10 +248,10 @@ public class DumpInfo { public static class LogsInfo { private String link; - public LogsInfo() { + public LogsInfo(GeyserImpl geyser) { try { Map fields = new HashMap<>(); - fields.put("content", FileUtils.readAllLines(GeyserImpl.getInstance().getBootstrap().getLogsPath()).collect(Collectors.joining("\n"))); + fields.put("content", FileUtils.readAllLines(geyser.getBootstrap().getLogsPath()).collect(Collectors.joining("\n"))); JsonNode logData = GeyserImpl.JSON_MAPPER.readTree(WebUtils.postForm("https://api.mclo.gs/1/log", fields)); diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java index 378031253..c8df1e6a5 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java @@ -25,9 +25,6 @@ package org.geysermc.geyser.entity; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType; -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import lombok.Setter; import lombok.experimental.Accessors; @@ -36,10 +33,16 @@ import org.cloudburstmc.nbt.NbtType; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.entity.EntityIdentifier; import org.geysermc.geyser.entity.factory.EntityFactory; +import org.geysermc.geyser.entity.properties.GeyserEntityProperties; +import org.geysermc.geyser.entity.properties.type.PropertyType; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.translator.entity.EntityMetadataTranslator; +import org.geysermc.geyser.util.BlockEntityUtils; import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import java.util.ArrayList; import java.util.List; @@ -52,10 +55,11 @@ import java.util.function.BiConsumer; * metadata translators needed to translate the properties sent from the server. The translators are structured in such * a way that inserting a new one (for example in version updates) is convenient. * + * @param identifier the Bedrock identifier of this entity * @param the entity type this definition represents */ public record EntityDefinition(EntityFactory factory, EntityType entityType, EntityIdentifier entityIdentifier, - float width, float height, float offset, List> translators, boolean custom) implements org.geysermc.geyser.api.entity.GeyserEntityDefinition { + float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators, boolean custom) implements org.geysermc.geyser.api.entity.GeyserEntityDefinition { public static EntityDefinitionBuilder inherited(EntityFactory factory, EntityDefinition parent) { return new EntityDefinitionBuilder<>(factory, parent.entityType, parent.entityIdentifier, parent.width, parent.height, parent.offset, new ObjectArrayList<>(parent.translators)); @@ -105,6 +109,7 @@ public record EntityDefinition(EntityFactory factory, Entit private float width; private float height; private float offset = 0.00001f; + private GeyserEntityProperties.Builder propertiesBuilder; private final List> translators; private final boolean custom; @@ -190,6 +195,14 @@ public record EntityDefinition(EntityFactory factory, Entit return this; } + public Builder property(PropertyType propertyType) { + if (this.propertiesBuilder == null) { + this.propertiesBuilder = new GeyserEntityProperties.Builder(this.identifier); + } + propertiesBuilder.add(propertyType); + return this; + } + public >> EntityDefinitionBuilder addTranslator(MetadataType type, BiConsumer translateFunction) { translators.add(new EntityMetadataTranslator<>(type, translateFunction)); return this; @@ -216,12 +229,20 @@ public record EntityDefinition(EntityFactory factory, Entit } else if (this.identifier != null && type == null) { identifier = this.identifier.identifier(); } + GeyserEntityProperties registeredProperties = propertiesBuilder == null ? null : propertiesBuilder.build(); + EntityDefinition definition = new EntityDefinition<>(factory, type, identifier, width, height, offset, registeredProperties, translators); + if (register && definition.entityType() != null) { - EntityDefinition definition = new EntityDefinition<>(factory, type, this.identifier, width, height, offset, translators, custom); - if (register && identifier != null) { - EntityUtils.registerEntity(identifier, definition); + Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition); + Registries.JAVA_ENTITY_IDENTIFIERS.get().putIfAbsent("minecraft:" + type.name().toLowerCase(Locale.ROOT), definition); } + // TODO + //EntityDefinition definition = new EntityDefinition<>(factory, type, this.identifier, width, height, offset, translators, custom); + //if (register && identifier != null) { + // EntityUtils.registerEntity(identifier, definition); + //} + return definition; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 4aa8d8aef..74f1c8907 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -25,56 +25,196 @@ package org.geysermc.geyser.entity; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.geysermc.geyser.entity.type.*; -import org.geysermc.geyser.entity.type.living.*; -import org.geysermc.geyser.entity.type.living.animal.*; -import org.geysermc.geyser.entity.type.living.animal.horse.*; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; +import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.entity.factory.EntityFactory; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.entity.properties.type.FloatProperty; +import org.geysermc.geyser.entity.properties.type.IntProperty; +import org.geysermc.geyser.entity.properties.type.PropertyType; +import org.geysermc.geyser.entity.properties.type.StringEnumProperty; +import org.geysermc.geyser.entity.type.AbstractArrowEntity; +import org.geysermc.geyser.entity.type.AbstractWindChargeEntity; +import org.geysermc.geyser.entity.type.AreaEffectCloudEntity; +import org.geysermc.geyser.entity.type.ArrowEntity; +import org.geysermc.geyser.entity.type.BoatEntity; +import org.geysermc.geyser.entity.type.ChestBoatEntity; +import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; +import org.geysermc.geyser.entity.type.DisplayBaseEntity; +import org.geysermc.geyser.entity.type.EnderCrystalEntity; +import org.geysermc.geyser.entity.type.EnderEyeEntity; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.EvokerFangsEntity; +import org.geysermc.geyser.entity.type.ExpOrbEntity; +import org.geysermc.geyser.entity.type.FallingBlockEntity; +import org.geysermc.geyser.entity.type.FireballEntity; +import org.geysermc.geyser.entity.type.FireworkEntity; +import org.geysermc.geyser.entity.type.FishingHookEntity; +import org.geysermc.geyser.entity.type.FurnaceMinecartEntity; +import org.geysermc.geyser.entity.type.HangingEntity; +import org.geysermc.geyser.entity.type.InteractionEntity; +import org.geysermc.geyser.entity.type.ItemEntity; +import org.geysermc.geyser.entity.type.ItemFrameEntity; +import org.geysermc.geyser.entity.type.LeashKnotEntity; +import org.geysermc.geyser.entity.type.LightningEntity; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.entity.type.MinecartEntity; +import org.geysermc.geyser.entity.type.PaintingEntity; +import org.geysermc.geyser.entity.type.SpawnerMinecartEntity; +import org.geysermc.geyser.entity.type.TNTEntity; +import org.geysermc.geyser.entity.type.TextDisplayEntity; +import org.geysermc.geyser.entity.type.ThrowableEggEntity; +import org.geysermc.geyser.entity.type.ThrowableEntity; +import org.geysermc.geyser.entity.type.ThrowableItemEntity; +import org.geysermc.geyser.entity.type.ThrownPotionEntity; +import org.geysermc.geyser.entity.type.TridentEntity; +import org.geysermc.geyser.entity.type.WitherSkullEntity; +import org.geysermc.geyser.entity.type.living.AbstractFishEntity; +import org.geysermc.geyser.entity.type.living.AgeableEntity; +import org.geysermc.geyser.entity.type.living.AllayEntity; +import org.geysermc.geyser.entity.type.living.ArmorStandEntity; +import org.geysermc.geyser.entity.type.living.BatEntity; +import org.geysermc.geyser.entity.type.living.CopperGolemEntity; +import org.geysermc.geyser.entity.type.living.DolphinEntity; +import org.geysermc.geyser.entity.type.living.GlowSquidEntity; +import org.geysermc.geyser.entity.type.living.IronGolemEntity; +import org.geysermc.geyser.entity.type.living.MagmaCubeEntity; +import org.geysermc.geyser.entity.type.living.MobEntity; +import org.geysermc.geyser.entity.type.living.SlimeEntity; +import org.geysermc.geyser.entity.type.living.SnowGolemEntity; +import org.geysermc.geyser.entity.type.living.SquidEntity; +import org.geysermc.geyser.entity.type.living.TadpoleEntity; +import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity; +import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity; +import org.geysermc.geyser.entity.type.living.animal.BeeEntity; +import org.geysermc.geyser.entity.type.living.animal.FoxEntity; +import org.geysermc.geyser.entity.type.living.animal.FrogEntity; +import org.geysermc.geyser.entity.type.living.animal.GoatEntity; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; +import org.geysermc.geyser.entity.type.living.animal.HoglinEntity; +import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity; +import org.geysermc.geyser.entity.type.living.animal.OcelotEntity; +import org.geysermc.geyser.entity.type.living.animal.PandaEntity; +import org.geysermc.geyser.entity.type.living.animal.PolarBearEntity; +import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity; +import org.geysermc.geyser.entity.type.living.animal.RabbitEntity; +import org.geysermc.geyser.entity.type.living.animal.SheepEntity; +import org.geysermc.geyser.entity.type.living.animal.SnifferEntity; +import org.geysermc.geyser.entity.type.living.animal.StriderEntity; +import org.geysermc.geyser.entity.type.living.animal.TropicalFishEntity; +import org.geysermc.geyser.entity.type.living.animal.TurtleEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.ChestedHorseEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.HorseEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.LlamaEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.SkeletonHorseEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.TraderLlamaEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.ZombieHorseEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.TameableEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.entity.type.living.merchant.AbstractMerchantEntity; import org.geysermc.geyser.entity.type.living.merchant.VillagerEntity; -import org.geysermc.geyser.entity.type.living.monster.*; +import org.geysermc.geyser.entity.type.living.monster.AbstractSkeletonEntity; +import org.geysermc.geyser.entity.type.living.monster.BasePiglinEntity; +import org.geysermc.geyser.entity.type.living.monster.BlazeEntity; +import org.geysermc.geyser.entity.type.living.monster.BoggedEntity; +import org.geysermc.geyser.entity.type.living.monster.BreezeEntity; +import org.geysermc.geyser.entity.type.living.monster.CreakingEntity; +import org.geysermc.geyser.entity.type.living.monster.CreeperEntity; +import org.geysermc.geyser.entity.type.living.monster.ElderGuardianEntity; +import org.geysermc.geyser.entity.type.living.monster.EnderDragonEntity; +import org.geysermc.geyser.entity.type.living.monster.EnderDragonPartEntity; +import org.geysermc.geyser.entity.type.living.monster.EndermanEntity; +import org.geysermc.geyser.entity.type.living.monster.GhastEntity; +import org.geysermc.geyser.entity.type.living.monster.GiantEntity; +import org.geysermc.geyser.entity.type.living.monster.GuardianEntity; +import org.geysermc.geyser.entity.type.living.monster.MonsterEntity; +import org.geysermc.geyser.entity.type.living.monster.PhantomEntity; +import org.geysermc.geyser.entity.type.living.monster.PiglinEntity; +import org.geysermc.geyser.entity.type.living.monster.ShulkerEntity; +import org.geysermc.geyser.entity.type.living.monster.SkeletonEntity; +import org.geysermc.geyser.entity.type.living.monster.SpiderEntity; +import org.geysermc.geyser.entity.type.living.monster.VexEntity; +import org.geysermc.geyser.entity.type.living.monster.WardenEntity; +import org.geysermc.geyser.entity.type.living.monster.WitherEntity; +import org.geysermc.geyser.entity.type.living.monster.ZoglinEntity; +import org.geysermc.geyser.entity.type.living.monster.ZombieEntity; +import org.geysermc.geyser.entity.type.living.monster.ZombieVillagerEntity; +import org.geysermc.geyser.entity.type.living.monster.ZombifiedPiglinEntity; import org.geysermc.geyser.entity.type.living.monster.raid.PillagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.RaidParticipantEntity; +import org.geysermc.geyser.entity.type.living.monster.raid.RavagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.VindicatorEntity; +import org.geysermc.geyser.entity.type.player.AvatarEntity; +import org.geysermc.geyser.entity.type.player.MannequinEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataTypes; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; public final class EntityDefinitions { + public static final EntityDefinition ACACIA_BOAT; + public static final EntityDefinition ACACIA_CHEST_BOAT; public static final EntityDefinition ALLAY; public static final EntityDefinition AREA_EFFECT_CLOUD; + public static final EntityDefinition ARMADILLO; public static final EntityDefinition ARMOR_STAND; - public static final EntityDefinition ARROW; + public static final EntityDefinition ARROW; public static final EntityDefinition AXOLOTL; + public static final EntityDefinition BAMBOO_RAFT; + public static final EntityDefinition BAMBOO_CHEST_RAFT; public static final EntityDefinition BAT; public static final EntityDefinition BEE; + public static final EntityDefinition BIRCH_BOAT; + public static final EntityDefinition BIRCH_CHEST_BOAT; public static final EntityDefinition BLAZE; - public static final EntityDefinition BOAT; + public static final EntityDefinition BOGGED; + public static final EntityDefinition BREEZE; + public static final EntityDefinition BREEZE_WIND_CHARGE; public static final EntityDefinition CAMEL; public static final EntityDefinition CAT; public static final EntityDefinition CAVE_SPIDER; + public static final EntityDefinition CHERRY_BOAT; + public static final EntityDefinition CHERRY_CHEST_BOAT; public static final EntityDefinition CHEST_MINECART; public static final EntityDefinition CHICKEN; - public static final EntityDefinition CHEST_BOAT; public static final EntityDefinition COD; + public static final EntityDefinition COPPER_GOLEM; public static final EntityDefinition COMMAND_BLOCK_MINECART; public static final EntityDefinition COW; + public static final EntityDefinition CREAKING; public static final EntityDefinition CREEPER; + public static final EntityDefinition DARK_OAK_BOAT; + public static final EntityDefinition DARK_OAK_CHEST_BOAT; public static final EntityDefinition DOLPHIN; public static final EntityDefinition DONKEY; public static final EntityDefinition DRAGON_FIREBALL; public static final EntityDefinition DROWNED; - public static final EntityDefinition EGG; + public static final EntityDefinition EGG; public static final EntityDefinition ELDER_GUARDIAN; public static final EntityDefinition ENDERMAN; public static final EntityDefinition ENDERMITE; @@ -85,7 +225,7 @@ public final class EntityDefinitions { public static final EntityDefinition EVOKER_FANGS; public static final EntityDefinition EXPERIENCE_BOTTLE; public static final EntityDefinition EXPERIENCE_ORB; - public static final EntityDefinition EYE_OF_ENDER; + public static final EntityDefinition EYE_OF_ENDER; public static final EntityDefinition FALLING_BLOCK; public static final EntityDefinition FIREBALL; public static final EntityDefinition FIREWORK_ROCKET; @@ -99,6 +239,7 @@ public final class EntityDefinitions { public static final EntityDefinition GLOW_SQUID; public static final EntityDefinition GOAT; public static final EntityDefinition GUARDIAN; + public static final EntityDefinition HAPPY_GHAST; public static final EntityDefinition HOGLIN; public static final EntityDefinition HOPPER_MINECART; public static final EntityDefinition HORSE; @@ -108,16 +249,25 @@ public final class EntityDefinitions { public static final EntityDefinition IRON_GOLEM; public static final EntityDefinition ITEM; public static final EntityDefinition ITEM_FRAME; + public static final EntityDefinition JUNGLE_BOAT; + public static final EntityDefinition JUNGLE_CHEST_BOAT; public static final EntityDefinition LEASH_KNOT; public static final EntityDefinition LIGHTNING_BOLT; public static final EntityDefinition LLAMA; public static final EntityDefinition LLAMA_SPIT; public static final EntityDefinition MAGMA_CUBE; + public static final EntityDefinition MANGROVE_BOAT; + public static final EntityDefinition MANGROVE_CHEST_BOAT; + public static final EntityDefinition MANNEQUIN; public static final EntityDefinition MINECART; public static final EntityDefinition MOOSHROOM; public static final EntityDefinition MULE; + public static final EntityDefinition OAK_BOAT; + public static final EntityDefinition OAK_CHEST_BOAT; public static final EntityDefinition OCELOT; public static final EntityDefinition PAINTING; + public static final EntityDefinition PALE_OAK_BOAT; + public static final EntityDefinition PALE_OAK_CHEST_BOAT; public static final EntityDefinition PANDA; public static final EntityDefinition PARROT; public static final EntityDefinition PHANTOM; @@ -127,10 +277,11 @@ public final class EntityDefinitions { public static final EntityDefinition PILLAGER; public static final EntityDefinition PLAYER; public static final EntityDefinition POLAR_BEAR; - public static final EntityDefinition POTION; + public static final EntityDefinition SPLASH_POTION; + public static final EntityDefinition LINGERING_POTION; public static final EntityDefinition PUFFERFISH; public static final EntityDefinition RABBIT; - public static final EntityDefinition RAVAGER; + public static final EntityDefinition RAVAGER; public static final EntityDefinition SALMON; public static final EntityDefinition SHEEP; public static final EntityDefinition SHULKER; @@ -146,6 +297,8 @@ public final class EntityDefinitions { public static final EntityDefinition SPAWNER_MINECART; // Not present on Bedrock public static final EntityDefinition SPECTRAL_ARROW; public static final EntityDefinition SPIDER; + public static final EntityDefinition SPRUCE_BOAT; + public static final EntityDefinition SPRUCE_CHEST_BOAT; public static final EntityDefinition SQUID; public static final EntityDefinition STRAY; public static final EntityDefinition STRIDER; @@ -162,6 +315,7 @@ public final class EntityDefinitions { public static final EntityDefinition VINDICATOR; public static final EntityDefinition WANDERING_TRADER; public static final EntityDefinition WARDEN; + public static final EntityDefinition WIND_CHARGE; public static final EntityDefinition WITCH; public static final EntityDefinition WITHER; public static final EntityDefinition WITHER_SKELETON; @@ -184,14 +338,14 @@ public final class EntityDefinitions { static { EntityDefinition entityBase = EntityDefinition.builder(Entity::new) - .addTranslator(MetadataType.BYTE, Entity::setFlags) - .addTranslator(MetadataType.INT, Entity::setAir) // Air/bubbles - .addTranslator(MetadataType.OPTIONAL_CHAT, Entity::setDisplayName) - .addTranslator(MetadataType.BOOLEAN, Entity::setDisplayNameVisible) - .addTranslator(MetadataType.BOOLEAN, Entity::setSilent) - .addTranslator(MetadataType.BOOLEAN, Entity::setGravity) - .addTranslator(MetadataType.POSE, (entity, entityMetadata) -> entity.setPose(entityMetadata.getValue())) - .addTranslator(MetadataType.INT, Entity::setFreezing) + .addTranslator(MetadataTypes.BYTE, Entity::setFlags) + .addTranslator(MetadataTypes.INT, Entity::setAir) // Air/bubbles + .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, Entity::setDisplayName) + .addTranslator(MetadataTypes.BOOLEAN, Entity::setDisplayNameVisible) + .addTranslator(MetadataTypes.BOOLEAN, Entity::setSilent) + .addTranslator(MetadataTypes.BOOLEAN, Entity::setGravity) + .addTranslator(MetadataTypes.POSE, (entity, entityMetadata) -> entity.setPose(entityMetadata.getValue())) + .addTranslator(MetadataTypes.INT, Entity::setFreezing) .build(); // Extends entity @@ -199,27 +353,9 @@ public final class EntityDefinitions { AREA_EFFECT_CLOUD = EntityDefinition.inherited(AreaEffectCloudEntity::new, entityBase) .type(EntityType.AREA_EFFECT_CLOUD) .height(0.5f).width(1.0f) - .addTranslator(MetadataType.FLOAT, AreaEffectCloudEntity::setRadius) - .addTranslator(MetadataType.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.EFFECT_COLOR, entityMetadata.getValue())) + .addTranslator(MetadataTypes.FLOAT, AreaEffectCloudEntity::setRadius) .addTranslator(null) // Waiting - .addTranslator(MetadataType.PARTICLE, AreaEffectCloudEntity::setParticle) - .build(); - BOAT = EntityDefinition.inherited(BoatEntity::new, entityBase) - .type(EntityType.BOAT) - .height(0.6f).width(1.6f) - .offset(0.35f) - .addTranslator(MetadataType.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_TICKS, entityMetadata.getValue())) // Time since last hit - .addTranslator(MetadataType.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_DIRECTION, entityMetadata.getValue())) // Rocking direction - .addTranslator(MetadataType.FLOAT, (boatEntity, entityMetadata) -> - // 'Health' in Bedrock, damage taken in Java - it makes motion in Bedrock - boatEntity.getDirtyMetadata().put(EntityDataTypes.STRUCTURAL_INTEGRITY, 40 - ((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue()))) - .addTranslator(MetadataType.INT, BoatEntity::setVariant) - .addTranslator(MetadataType.BOOLEAN, BoatEntity::setPaddlingLeft) - .addTranslator(MetadataType.BOOLEAN, BoatEntity::setPaddlingRight) - .addTranslator(MetadataType.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.BOAT_BUBBLE_TIME, entityMetadata.getValue())) // May not actually do anything - .build(); - CHEST_BOAT = EntityDefinition.inherited(ChestBoatEntity::new, BOAT) - .type(EntityType.CHEST_BOAT) + .addTranslator(MetadataTypes.PARTICLE, AreaEffectCloudEntity::setParticle) .build(); DRAGON_FIREBALL = EntityDefinition.inherited(FireballEntity::new, entityBase) .type(EntityType.DRAGON_FIREBALL) @@ -229,12 +365,13 @@ public final class EntityDefinitions { .type(EntityType.END_CRYSTAL) .heightAndWidth(2.0f) .identifier("minecraft:ender_crystal") - .addTranslator(MetadataType.OPTIONAL_POSITION, EnderCrystalEntity::setBlockTarget) - .addTranslator(MetadataType.BOOLEAN, + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, EnderCrystalEntity::setBlockTarget) + .addTranslator(MetadataTypes.BOOLEAN, (enderCrystalEntity, entityMetadata) -> enderCrystalEntity.setFlag(EntityFlag.SHOW_BOTTOM, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) // There is a base located on the ender crystal .build(); - EXPERIENCE_ORB = EntityDefinition.inherited(null, entityBase) + EXPERIENCE_ORB = EntityDefinition.inherited(ExpOrbEntity::new, entityBase) .type(EntityType.EXPERIENCE_ORB) + .addTranslator(null) // int determining xb orb texture .identifier("minecraft:xp_orb") .build(); EVOKER_FANGS = EntityDefinition.inherited(EvokerFangsEntity::new, entityBase) @@ -242,7 +379,7 @@ public final class EntityDefinitions { .height(0.8f).width(0.5f) .identifier("minecraft:evocation_fang") .build(); - EYE_OF_ENDER = EntityDefinition.inherited(Entity::new, entityBase) + EYE_OF_ENDER = EntityDefinition.inherited(EnderEyeEntity::new, entityBase) .type(EntityType.EYE_OF_ENDER) .heightAndWidth(0.25f) .identifier("minecraft:eye_of_ender_signal") @@ -257,21 +394,21 @@ public final class EntityDefinitions { .type(EntityType.FIREWORK_ROCKET) .heightAndWidth(0.25f) .identifier("minecraft:fireworks_rocket") - .addTranslator(MetadataType.ITEM, FireworkEntity::setFireworkItem) - .addTranslator(MetadataType.OPTIONAL_VARINT, FireworkEntity::setPlayerGliding) + .addTranslator(MetadataTypes.ITEM_STACK, FireworkEntity::setFireworkItem) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FireworkEntity::setPlayerGliding) .addTranslator(null) // Shot at angle .build(); FISHING_BOBBER = EntityDefinition.inherited(null, entityBase) .type(EntityType.FISHING_BOBBER) .identifier("minecraft:fishing_hook") - .addTranslator(MetadataType.INT, FishingHookEntity::setHookedEntity) + .addTranslator(MetadataTypes.INT, FishingHookEntity::setHookedEntity) .addTranslator(null) // Biting TODO check .build(); ITEM = EntityDefinition.inherited(ItemEntity::new, entityBase) .type(EntityType.ITEM) .heightAndWidth(0.25f) .offset(0.125f) - .addTranslator(MetadataType.ITEM, ItemEntity::setItem) + .addTranslator(MetadataTypes.ITEM_STACK, ItemEntity::setItem) .build(); LEASH_KNOT = EntityDefinition.inherited(LeashKnotEntity::new, entityBase) .type(EntityType.LEASH_KNOT) @@ -284,10 +421,6 @@ public final class EntityDefinitions { .type(EntityType.LLAMA_SPIT) .heightAndWidth(0.25f) .build(); - PAINTING = EntityDefinition.inherited(null, entityBase) - .type(EntityType.PAINTING) - .addTranslator(MetadataType.PAINTING_VARIANT, PaintingEntity::setPaintingType) - .build(); SHULKER_BULLET = EntityDefinition.inherited(ThrowableEntity::new, entityBase) .type(EntityType.SHULKER_BULLET) .heightAndWidth(0.3125f) @@ -295,14 +428,15 @@ public final class EntityDefinitions { TNT = EntityDefinition.inherited(TNTEntity::new, entityBase) .type(EntityType.TNT) .heightAndWidth(0.98f) - .addTranslator(MetadataType.INT, TNTEntity::setFuseLength) + .offset(0.49f) + .addTranslator(MetadataTypes.INT, TNTEntity::setFuseLength) .build(); - EntityDefinition displayBase = EntityDefinition.inherited(entityBase.factory(), entityBase) + EntityDefinition displayBase = EntityDefinition.inherited(DisplayBaseEntity::new, entityBase) .addTranslator(null) // Interpolation delay .addTranslator(null) // Transformation interpolation duration .addTranslator(null) // Position/Rotation interpolation duration - .addTranslator(null) // Translation + .addTranslator(MetadataTypes.VECTOR3, DisplayBaseEntity::setTranslation) // Translation .addTranslator(null) // Scale .addTranslator(null) // Left rotation .addTranslator(null) // Right rotation @@ -318,7 +452,8 @@ public final class EntityDefinitions { TEXT_DISPLAY = EntityDefinition.inherited(TextDisplayEntity::new, displayBase) .type(EntityType.TEXT_DISPLAY) .identifier("minecraft:armor_stand") - .addTranslator(MetadataType.CHAT, TextDisplayEntity::setText) + .offset(-0.5f) + .addTranslator(MetadataTypes.COMPONENT, TextDisplayEntity::setText) .addTranslator(null) // Line width .addTranslator(null) // Background color .addTranslator(null) // Text opacity @@ -329,9 +464,9 @@ public final class EntityDefinitions { .type(EntityType.INTERACTION) .heightAndWidth(1.0f) // default size until server specifies otherwise .identifier("minecraft:armor_stand") - .addTranslator(MetadataType.FLOAT, InteractionEntity::setWidth) - .addTranslator(MetadataType.FLOAT, InteractionEntity::setHeight) - .addTranslator(MetadataType.BOOLEAN, InteractionEntity::setResponse) + .addTranslator(MetadataTypes.FLOAT, InteractionEntity::setWidth) + .addTranslator(MetadataTypes.FLOAT, InteractionEntity::setHeight) + .addTranslator(MetadataTypes.BOOLEAN, InteractionEntity::setResponse) .build(); EntityDefinition fireballBase = EntityDefinition.inherited(FireballEntity::new, entityBase) @@ -347,11 +482,12 @@ public final class EntityDefinitions { .build(); EntityDefinition throwableItemBase = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase) - .addTranslator(MetadataType.ITEM, ThrowableItemEntity::setItem) + .addTranslator(MetadataTypes.ITEM_STACK, ThrowableItemEntity::setItem) .build(); - EGG = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) + EGG = EntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase) .type(EntityType.EGG) .heightAndWidth(0.25f) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) .build(); ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.ENDER_PEARL) @@ -362,24 +498,42 @@ public final class EntityDefinitions { .heightAndWidth(0.25f) .identifier("minecraft:xp_bottle") .build(); - POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) - .type(EntityType.POTION) + SPLASH_POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) + .type(EntityType.SPLASH_POTION) .heightAndWidth(0.25f) .identifier("minecraft:splash_potion") .build(); + LINGERING_POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) + .type(EntityType.LINGERING_POTION) + .heightAndWidth(0.25f) + .identifier("minecraft:splash_potion") + .build(); SNOWBALL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.SNOWBALL) .heightAndWidth(0.25f) .build(); - EntityDefinition abstractArrowBase = EntityDefinition.inherited(AbstractArrowEntity::new, entityBase) - .addTranslator(MetadataType.BYTE, AbstractArrowEntity::setArrowFlags) - .addTranslator(null) // "Piercing level" + EntityFactory windChargeSupplier = AbstractWindChargeEntity::new; + BREEZE_WIND_CHARGE = EntityDefinition.inherited(windChargeSupplier, entityBase) + .type(EntityType.BREEZE_WIND_CHARGE) + .identifier("minecraft:breeze_wind_charge_projectile") + .heightAndWidth(0.3125f) .build(); - ARROW = EntityDefinition.inherited(TippedArrowEntity::new, abstractArrowBase) + WIND_CHARGE = EntityDefinition.inherited(windChargeSupplier, entityBase) + .type(EntityType.WIND_CHARGE) + .identifier("minecraft:wind_charge_projectile") + .heightAndWidth(0.3125f) + .build(); + + EntityDefinition abstractArrowBase = EntityDefinition.inherited(AbstractArrowEntity::new, entityBase) + .addTranslator(MetadataTypes.BYTE, AbstractArrowEntity::setArrowFlags) + .addTranslator(null) // "Piercing level" + .addTranslator(null) // If the arrow is in the ground + .build(); + ARROW = EntityDefinition.inherited(ArrowEntity::new, abstractArrowBase) .type(EntityType.ARROW) .heightAndWidth(0.25f) - .addTranslator(MetadataType.INT, TippedArrowEntity::setPotionEffectColor) + .addTranslator(MetadataTypes.INT, ArrowEntity::setPotionEffectColor) .build(); SPECTRAL_ARROW = EntityDefinition.inherited(abstractArrowBase.factory(), abstractArrowBase) .type(EntityType.SPECTRAL_ARROW) @@ -390,14 +544,23 @@ public final class EntityDefinitions { .type(EntityType.TRIDENT) .identifier("minecraft:thrown_trident") .addTranslator(null) // Loyalty - .addTranslator(MetadataType.BOOLEAN, (tridentEntity, entityMetadata) -> tridentEntity.setFlag(EntityFlag.ENCHANTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .addTranslator(MetadataTypes.BOOLEAN, (tridentEntity, entityMetadata) -> tridentEntity.setFlag(EntityFlag.ENCHANTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); + EntityDefinition hangingEntityBase = EntityDefinition.inherited(null, entityBase) + .addTranslator(MetadataTypes.DIRECTION, HangingEntity::setDirectionMetadata) + .build(); + + PAINTING = EntityDefinition.inherited(PaintingEntity::new, hangingEntityBase) + .type(EntityType.PAINTING) + .addTranslator(MetadataTypes.PAINTING_VARIANT, PaintingEntity::setPaintingType) + .build(); + // Item frames are handled differently as they are blocks, not items, in Bedrock - ITEM_FRAME = EntityDefinition.inherited(null, entityBase) + ITEM_FRAME = EntityDefinition.inherited(ItemFrameEntity::new, hangingEntityBase) .type(EntityType.ITEM_FRAME) - .addTranslator(MetadataType.ITEM, ItemFrameEntity::setItemInFrame) - .addTranslator(MetadataType.INT, ItemFrameEntity::setItemRotation) + .addTranslator(MetadataTypes.ITEM_STACK, ItemFrameEntity::setItemInFrame) + .addTranslator(MetadataTypes.INT, ItemFrameEntity::setItemRotation) .build(); GLOW_ITEM_FRAME = EntityDefinition.inherited(ITEM_FRAME.factory(), ITEM_FRAME) .type(EntityType.GLOW_ITEM_FRAME) @@ -407,27 +570,26 @@ public final class EntityDefinitions { .type(EntityType.MINECART) .height(0.7f).width(0.98f) .offset(0.35f) - .addTranslator(MetadataType.INT, (minecartEntity, entityMetadata) -> minecartEntity.getDirtyMetadata().put(EntityDataTypes.STRUCTURAL_INTEGRITY, entityMetadata.getValue())) - .addTranslator(MetadataType.INT, (minecartEntity, entityMetadata) -> minecartEntity.getDirtyMetadata().put(EntityDataTypes.HURT_DIRECTION, entityMetadata.getValue())) // Direction in which the minecart is shaking - .addTranslator(MetadataType.FLOAT, (minecartEntity, entityMetadata) -> + .addTranslator(MetadataTypes.INT, (minecartEntity, entityMetadata) -> minecartEntity.getDirtyMetadata().put(EntityDataTypes.STRUCTURAL_INTEGRITY, entityMetadata.getValue())) + .addTranslator(MetadataTypes.INT, (minecartEntity, entityMetadata) -> minecartEntity.getDirtyMetadata().put(EntityDataTypes.HURT_DIRECTION, entityMetadata.getValue())) // Direction in which the minecart is shaking + .addTranslator(MetadataTypes.FLOAT, (minecartEntity, entityMetadata) -> // Power in Java, hurt ticks in Bedrock minecartEntity.getDirtyMetadata().put(EntityDataTypes.HURT_TICKS, Math.min((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue(), 15))) - .addTranslator(MetadataType.INT, MinecartEntity::setCustomBlock) - .addTranslator(MetadataType.INT, MinecartEntity::setCustomBlockOffset) - .addTranslator(MetadataType.BOOLEAN, MinecartEntity::setShowCustomBlock) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_STATE, MinecartEntity::setCustomBlock) + .addTranslator(MetadataTypes.INT, MinecartEntity::setCustomBlockOffset) .build(); CHEST_MINECART = EntityDefinition.inherited(MINECART.factory(), MINECART) .type(EntityType.CHEST_MINECART) .build(); COMMAND_BLOCK_MINECART = EntityDefinition.inherited(CommandBlockMinecartEntity::new, MINECART) .type(EntityType.COMMAND_BLOCK_MINECART) - .addTranslator(MetadataType.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_NAME, entityMetadata.getValue())) - .addTranslator(MetadataType.CHAT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue()))) + .addTranslator(MetadataTypes.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_NAME, entityMetadata.getValue())) + .addTranslator(MetadataTypes.COMPONENT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue()))) .build(); FURNACE_MINECART = EntityDefinition.inherited(FurnaceMinecartEntity::new, MINECART) .type(EntityType.FURNACE_MINECART) .identifier("minecraft:minecart") - .addTranslator(MetadataType.BOOLEAN, FurnaceMinecartEntity::setHasFuel) + .addTranslator(MetadataTypes.BOOLEAN, FurnaceMinecartEntity::setHasFuel) .build(); HOPPER_MINECART = EntityDefinition.inherited(MINECART.factory(), MINECART) .type(EntityType.HOPPER_MINECART) @@ -443,49 +605,100 @@ public final class EntityDefinitions { WITHER_SKULL = EntityDefinition.inherited(WitherSkullEntity::new, entityBase) .type(EntityType.WITHER_SKULL) .heightAndWidth(0.3125f) - .addTranslator(MetadataType.BOOLEAN, WitherSkullEntity::setDangerous) + .addTranslator(MetadataTypes.BOOLEAN, WitherSkullEntity::setDangerous) .build(); WITHER_SKULL_DANGEROUS = EntityDefinition.inherited(WITHER_SKULL.factory(), WITHER_SKULL) .build(false); } + // Boats + { + EntityDefinition boatBase = EntityDefinition.inherited(null, entityBase) + .height(0.6f).width(1.6f) + .offset(0.35f) + .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_TICKS, entityMetadata.getValue())) // Time since last hit + .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_DIRECTION, entityMetadata.getValue())) // Rocking direction + .addTranslator(MetadataTypes.FLOAT, (boatEntity, entityMetadata) -> + // 'Health' in Bedrock, damage taken in Java - it makes motion in Bedrock + boatEntity.getDirtyMetadata().put(EntityDataTypes.STRUCTURAL_INTEGRITY, 40 - ((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue()))) + .addTranslator(MetadataTypes.BOOLEAN, BoatEntity::setPaddlingLeft) + .addTranslator(MetadataTypes.BOOLEAN, BoatEntity::setPaddlingRight) + .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.BOAT_BUBBLE_TIME, entityMetadata.getValue())) // May not actually do anything + .build(); + + ACACIA_BOAT = buildBoat(boatBase, EntityType.ACACIA_BOAT, BoatEntity.BoatVariant.ACACIA); + BAMBOO_RAFT = buildBoat(boatBase, EntityType.BAMBOO_RAFT, BoatEntity.BoatVariant.BAMBOO); + BIRCH_BOAT = buildBoat(boatBase, EntityType.BIRCH_BOAT, BoatEntity.BoatVariant.BIRCH); + CHERRY_BOAT = buildBoat(boatBase, EntityType.CHERRY_BOAT, BoatEntity.BoatVariant.CHERRY); + DARK_OAK_BOAT = buildBoat(boatBase, EntityType.DARK_OAK_BOAT, BoatEntity.BoatVariant.DARK_OAK); + JUNGLE_BOAT = buildBoat(boatBase, EntityType.JUNGLE_BOAT, BoatEntity.BoatVariant.JUNGLE); + MANGROVE_BOAT = buildBoat(boatBase, EntityType.MANGROVE_BOAT, BoatEntity.BoatVariant.MANGROVE); + OAK_BOAT = buildBoat(boatBase, EntityType.OAK_BOAT, BoatEntity.BoatVariant.OAK); + SPRUCE_BOAT = buildBoat(boatBase, EntityType.SPRUCE_BOAT, BoatEntity.BoatVariant.SPRUCE); + PALE_OAK_BOAT = buildBoat(boatBase, EntityType.PALE_OAK_BOAT, BoatEntity.BoatVariant.PALE_OAK); + + EntityDefinition chestBoatBase = EntityDefinition.inherited(null, boatBase) + .build(); + + ACACIA_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.ACACIA_CHEST_BOAT, BoatEntity.BoatVariant.ACACIA); + BAMBOO_CHEST_RAFT = buildChestBoat(chestBoatBase, EntityType.BAMBOO_CHEST_RAFT, BoatEntity.BoatVariant.BAMBOO); + BIRCH_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.BIRCH_CHEST_BOAT, BoatEntity.BoatVariant.BIRCH); + CHERRY_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.CHERRY_CHEST_BOAT, BoatEntity.BoatVariant.CHERRY); + DARK_OAK_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.DARK_OAK_CHEST_BOAT, BoatEntity.BoatVariant.DARK_OAK); + JUNGLE_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.JUNGLE_CHEST_BOAT, BoatEntity.BoatVariant.JUNGLE); + MANGROVE_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.MANGROVE_CHEST_BOAT, BoatEntity.BoatVariant.MANGROVE); + OAK_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.OAK_CHEST_BOAT, BoatEntity.BoatVariant.OAK); + SPRUCE_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.SPRUCE_CHEST_BOAT, BoatEntity.BoatVariant.SPRUCE); + PALE_OAK_CHEST_BOAT = buildChestBoat(chestBoatBase, EntityType.PALE_OAK_CHEST_BOAT, BoatEntity.BoatVariant.PALE_OAK); + } + EntityDefinition livingEntityBase = EntityDefinition.inherited(LivingEntity::new, entityBase) - .addTranslator(MetadataType.BYTE, LivingEntity::setLivingEntityFlags) - .addTranslator(MetadataType.FLOAT, LivingEntity::setHealth) - .addTranslator(MetadataType.INT, - (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityDataTypes.EFFECT_COLOR, entityMetadata.getValue())) - .addTranslator(MetadataType.BOOLEAN, + .addTranslator(MetadataTypes.BYTE, LivingEntity::setLivingEntityFlags) + .addTranslator(MetadataTypes.FLOAT, LivingEntity::setHealth) + .addTranslator(MetadataTypes.PARTICLES, LivingEntity::setParticles) + .addTranslator(MetadataTypes.BOOLEAN, (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityDataTypes.EFFECT_AMBIENCE, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0))) .addTranslator(null) // Arrow count .addTranslator(null) // Stinger count - .addTranslator(MetadataType.OPTIONAL_POSITION, LivingEntity::setBedPosition) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, LivingEntity::setBedPosition) .build(); ARMOR_STAND = EntityDefinition.inherited(ArmorStandEntity::new, livingEntityBase) .type(EntityType.ARMOR_STAND) .height(1.975f).width(0.5f) - .addTranslator(MetadataType.BYTE, ArmorStandEntity::setArmorStandFlags) - .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setHeadRotation) - .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setBodyRotation) - .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setLeftArmRotation) - .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setRightArmRotation) - .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setLeftLegRotation) - .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setRightLegRotation) + .addTranslator(MetadataTypes.BYTE, ArmorStandEntity::setArmorStandFlags) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setHeadRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setBodyRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftArmRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightArmRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .build(); - PLAYER = EntityDefinition.inherited(null, livingEntityBase) + + EntityDefinition avatarEntityBase = EntityDefinition.inherited(null, livingEntityBase) + .height(1.8f).width(0.6f) + .offset(1.62f) + .addTranslator(null) // Player main hand + .addTranslator(MetadataTypes.BYTE, AvatarEntity::setSkinVisibility) + .build(); + + MANNEQUIN = EntityDefinition.inherited(MannequinEntity::new, avatarEntityBase) + .type(EntityType.MANNEQUIN) + .addTranslator(MetadataTypes.RESOLVABLE_PROFILE, MannequinEntity::setProfile) + .addTranslator(null) // Immovable + .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, MannequinEntity::setDescription) + .build(); + + PLAYER = EntityDefinition.inherited(null, avatarEntityBase) .type(EntityType.PLAYER) - .height(1.8f).width(0.6f) - .offset(1.62f) - .addTranslator(MetadataType.FLOAT, PlayerEntity::setAbsorptionHearts) + .addTranslator(MetadataTypes.FLOAT, PlayerEntity::setAbsorptionHearts) .addTranslator(null) // Player score - .addTranslator(MetadataType.BYTE, PlayerEntity::setSkinVisibility) - .addTranslator(null) // Player main hand - .addTranslator(MetadataType.NBT_TAG, PlayerEntity::setLeftParrot) - .addTranslator(MetadataType.NBT_TAG, PlayerEntity::setRightParrot) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setLeftParrot) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setRightParrot) .build(); EntityDefinition mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase) - .addTranslator(MetadataType.BYTE, MobEntity::setMobFlags) + .addTranslator(MetadataTypes.BYTE, MobEntity::setMobFlags) .build(); // Extends mob @@ -493,41 +706,61 @@ public final class EntityDefinitions { ALLAY = EntityDefinition.inherited(AllayEntity::new, mobEntityBase) .type(EntityType.ALLAY) .height(0.6f).width(0.35f) - .addTranslator(MetadataType.BOOLEAN, AllayEntity::setDancing) - .addTranslator(MetadataType.BOOLEAN, AllayEntity::setCanDuplicate) + .addTranslator(MetadataTypes.BOOLEAN, AllayEntity::setDancing) + .addTranslator(MetadataTypes.BOOLEAN, AllayEntity::setCanDuplicate) .build(); BAT = EntityDefinition.inherited(BatEntity::new, mobEntityBase) .type(EntityType.BAT) .height(0.9f).width(0.5f) - .addTranslator(MetadataType.BYTE, BatEntity::setBatFlags) + .addTranslator(MetadataTypes.BYTE, BatEntity::setBatFlags) + .build(); + BOGGED = EntityDefinition.inherited(BoggedEntity::new, mobEntityBase) + .type(EntityType.BOGGED) + .height(1.99f).width(0.6f) + .addTranslator(MetadataTypes.BOOLEAN, BoggedEntity::setSheared) .build(); BLAZE = EntityDefinition.inherited(BlazeEntity::new, mobEntityBase) .type(EntityType.BLAZE) .height(1.8f).width(0.6f) - .addTranslator(MetadataType.BYTE, BlazeEntity::setBlazeFlags) + .addTranslator(MetadataTypes.BYTE, BlazeEntity::setBlazeFlags) + .build(); + BREEZE = EntityDefinition.inherited(BreezeEntity::new, mobEntityBase) + .type(EntityType.BREEZE) + .height(1.77f).width(0.6f) + .build(); + COPPER_GOLEM = EntityDefinition.inherited(CopperGolemEntity::new, mobEntityBase) + .type(EntityType.COPPER_GOLEM) + .height(0.49f).width(0.98f) + .addTranslator(MetadataTypes.WEATHERING_COPPER_STATE, CopperGolemEntity::setWeatheringState) + .addTranslator(MetadataTypes.COPPER_GOLEM_STATE, CopperGolemEntity::setGolemState) + .property(CopperGolemEntity.CHEST_INTERACTION_PROPERTY) + .property(CopperGolemEntity.HAS_FLOWER_PROPERTY) + .property(CopperGolemEntity.OXIDATION_LEVEL_STATE_ENUM_PROPERTY) + .build(); + CREAKING = EntityDefinition.inherited(CreakingEntity::new, mobEntityBase) + .type(EntityType.CREAKING) + .height(2.7f).width(0.9f) + .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setCanMove) + .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive) + .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos) + .property(CreakingEntity.STATE_PROPERTY) + .property(CreakingEntity.SWAYING_TICKS_PROPERTY) .build(); CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase) .type(EntityType.CREEPER) .height(1.7f).width(0.6f) .offset(1.62f) - .addTranslator(MetadataType.INT, CreeperEntity::setSwelling) - .addTranslator(MetadataType.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.POWERED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) - .addTranslator(MetadataType.BOOLEAN, CreeperEntity::setIgnited) - .build(); - DOLPHIN = EntityDefinition.inherited(DolphinEntity::new, mobEntityBase) - .type(EntityType.DOLPHIN) - .height(0.6f).width(0.9f) - //TODO check - .addTranslator(null) // treasure position - .addTranslator(null) // "got fish" - .addTranslator(null) // "moistness level" + .addTranslator(MetadataTypes.INT, CreeperEntity::setSwelling) + .addTranslator(MetadataTypes.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.POWERED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .addTranslator(MetadataTypes.BOOLEAN, CreeperEntity::setIgnited) .build(); ENDERMAN = EntityDefinition.inherited(EndermanEntity::new, mobEntityBase) .type(EntityType.ENDERMAN) .height(2.9f).width(0.6f) - .addTranslator(MetadataType.OPTIONAL_BLOCK_STATE, EndermanEntity::setCarriedBlock) - .addTranslator(MetadataType.BOOLEAN, EndermanEntity::setScreaming) - .addTranslator(MetadataType.BOOLEAN, EndermanEntity::setAngry) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_STATE, EndermanEntity::setCarriedBlock) + .addTranslator(MetadataTypes.BOOLEAN, EndermanEntity::setScreaming) + .addTranslator(MetadataTypes.BOOLEAN, EndermanEntity::setAngry) .build(); ENDERMITE = EntityDefinition.inherited(MonsterEntity::new, mobEntityBase) .type(EntityType.ENDERMITE) @@ -535,12 +768,12 @@ public final class EntityDefinitions { .build(); ENDER_DRAGON = EntityDefinition.inherited(EnderDragonEntity::new, mobEntityBase) .type(EntityType.ENDER_DRAGON) - .addTranslator(MetadataType.INT, EnderDragonEntity::setPhase) + .addTranslator(MetadataTypes.INT, EnderDragonEntity::setPhase) .build(); GHAST = EntityDefinition.inherited(GhastEntity::new, mobEntityBase) .type(EntityType.GHAST) .heightAndWidth(4.0f) - .addTranslator(MetadataType.BOOLEAN, GhastEntity::setGhastAttacking) + .addTranslator(MetadataTypes.BOOLEAN, GhastEntity::setGhastAttacking) .build(); GIANT = EntityDefinition.inherited(GiantEntity::new, mobEntityBase) .type(EntityType.GIANT) @@ -557,7 +790,7 @@ public final class EntityDefinitions { .type(EntityType.PHANTOM) .height(0.5f).width(0.9f) .offset(0.6f) - .addTranslator(MetadataType.INT, PhantomEntity::setPhantomScale) + .addTranslator(MetadataTypes.INT, PhantomEntity::setPhantomScale) .build(); SILVERFISH = EntityDefinition.inherited(MonsterEntity::new, mobEntityBase) .type(EntityType.SILVERFISH) @@ -566,35 +799,31 @@ public final class EntityDefinitions { SHULKER = EntityDefinition.inherited(ShulkerEntity::new, mobEntityBase) .type(EntityType.SHULKER) .heightAndWidth(1f) - .addTranslator(MetadataType.DIRECTION, ShulkerEntity::setAttachedFace) - .addTranslator(MetadataType.BYTE, ShulkerEntity::setShulkerHeight) - .addTranslator(MetadataType.BYTE, ShulkerEntity::setShulkerColor) + .addTranslator(MetadataTypes.DIRECTION, ShulkerEntity::setAttachedFace) + .addTranslator(MetadataTypes.BYTE, ShulkerEntity::setShulkerHeight) + .addTranslator(MetadataTypes.BYTE, ShulkerEntity::setShulkerColor) .build(); SKELETON = EntityDefinition.inherited(SkeletonEntity::new, mobEntityBase) .type(EntityType.SKELETON) .height(1.8f).width(0.6f) .offset(1.62f) - .addTranslator(MetadataType.BOOLEAN, SkeletonEntity::setConvertingToStray) + .addTranslator(MetadataTypes.BOOLEAN, SkeletonEntity::setConvertingToStray) .build(); SNOW_GOLEM = EntityDefinition.inherited(SnowGolemEntity::new, mobEntityBase) .type(EntityType.SNOW_GOLEM) .height(1.9f).width(0.7f) - .addTranslator(MetadataType.BYTE, SnowGolemEntity::setSnowGolemFlags) + .addTranslator(MetadataTypes.BYTE, SnowGolemEntity::setSnowGolemFlags) .build(); SPIDER = EntityDefinition.inherited(SpiderEntity::new, mobEntityBase) .type(EntityType.SPIDER) .height(0.9f).width(1.4f) .offset(1f) - .addTranslator(MetadataType.BYTE, SpiderEntity::setSpiderFlags) + .addTranslator(MetadataTypes.BYTE, SpiderEntity::setSpiderFlags) .build(); CAVE_SPIDER = EntityDefinition.inherited(SpiderEntity::new, SPIDER) .type(EntityType.CAVE_SPIDER) .height(0.5f).width(0.7f) .build(); - SQUID = EntityDefinition.inherited(SquidEntity::new, mobEntityBase) - .type(EntityType.SQUID) - .heightAndWidth(0.8f) - .build(); STRAY = EntityDefinition.inherited(AbstractSkeletonEntity::new, mobEntityBase) .type(EntityType.STRAY) .height(1.8f).width(0.6f) @@ -603,20 +832,20 @@ public final class EntityDefinitions { VEX = EntityDefinition.inherited(VexEntity::new, mobEntityBase) .type(EntityType.VEX) .height(0.8f).width(0.4f) - .addTranslator(MetadataType.BYTE, VexEntity::setVexFlags) + .addTranslator(MetadataTypes.BYTE, VexEntity::setVexFlags) .build(); WARDEN = EntityDefinition.inherited(WardenEntity::new, mobEntityBase) .type(EntityType.WARDEN) .height(2.9f).width(0.9f) - .addTranslator(MetadataType.INT, WardenEntity::setAngerLevel) + .addTranslator(MetadataTypes.INT, WardenEntity::setAngerLevel) .build(); WITHER = EntityDefinition.inherited(WitherEntity::new, mobEntityBase) .type(EntityType.WITHER) .height(3.5f).width(0.9f) - .addTranslator(MetadataType.INT, WitherEntity::setTarget1) - .addTranslator(MetadataType.INT, WitherEntity::setTarget2) - .addTranslator(MetadataType.INT, WitherEntity::setTarget3) - .addTranslator(MetadataType.INT, WitherEntity::setInvulnerableTicks) + .addTranslator(MetadataTypes.INT, WitherEntity::setTarget1) + .addTranslator(MetadataTypes.INT, WitherEntity::setTarget2) + .addTranslator(MetadataTypes.INT, WitherEntity::setTarget3) + .addTranslator(MetadataTypes.INT, WitherEntity::setInvulnerableTicks) .build(); WITHER_SKELETON = EntityDefinition.inherited(AbstractSkeletonEntity::new, mobEntityBase) .type(EntityType.WITHER_SKELETON) @@ -625,23 +854,23 @@ public final class EntityDefinitions { ZOGLIN = EntityDefinition.inherited(ZoglinEntity::new, mobEntityBase) .type(EntityType.ZOGLIN) .height(1.4f).width(1.3965f) - .addTranslator(MetadataType.BOOLEAN, ZoglinEntity::setBaby) + .addTranslator(MetadataTypes.BOOLEAN, ZoglinEntity::setBaby) .build(); ZOMBIE = EntityDefinition.inherited(ZombieEntity::new, mobEntityBase) .type(EntityType.ZOMBIE) .height(1.8f).width(0.6f) .offset(1.62f) - .addTranslator(MetadataType.BOOLEAN, ZombieEntity::setZombieBaby) + .addTranslator(MetadataTypes.BOOLEAN, ZombieEntity::setZombieBaby) .addTranslator(null) // "set special type", doesn't do anything - .addTranslator(MetadataType.BOOLEAN, ZombieEntity::setConvertingToDrowned) + .addTranslator(MetadataTypes.BOOLEAN, ZombieEntity::setConvertingToDrowned) .build(); ZOMBIE_VILLAGER = EntityDefinition.inherited(ZombieVillagerEntity::new, ZOMBIE) .type(EntityType.ZOMBIE_VILLAGER) .height(1.8f).width(0.6f) .offset(1.62f) .identifier("minecraft:zombie_villager_v2") - .addTranslator(MetadataType.BOOLEAN, ZombieVillagerEntity::setTransforming) - .addTranslator(MetadataType.VILLAGER_DATA, ZombieVillagerEntity::setZombieVillagerData) + .addTranslator(MetadataTypes.BOOLEAN, ZombieVillagerEntity::setTransforming) + .addTranslator(MetadataTypes.VILLAGER_DATA, ZombieVillagerEntity::setZombieVillagerData) .build(); ZOMBIFIED_PIGLIN = EntityDefinition.inherited(ZombifiedPiglinEntity::new, ZOMBIE) //TODO test how zombie entity metadata is handled? .type(EntityType.ZOMBIFIED_PIGLIN) @@ -662,7 +891,7 @@ public final class EntityDefinitions { .type(EntityType.GUARDIAN) .heightAndWidth(0.85f) .addTranslator(null) // Moving //TODO - .addTranslator(MetadataType.INT, GuardianEntity::setGuardianTarget) + .addTranslator(MetadataTypes.INT, GuardianEntity::setGuardianTarget) .build(); ELDER_GUARDIAN = EntityDefinition.inherited(ElderGuardianEntity::new, GUARDIAN) .type(EntityType.ELDER_GUARDIAN) @@ -672,7 +901,7 @@ public final class EntityDefinitions { SLIME = EntityDefinition.inherited(SlimeEntity::new, mobEntityBase) .type(EntityType.SLIME) .heightAndWidth(0.51f) - .addTranslator(MetadataType.INT, SlimeEntity::setScale) + .addTranslator(MetadataTypes.INT, SlimeEntity::setSlimeScale) .build(); MAGMA_CUBE = EntityDefinition.inherited(MagmaCubeEntity::new, SLIME) .type(EntityType.MAGMA_CUBE) @@ -688,11 +917,12 @@ public final class EntityDefinitions { PUFFERFISH = EntityDefinition.inherited(PufferFishEntity::new, abstractFishEntityBase) .type(EntityType.PUFFERFISH) .heightAndWidth(0.7f) - .addTranslator(MetadataType.INT, PufferFishEntity::setPufferfishSize) + .addTranslator(MetadataTypes.INT, PufferFishEntity::setPufferfishSize) .build(); SALMON = EntityDefinition.inherited(abstractFishEntityBase.factory(), abstractFishEntityBase) .type(EntityType.SALMON) .height(0.5f).width(0.7f) + .addTranslator(null) // Scale/variant - TODO .build(); TADPOLE = EntityDefinition.inherited(TadpoleEntity::new, abstractFishEntityBase) .type(EntityType.TADPOLE) @@ -702,34 +932,29 @@ public final class EntityDefinitions { .type(EntityType.TROPICAL_FISH) .heightAndWidth(0.6f) .identifier("minecraft:tropicalfish") - .addTranslator(MetadataType.INT, TropicalFishEntity::setFishVariant) + .addTranslator(MetadataTypes.INT, TropicalFishEntity::setFishVariant) .build(); EntityDefinition abstractPiglinEntityBase = EntityDefinition.inherited(BasePiglinEntity::new, mobEntityBase) - .addTranslator(MetadataType.BOOLEAN, BasePiglinEntity::setImmuneToZombification) + .addTranslator(MetadataTypes.BOOLEAN, BasePiglinEntity::setImmuneToZombification) .build(); PIGLIN = EntityDefinition.inherited(PiglinEntity::new, abstractPiglinEntityBase) .type(EntityType.PIGLIN) .height(1.95f).width(0.6f) - .addTranslator(MetadataType.BOOLEAN, PiglinEntity::setBaby) - .addTranslator(MetadataType.BOOLEAN, PiglinEntity::setChargingCrossbow) - .addTranslator(MetadataType.BOOLEAN, PiglinEntity::setDancing) + .addTranslator(MetadataTypes.BOOLEAN, PiglinEntity::setBaby) + .addTranslator(MetadataTypes.BOOLEAN, PiglinEntity::setChargingCrossbow) + .addTranslator(MetadataTypes.BOOLEAN, PiglinEntity::setDancing) .build(); PIGLIN_BRUTE = EntityDefinition.inherited(abstractPiglinEntityBase.factory(), abstractPiglinEntityBase) .type(EntityType.PIGLIN_BRUTE) .height(1.95f).width(0.6f) .build(); - GLOW_SQUID = EntityDefinition.inherited(GlowSquidEntity::new, SQUID) - .type(EntityType.GLOW_SQUID) - .addTranslator(null) // Set dark ticks remaining, possible TODO - .build(); - EntityDefinition raidParticipantEntityBase = EntityDefinition.inherited(RaidParticipantEntity::new, mobEntityBase) .addTranslator(null) // Celebrating //TODO .build(); EntityDefinition spellcasterEntityBase = EntityDefinition.inherited(SpellcasterIllagerEntity::new, raidParticipantEntityBase) - .addTranslator(MetadataType.BYTE, SpellcasterIllagerEntity::setSpellType) + .addTranslator(MetadataTypes.BYTE, SpellcasterIllagerEntity::setSpellType) .build(); EVOKER = EntityDefinition.inherited(spellcasterEntityBase.factory(), spellcasterEntityBase) .type(EntityType.EVOKER) @@ -745,9 +970,9 @@ public final class EntityDefinitions { .type(EntityType.PILLAGER) .height(1.8f).width(0.6f) .offset(1.62f) - .addTranslator(null) // Charging; doesn't have an equivalent on Bedrock //TODO check + .addTranslator(MetadataTypes.BOOLEAN, PillagerEntity::setChargingCrossbow) .build(); - RAVAGER = EntityDefinition.inherited(raidParticipantEntityBase.factory(), raidParticipantEntityBase) + RAVAGER = EntityDefinition.inherited(RavagerEntity::new, raidParticipantEntityBase) .type(EntityType.RAVAGER) .height(1.9f).width(1.2f) .build(); @@ -765,118 +990,136 @@ public final class EntityDefinitions { } EntityDefinition ageableEntityBase = EntityDefinition.inherited(AgeableEntity::new, mobEntityBase) - .addTranslator(MetadataType.BOOLEAN, AgeableEntity::setBaby) + .addTranslator(MetadataTypes.BOOLEAN, AgeableEntity::setBaby) .build(); // Extends ageable { + ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase) + .type(EntityType.ARMADILLO) + .height(0.65f).width(0.7f) + .property(ArmadilloEntity.STATE_PROPERTY) + .addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState) + .build(); AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase) .type(EntityType.AXOLOTL) .height(0.42f).width(0.7f) - .addTranslator(MetadataType.INT, AxolotlEntity::setVariant) - .addTranslator(MetadataType.BOOLEAN, AxolotlEntity::setPlayingDead) + .addTranslator(MetadataTypes.INT, AxolotlEntity::setVariant) + .addTranslator(MetadataTypes.BOOLEAN, AxolotlEntity::setPlayingDead) .addTranslator(null) // From bucket .build(); BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase) .type(EntityType.BEE) .heightAndWidth(0.6f) - .addTranslator(MetadataType.BYTE, BeeEntity::setBeeFlags) - .addTranslator(MetadataType.INT, BeeEntity::setAngerTime) + .property(BeeEntity.NECTAR_PROPERTY) + .addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags) + .addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime) .build(); CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase) .type(EntityType.CHICKEN) .height(0.7f).width(0.4f) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) + .addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant) .build(); COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) .type(EntityType.COW) .height(1.4f).width(0.9f) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) + .addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant) .build(); FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase) .type(EntityType.FOX) .height(0.7f).width(0.6f) - .addTranslator(MetadataType.INT, FoxEntity::setFoxVariant) - .addTranslator(MetadataType.BYTE, FoxEntity::setFoxFlags) + .addTranslator(MetadataTypes.INT, FoxEntity::setFoxVariant) + .addTranslator(MetadataTypes.BYTE, FoxEntity::setFoxFlags) .addTranslator(null) // Trusted player 1 .addTranslator(null) // Trusted player 2 .build(); FROG = EntityDefinition.inherited(FrogEntity::new, ageableEntityBase) .type(EntityType.FROG) .heightAndWidth(0.5f) - .addTranslator(MetadataType.FROG_VARIANT, FrogEntity::setFrogVariant) - .addTranslator(MetadataType.OPTIONAL_VARINT, FrogEntity::setTongueTarget) + .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setVariant) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FrogEntity::setTongueTarget) + .build(); + HAPPY_GHAST = EntityDefinition.inherited(HappyGhastEntity::new, ageableEntityBase) + .type(EntityType.HAPPY_GHAST) + .heightAndWidth(4f) + .property(HappyGhastEntity.CAN_MOVE_PROPERTY) + .addTranslator(null) // Is leash holder + .addTranslator(MetadataTypes.BOOLEAN, HappyGhastEntity::setStaysStill) .build(); HOGLIN = EntityDefinition.inherited(HoglinEntity::new, ageableEntityBase) .type(EntityType.HOGLIN) .height(1.4f).width(1.3965f) - .addTranslator(MetadataType.BOOLEAN, HoglinEntity::setImmuneToZombification) + .addTranslator(MetadataTypes.BOOLEAN, HoglinEntity::setImmuneToZombification) .build(); GOAT = EntityDefinition.inherited(GoatEntity::new, ageableEntityBase) .type(EntityType.GOAT) .height(1.3f).width(0.9f) - .addTranslator(MetadataType.BOOLEAN, GoatEntity::setScreamer) - .addTranslator(MetadataType.BOOLEAN, GoatEntity::setHasLeftHorn) - .addTranslator(MetadataType.BOOLEAN, GoatEntity::setHasRightHorn) + .addTranslator(MetadataTypes.BOOLEAN, GoatEntity::setScreamer) + .addTranslator(MetadataTypes.BOOLEAN, GoatEntity::setHasLeftHorn) + .addTranslator(MetadataTypes.BOOLEAN, GoatEntity::setHasRightHorn) .build(); MOOSHROOM = EntityDefinition.inherited(MooshroomEntity::new, ageableEntityBase) .type(EntityType.MOOSHROOM) .height(1.4f).width(0.9f) - .addTranslator(MetadataType.STRING, MooshroomEntity::setVariant) + .addTranslator(MetadataTypes.INT, MooshroomEntity::setMooshroomVariant) .build(); OCELOT = EntityDefinition.inherited(OcelotEntity::new, ageableEntityBase) .type(EntityType.OCELOT) .height(0.7f).width(0.6f) - .addTranslator(MetadataType.BOOLEAN, (ocelotEntity, entityMetadata) -> ocelotEntity.setFlag(EntityFlag.TRUSTING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .addTranslator(MetadataTypes.BOOLEAN, (ocelotEntity, entityMetadata) -> ocelotEntity.setFlag(EntityFlag.TRUSTING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); PANDA = EntityDefinition.inherited(PandaEntity::new, ageableEntityBase) .type(EntityType.PANDA) .height(1.25f).width(1.125f) .addTranslator(null) // Unhappy counter .addTranslator(null) // Sneeze counter - .addTranslator(MetadataType.INT, PandaEntity::setEatingCounter) - .addTranslator(MetadataType.BYTE, PandaEntity::setMainGene) - .addTranslator(MetadataType.BYTE, PandaEntity::setHiddenGene) - .addTranslator(MetadataType.BYTE, PandaEntity::setPandaFlags) + .addTranslator(MetadataTypes.INT, PandaEntity::setEatingCounter) + .addTranslator(MetadataTypes.BYTE, PandaEntity::setMainGene) + .addTranslator(MetadataTypes.BYTE, PandaEntity::setHiddenGene) + .addTranslator(MetadataTypes.BYTE, PandaEntity::setPandaFlags) .build(); PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase) .type(EntityType.PIG) .heightAndWidth(0.9f) - .addTranslator(MetadataType.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) - .addTranslator(null) // Boost time + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) + .addTranslator(MetadataTypes.INT, PigEntity::setBoost) + .addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant) .build(); POLAR_BEAR = EntityDefinition.inherited(PolarBearEntity::new, ageableEntityBase) .type(EntityType.POLAR_BEAR) .height(1.4f).width(1.3f) - .addTranslator(MetadataType.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.STANDING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .addTranslator(MetadataTypes.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.STANDING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); RABBIT = EntityDefinition.inherited(RabbitEntity::new, ageableEntityBase) .type(EntityType.RABBIT) .height(0.5f).width(0.4f) - .addTranslator(MetadataType.INT, RabbitEntity::setRabbitVariant) + .addTranslator(MetadataTypes.INT, RabbitEntity::setRabbitVariant) .build(); SHEEP = EntityDefinition.inherited(SheepEntity::new, ageableEntityBase) .type(EntityType.SHEEP) .height(1.3f).width(0.9f) - .addTranslator(MetadataType.BYTE, SheepEntity::setSheepFlags) + .addTranslator(MetadataTypes.BYTE, SheepEntity::setSheepFlags) .build(); SNIFFER = EntityDefinition.inherited(SnifferEntity::new, ageableEntityBase) .type(EntityType.SNIFFER) .height(1.75f).width(1.9f) - .addTranslator(MetadataType.SNIFFER_STATE, SnifferEntity::setSnifferState) + .addTranslator(MetadataTypes.SNIFFER_STATE, SnifferEntity::setSnifferState) .addTranslator(null) // Integer, drop seed at tick .build(); STRIDER = EntityDefinition.inherited(StriderEntity::new, ageableEntityBase) .type(EntityType.STRIDER) .height(1.7f).width(0.9f) - .addTranslator(null) // Boost time - .addTranslator(MetadataType.BOOLEAN, StriderEntity::setCold) - .addTranslator(MetadataType.BOOLEAN, StriderEntity::setSaddled) + .addTranslator(MetadataTypes.INT, StriderEntity::setBoost) + .addTranslator(MetadataTypes.BOOLEAN, StriderEntity::setCold) .build(); TURTLE = EntityDefinition.inherited(TurtleEntity::new, ageableEntityBase) .type(EntityType.TURTLE) .height(0.4f).width(1.2f) .addTranslator(null) // Home position - .addTranslator(MetadataType.BOOLEAN, TurtleEntity::setPregnant) - .addTranslator(MetadataType.BOOLEAN, TurtleEntity::setLayingEgg) + .addTranslator(MetadataTypes.BOOLEAN, TurtleEntity::setPregnant) + .addTranslator(MetadataTypes.BOOLEAN, TurtleEntity::setLayingEgg) .addTranslator(null) // Travel position .addTranslator(null) // Going home .addTranslator(null) // Travelling @@ -890,7 +1133,7 @@ public final class EntityDefinitions { .height(1.8f).width(0.6f) .offset(1.62f) .identifier("minecraft:villager_v2") - .addTranslator(MetadataType.VILLAGER_DATA, VillagerEntity::setVillagerData) + .addTranslator(MetadataTypes.VILLAGER_DATA, VillagerEntity::setVillagerData) .build(); WANDERING_TRADER = EntityDefinition.inherited(abstractVillagerEntityBase.factory(), abstractVillagerEntityBase) .type(EntityType.WANDERING_TRADER) @@ -899,21 +1142,41 @@ public final class EntityDefinitions { .build(); } + // Water creatures (AgeableWaterCreature) + { + DOLPHIN = EntityDefinition.inherited(DolphinEntity::new, ageableEntityBase) + .type(EntityType.DOLPHIN) + .height(0.6f).width(0.9f) + //TODO check + .addTranslator(null) // treasure position + .addTranslator(null) // "got fish" + .addTranslator(null) // "moistness level" + .build(); + SQUID = EntityDefinition.inherited(SquidEntity::new, ageableEntityBase) + .type(EntityType.SQUID) + .heightAndWidth(0.8f) + .build(); + GLOW_SQUID = EntityDefinition.inherited(GlowSquidEntity::new, SQUID) + .type(EntityType.GLOW_SQUID) + .addTranslator(null) // Set dark ticks remaining, possible TODO + .build(); + } + // Horses { EntityDefinition abstractHorseEntityBase = EntityDefinition.inherited(AbstractHorseEntity::new, ageableEntityBase) - .addTranslator(MetadataType.BYTE, AbstractHorseEntity::setHorseFlags) + .addTranslator(MetadataTypes.BYTE, AbstractHorseEntity::setHorseFlags) .build(); CAMEL = EntityDefinition.inherited(CamelEntity::new, abstractHorseEntityBase) .type(EntityType.CAMEL) .height(2.375f).width(1.7f) - .addTranslator(MetadataType.BOOLEAN, CamelEntity::setDashing) - .addTranslator(null) // Last pose change tick + .addTranslator(MetadataTypes.BOOLEAN, CamelEntity::setDashing) + .addTranslator(MetadataTypes.LONG, CamelEntity::setLastPoseTick) .build(); HORSE = EntityDefinition.inherited(HorseEntity::new, abstractHorseEntityBase) .type(EntityType.HORSE) .height(1.6f).width(1.3965f) - .addTranslator(MetadataType.INT, HorseEntity::setHorseVariant) + .addTranslator(MetadataTypes.INT, HorseEntity::setHorseVariant) .build(); SKELETON_HORSE = EntityDefinition.inherited(SkeletonHorseEntity::new, abstractHorseEntityBase) .type(EntityType.SKELETON_HORSE) @@ -924,7 +1187,7 @@ public final class EntityDefinitions { .height(1.6f).width(1.3965f) .build(); EntityDefinition chestedHorseEntityBase = EntityDefinition.inherited(ChestedHorseEntity::new, abstractHorseEntityBase) - .addTranslator(MetadataType.BOOLEAN, (horseEntity, entityMetadata) -> horseEntity.setFlag(EntityFlag.CHESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .addTranslator(MetadataTypes.BOOLEAN, (horseEntity, entityMetadata) -> horseEntity.setFlag(EntityFlag.CHESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); DONKEY = EntityDefinition.inherited(chestedHorseEntityBase.factory(), chestedHorseEntityBase) .type(EntityType.DONKEY) @@ -937,9 +1200,8 @@ public final class EntityDefinitions { LLAMA = EntityDefinition.inherited(LlamaEntity::new, chestedHorseEntityBase) .type(EntityType.LLAMA) .height(1.87f).width(0.9f) - .addTranslator(MetadataType.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.STRENGTH, entityMetadata.getValue())) - .addTranslator(MetadataType.INT, LlamaEntity::setCarpetedColor) - .addTranslator(MetadataType.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.VARIANT, entityMetadata.getValue())) + .addTranslator(MetadataTypes.INT, LlamaEntity::setStrength) + .addTranslator(MetadataTypes.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.VARIANT, entityMetadata.getValue())) .build(); TRADER_LLAMA = EntityDefinition.inherited(TraderLlamaEntity::new, LLAMA) .type(EntityType.TRADER_LLAMA) @@ -947,30 +1209,33 @@ public final class EntityDefinitions { .build(); } - EntityDefinition tameableEntityBase = EntityDefinition.inherited(TameableEntity::new, ageableEntityBase) - .addTranslator(MetadataType.BYTE, TameableEntity::setTameableFlags) - .addTranslator(MetadataType.OPTIONAL_UUID, TameableEntity::setOwner) + EntityDefinition tameableEntityBase = EntityDefinition.inherited(null, ageableEntityBase) // No factory, is abstract + .addTranslator(MetadataTypes.BYTE, TameableEntity::setTameableFlags) + .addTranslator(MetadataTypes.OPTIONAL_LIVING_ENTITY_REFERENCE, TameableEntity::setOwner) .build(); CAT = EntityDefinition.inherited(CatEntity::new, tameableEntityBase) .type(EntityType.CAT) .height(0.35f).width(0.3f) - .addTranslator(MetadataType.CAT_VARIANT, CatEntity::setCatVariant) - .addTranslator(MetadataType.BOOLEAN, CatEntity::setResting) + .addTranslator(MetadataTypes.CAT_VARIANT, CatEntity::setVariant) + .addTranslator(MetadataTypes.BOOLEAN, CatEntity::setResting) .addTranslator(null) // "resting state one" //TODO - .addTranslator(MetadataType.INT, CatEntity::setCollarColor) + .addTranslator(MetadataTypes.INT, CatEntity::setCollarColor) .build(); PARROT = EntityDefinition.inherited(ParrotEntity::new, tameableEntityBase) .type(EntityType.PARROT) .height(0.9f).width(0.5f) - .addTranslator(MetadataType.INT, (parrotEntity, entityMetadata) -> parrotEntity.getDirtyMetadata().put(EntityDataTypes.VARIANT, entityMetadata.getValue())) // Parrot color + .addTranslator(MetadataTypes.INT, (parrotEntity, entityMetadata) -> parrotEntity.getDirtyMetadata().put(EntityDataTypes.VARIANT, entityMetadata.getValue())) // Parrot color .build(); WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase) .type(EntityType.WOLF) .height(0.85f).width(0.6f) + .property(WolfEntity.SOUND_VARIANT) // "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head - .addTranslator(MetadataType.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) - .addTranslator(MetadataType.INT, WolfEntity::setCollarColor) - .addTranslator(MetadataType.INT, WolfEntity::setWolfAngerTime) + .addTranslator(MetadataTypes.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor) + .addTranslator(MetadataTypes.INT, WolfEntity::setWolfAngerTime) + .addTranslator(MetadataTypes.WOLF_VARIANT, WolfEntity::setVariant) + .addTranslator(null) // sound variant; these aren't clientsided anyways... right?? .build(); // As of 1.18 these don't track entity data at all @@ -981,8 +1246,112 @@ public final class EntityDefinitions { Registries.ENTITY_IDENTIFIERS.get().put("minecraft:marker", null); // We don't need an entity definition for this as it is never sent over the network } + private static EntityDefinition buildBoat(EntityDefinition base, EntityType entityType, BoatEntity.BoatVariant variant) { + return EntityDefinition.inherited((session, javaId, bedrockId, uuid, definition, position, motion, yaw, pitch, headYaw) -> + new BoatEntity(session, javaId, bedrockId, uuid, definition, position, motion, yaw, variant), base) + .type(entityType) + .identifier("minecraft:boat") + .build(); + } + + private static EntityDefinition buildChestBoat(EntityDefinition base, EntityType entityType, BoatEntity.BoatVariant variant) { + return EntityDefinition.inherited((session, javaId, bedrockId, uuid, definition, position, motion, yaw, pitch, headYaw) -> + new ChestBoatEntity(session, javaId, bedrockId, uuid, definition, position, motion, yaw, variant), base) + .type(entityType) + .identifier("minecraft:chest_boat") + .build(); + } + public static void init() { - // no-op + // entities would be initialized before this event is called + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineEntityPropertiesEvent() { + @Override + public GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, float min, float max, @Nullable Float defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + FloatProperty property = new FloatProperty(propertyId, min, max, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public IntProperty registerIntegerProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, int min, int max, @Nullable Integer defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + IntProperty property = new IntProperty(propertyId, min, max, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public BooleanProperty registerBooleanProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, boolean defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + BooleanProperty property = new BooleanProperty(propertyId, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public > EnumProperty registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull Class enumClass, @Nullable E defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + Objects.requireNonNull(enumClass); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + EnumProperty property = new EnumProperty<>(propertyId, enumClass, defaultValue == null ? enumClass.getEnumConstants()[0] : defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull List values, @Nullable String defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + Objects.requireNonNull(values); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + StringEnumProperty property = new StringEnumProperty(propertyId, values, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public Collection> properties(@NonNull Identifier identifier) { + Objects.requireNonNull(identifier); + var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(identifier.toString()); + if (definition == null) { + throw new IllegalArgumentException("Unknown entity type: " + identifier); + } + return List.copyOf(definition.registeredProperties().getProperties()); + } + }); + + for (var definition : Registries.ENTITY_DEFINITIONS.get().values()) { + if (definition.registeredProperties() != null) { + Registries.BEDROCK_ENTITY_PROPERTIES.get().add(definition.registeredProperties().toNbtMap(definition.identifier())); + } + } + } + + private static void registerProperty(Identifier entityType, PropertyType property) { + var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityType.toString()); + if (definition == null) { + throw new IllegalArgumentException("Unknown entity type: " + entityType); + } + + definition.registeredProperties().add(entityType.toString(), property); } private EntityDefinitions() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserDirtyMetadata.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserDirtyMetadata.java index bc567ab91..1ee84f4b5 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserDirtyMetadata.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserDirtyMetadata.java @@ -32,7 +32,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType; import java.util.Map; /** - * A write-only wrapper for temporarily storing entity metadata that will be sent to Bedrock. + * A wrapper for temporarily storing entity metadata that will be sent to Bedrock. */ public final class GeyserDirtyMetadata { private final Map, Object> metadata = new Object2ObjectLinkedOpenHashMap<>(); @@ -53,6 +53,14 @@ public final class GeyserDirtyMetadata { return !metadata.isEmpty(); } + /** + * Intended for testing purposes only + */ + public T get(EntityDataType entityData) { + //noinspection unchecked + return (T) metadata.get(entityData); + } + @Override public String toString() { return metadata.toString(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java index c9ef7a2dd..e0267ff13 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java @@ -44,7 +44,6 @@ import java.util.concurrent.CompletableFuture; public class GeyserEntityData implements EntityData { private final GeyserSession session; - private final Set movementLockOwners = new HashSet<>(); public GeyserEntityData(GeyserSession session) { @@ -96,4 +95,9 @@ public class GeyserEntityData implements EntityData { public boolean isMovementLocked() { return !movementLockOwners.isEmpty(); } + + @Override + public void switchHands() { + session.requestOffhandSwap(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java index 234c1afe9..80db3354e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java @@ -25,34 +25,39 @@ package org.geysermc.geyser.entity.attribute; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.cloudburstmc.protocol.bedrock.data.AttributeData; import lombok.AllArgsConstructor; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; @Getter @AllArgsConstructor public enum GeyserAttributeType { // Universal Attributes - FOLLOW_RANGE("minecraft:generic.follow_range", "minecraft:follow_range", 0f, 2048f, 32f), - KNOCKBACK_RESISTANCE("minecraft:generic.knockback_resistance", "minecraft:knockback_resistance", 0f, 1f, 0f), - MOVEMENT_SPEED("minecraft:generic.movement_speed", "minecraft:movement", 0f, 1024f, 0.1f), - FLYING_SPEED("minecraft:generic.flying_speed", "minecraft:movement", 0.0f, 1024.0f, 0.4000000059604645f), - ATTACK_DAMAGE("minecraft:generic.attack_damage", "minecraft:attack_damage", 0f, 2048f, 1f), - HORSE_JUMP_STRENGTH("minecraft:horse.jump_strength", "minecraft:horse.jump_strength", 0.0f, 2.0f, 0.7f), - LUCK("minecraft:generic.luck", "minecraft:luck", -1024f, 1024f, 0f), + FOLLOW_RANGE("minecraft:follow_range", "minecraft:follow_range", 0f, 2048f, 32f), + KNOCKBACK_RESISTANCE("minecraft:knockback_resistance", "minecraft:knockback_resistance", 0f, 1f, 0f), + MOVEMENT_SPEED("minecraft:movement_speed", "minecraft:movement", 0f, 1024f, 0.1f), + FLYING_SPEED("minecraft:flying_speed", "minecraft:movement", 0.0f, 1024.0f, 0.4000000059604645f), + ATTACK_DAMAGE("minecraft:attack_damage", "minecraft:attack_damage", 0f, 2048f, 1f), + HORSE_JUMP_STRENGTH("minecraft:jump_strength", "minecraft:horse.jump_strength", 0.0f, 2.0f, 0.7f), + LUCK("minecraft:luck", "minecraft:luck", -1024f, 1024f, 0f), // Java Attributes - ARMOR("minecraft:generic.armor", null, 0f, 30f, 0f), - ARMOR_TOUGHNESS("minecraft:generic.armor_toughness", null, 0F, 20f, 0f), - ATTACK_KNOCKBACK("minecraft:generic.attack_knockback", null, 1.5f, Float.MAX_VALUE, 0f), - ATTACK_SPEED("minecraft:generic.attack_speed", null, 0f, 1024f, 4f), - MAX_HEALTH("minecraft:generic.max_health", null, 0f, 1024f, 20f), + ARMOR("minecraft:armor", null, 0f, 30f, 0f), + ARMOR_TOUGHNESS("minecraft:armor_toughness", null, 0F, 20f, 0f), + ATTACK_KNOCKBACK("minecraft:attack_knockback", null, 1.5f, Float.MAX_VALUE, 0f), + ATTACK_SPEED("minecraft:attack_speed", null, 0f, 1024f, 4f), + MAX_HEALTH("minecraft:max_health", null, 0f, 1024f, 20f), + SCALE("minecraft:scale", null, 0.0625f, 16f, 1f), + BLOCK_INTERACTION_RANGE("minecraft:block_interaction_range", null, 0.0f, 64f, 4.5f), + MINING_EFFICIENCY("minecraft:mining_efficiency", null, 0f, 1024f, 0f), + BLOCK_BREAK_SPEED("minecraft:block_break_speed", null, 0f, 1024f, 1f), + SUBMERGED_MINING_SPEED("minecraft:submerged_mining_speed", null, 0f, 20f, 0.2f), // Bedrock Attributes - ABSORPTION(null, "minecraft:absorption", 0f, Float.MAX_VALUE, 0f), - EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 5f, 0f), + ABSORPTION(null, "minecraft:absorption", 0f, 1024f, 0f), + EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 20f, 0f), EXPERIENCE(null, "minecraft:player.experience", 0f, 1f, 0f), EXPERIENCE_LEVEL(null, "minecraft:player.level", 0f, 24791.00f, 0f), HEALTH(null, "minecraft:health", 0f, 1024f, 20f), @@ -66,6 +71,10 @@ public enum GeyserAttributeType { private final float maximum; private final float defaultValue; + public AttributeData getAttribute() { + return getAttribute(defaultValue); + } + public AttributeData getAttribute(float value) { return getAttribute(value, maximum); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java new file mode 100644 index 000000000..fa9439998 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; +import org.geysermc.geyser.entity.properties.type.PropertyType; +import org.geysermc.geyser.registry.Registries; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +@EqualsAndHashCode +@ToString +public class GeyserEntityProperties { + + private final static Pattern ENTITY_PROPERTY_PATTERN = Pattern.compile("^[a-z0-9_.:-]*:[a-z0-9_.:-]*$"); + + private final ObjectArrayList> properties; + private final Object2IntMap propertyIndices; + + private GeyserEntityProperties() { + this.properties = new ObjectArrayList<>(); + this.propertyIndices = new Object2IntOpenHashMap<>(); + } + + public NbtMap toNbtMap(String entityType) { + NbtMapBuilder mapBuilder = NbtMap.builder(); + List nbtProperties = new ArrayList<>(); + + for (PropertyType property : properties) { + nbtProperties.add(property.nbtMap()); + } + mapBuilder.putList("properties", NbtType.COMPOUND, nbtProperties); + + return mapBuilder.putString("type", entityType).build(); + } + + public void add(String entityType, @NonNull PropertyType property) { + if (!Registries.BEDROCK_ENTITY_PROPERTIES.get().isEmpty()) { + throw new IllegalStateException("Cannot add properties outside the GeyserDefineEntityProperties event!"); + } + + if (this.properties.size() > 32) { + throw new IllegalArgumentException("Cannot register more than 32 properties for entity type " + entityType); + } + + Objects.requireNonNull(property, "property cannot be null!"); + String name = property.identifier().toString(); + if (propertyIndices.containsKey(name)) { + throw new IllegalArgumentException( + "Property with name " + name + " already exists on builder!"); + } else if (!ENTITY_PROPERTY_PATTERN.matcher(name).matches()) { + throw new IllegalArgumentException( + "Cannot register property with name " + name + " because property name is invalid! Must match: " + ENTITY_PROPERTY_PATTERN.pattern() + ); + } + this.properties.add(property); + propertyIndices.put(name, properties.size() - 1); + } + + public @NonNull List> getProperties() { + return properties; + } + + public int getPropertyIndex(String name) { + return propertyIndices.getOrDefault(name, -1); + } + + public static class Builder { + private GeyserEntityProperties properties; + private final String identifier; + + public Builder(String identifier) { + this.identifier = identifier; + } + + public Builder add(@NonNull PropertyType property) { + Objects.requireNonNull(property, "property cannot be null!"); + if (properties == null) { + properties = new GeyserEntityProperties(); + } + properties.add(identifier, property); + return this; + } + + public @Nullable GeyserEntityProperties build() { + return properties; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java new file mode 100644 index 000000000..a5f189cc7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties; + +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; +import org.cloudburstmc.protocol.bedrock.data.entity.FloatEntityProperty; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.entity.properties.type.PropertyType; + +import java.util.List; + +public class GeyserEntityPropertyManager { + + private final GeyserEntityProperties properties; + private final Object2ObjectMap intEntityProperties = new Object2ObjectArrayMap<>(); + private final Object2ObjectMap floatEntityProperties = new Object2ObjectArrayMap<>(); + + public GeyserEntityPropertyManager(GeyserEntityProperties properties) { + this.properties = properties; + for (PropertyType property : properties.getProperties()) { + String name = property.identifier().toString(); + int index = properties.getPropertyIndex(name); + addProperty(name, property.defaultValue(index)); + } + } + + public void addProperty(PropertyType propertyType, T value) { + int index = properties.getPropertyIndex(propertyType.identifier().toString()); + this.addProperty(propertyType.identifier().toString(), propertyType.createValue(index, value)); + } + + private void addProperty(String propertyName, EntityProperty entityProperty) { + if (entityProperty instanceof FloatEntityProperty floatEntityProperty) { + floatEntityProperties.put(propertyName, floatEntityProperty); + } else if (entityProperty instanceof IntEntityProperty intEntityProperty) { + intEntityProperties.put(propertyName, intEntityProperty); + } + } + + public boolean hasFloatProperties() { + return !this.floatEntityProperties.isEmpty(); + } + + public boolean hasIntProperties() { + return !this.intEntityProperties.isEmpty(); + } + + public boolean hasProperties() { + return hasFloatProperties() || hasIntProperties(); + } + + public void applyIntProperties(List properties) { + properties.addAll(intEntityProperties.values()); + intEntityProperties.clear(); + } + + public void applyFloatProperties(List properties) { + properties.addAll(floatEntityProperties.values()); + floatEntityProperties.clear(); + } + + public NbtMap toNbtMap(String entityType) { + return this.properties.toNbtMap(entityType); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java new file mode 100644 index 000000000..0c921b54e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.List; +import java.util.regex.Pattern; + +public interface AbstractEnumProperty extends PropertyType { + + Pattern VALUE_VALIDATION_REGEX = Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,31}$"); + + @Override + default NbtMap nbtMap() { + return NbtMap.builder() + .putString("name", identifier().toString()) + .putList("enum", NbtType.STRING, allBedrockValues()) + .putInt("type", 3) + .build(); + } + + default void validateAllValues(Identifier name, List values) { + if (values.size() > 16) { + throw new IllegalArgumentException("Cannot register enum property with name " + name + " because it has more than 16 values!"); + } + + for (String value : values) { + if (!VALUE_VALIDATION_REGEX.matcher(value).matches()) { + throw new IllegalArgumentException( + "Cannot register enum property with name " + name + " and value " + value + + " because enum values can only contain alphanumeric characters and underscores." + ); + } + } + } + + List allBedrockValues(); + + @Override + default IntEntityProperty defaultValue(int index) { + return new IntEntityProperty(index, defaultIndex()); + } + + @Override + default IntEntityProperty createValue(int index, @Nullable T value) { + if (value == null) { + return defaultValue(index); + } + + int valueIndex = indexOf(value); + if (valueIndex == -1) { + throw new IllegalArgumentException("Enum value " + value + " is not a valid enum value!"); + } + return new IntEntityProperty(index, valueIndex); + } + + int indexOf(T value); + + int defaultIndex(); +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java new file mode 100644 index 000000000..9bbb1cc2d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty; +import org.geysermc.geyser.api.util.Identifier; + +public record BooleanProperty( + Identifier identifier, + Boolean defaultValue +) implements PropertyType, GeyserBooleanEntityProperty { + + @Override + public NbtMap nbtMap() { + return NbtMap.builder() + .putString("name", identifier.toString()) + .putInt("type", 2) + .build(); + } + + @Override + public IntEntityProperty defaultValue(int index) { + return createValue(index, defaultValue != null && defaultValue); + } + + @Override + public IntEntityProperty createValue(int index, Boolean value) { + if (value == null) { + return defaultValue(index); + } + return new IntEntityProperty(index, value ? 1 : 0); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java new file mode 100644 index 000000000..42fbb6a81 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public record EnumProperty>( + Identifier identifier, + Class enumClass, + E defaultValue +) implements AbstractEnumProperty, GeyserEnumEntityProperty { + + public EnumProperty { + validateAllValues(identifier, Arrays.stream(enumClass.getEnumConstants()).map(value -> value.name().toLowerCase(Locale.ROOT)).toList()); + } + + public List values() { + return List.of(enumClass.getEnumConstants()); + } + + public List allBedrockValues() { + return values().stream().map( + value -> value.name().toLowerCase(Locale.ROOT) + ).toList(); + } + + @Override + public int indexOf(E value) { + return value.ordinal(); + } + + @Override + public int defaultIndex() { + return defaultValue.ordinal(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java new file mode 100644 index 000000000..16997dc8a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.FloatEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; +import org.geysermc.geyser.api.util.Identifier; + +public record FloatProperty( + Identifier identifier, + float max, + float min, + Float defaultValue +) implements PropertyType, GeyserFloatEntityProperty { + + public FloatProperty { + if (min > max) { + throw new IllegalArgumentException("Cannot create float entity property (%s) with a minimum value (%s) greater than maximum (%s)!" + .formatted(identifier, min, max)); + } + if (defaultValue < min || defaultValue > max) { + throw new IllegalArgumentException("Cannot create float entity property (%s) with a default value (%s) outside of the range (%s - %s)!" + .formatted(identifier, defaultValue, min, max)); + } + } + + @Override + public NbtMap nbtMap() { + return NbtMap.builder() + .putString("name", identifier.toString()) + .putFloat("max", max) + .putFloat("min", min) + .putInt("type", 1) + .build(); + } + + @Override + public FloatEntityProperty defaultValue(int index) { + return createValue(index, defaultValue == null ? min : defaultValue); + } + + @Override + public FloatEntityProperty createValue(int index, Float value) { + if (value == null) { + return defaultValue(index); + } + return new FloatEntityProperty(index, value); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java new file mode 100644 index 000000000..966277696 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.entity.property.type.GeyserIntEntityProperty; +import org.geysermc.geyser.api.util.Identifier; + +public record IntProperty( + Identifier identifier, + int max, + int min, + Integer defaultValue +) implements PropertyType, GeyserIntEntityProperty { + + public IntProperty { + if (min > max) { + throw new IllegalArgumentException("Cannot create int entity property (%s) with a minimum value (%s) greater than maximum (%s)!" + .formatted(identifier, min, max)); + } + if (defaultValue < min || defaultValue > max) { + throw new IllegalArgumentException("Cannot create int entity property (%s) with a default value (%s) outside of the range (%s - %s)!" + .formatted(identifier, defaultValue, min, max)); + } + if (min < -1000000 || max > 1000000) { + // https://learn.microsoft.com/en-us/minecraft/creator/documents/introductiontoentityproperties?view=minecraft-bedrock-stable#a-note-on-large-integer-entity-property-values + GeyserImpl.getInstance().getLogger().warning("Using int entity properties with min / max values larger than +- 1 million is not recommended!"); + } + } + + @Override + public NbtMap nbtMap() { + return NbtMap.builder() + .putString("name", identifier.toString()) + .putInt("max", max) + .putInt("min", min) + .putInt("type", 0) + .build(); + } + + @Override + public IntEntityProperty defaultValue(int index) { + return createValue(index, defaultValue == null ? min : defaultValue); + } + + @Override + public IntEntityProperty createValue(int index, Integer value) { + if (value == null) { + return defaultValue(index); + } + return new IntEntityProperty(index, value); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java new file mode 100644 index 000000000..cd1471273 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager; + +public interface PropertyType extends GeyserEntityProperty { + NbtMap nbtMap(); + + NetworkRepresentation defaultValue(int index); + + NetworkRepresentation createValue(int index, @Nullable Type value); + + default void apply(GeyserEntityPropertyManager manager, Type value) { + manager.addProperty(this, value); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java new file mode 100644 index 000000000..278f60c7f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.properties.type; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.List; + +public record StringEnumProperty( + Identifier identifier, + List values, + int defaultIndex +) implements AbstractEnumProperty, GeyserStringEnumProperty { + + public StringEnumProperty { + if (defaultIndex < 0) { + throw new IllegalArgumentException("Unable to find default value for enum property with name " + identifier); + } + validateAllValues(identifier, values); + } + + public StringEnumProperty(Identifier name, List values, @Nullable String defaultValue) { + this(name, values, defaultValue == null ? 0 : values.indexOf(defaultValue)); + } + + @Override + public List allBedrockValues() { + return values; + } + + @Override + public int indexOf(String value) { + return values.indexOf(value); + } + + @Override + public @NonNull String defaultValue() { + return values.get(defaultIndex); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/AbstractArrowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/AbstractArrowEntity.java index 6828d1020..1cc746d7a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/AbstractArrowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/AbstractArrowEntity.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/AbstractWindChargeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/AbstractWindChargeEntity.java new file mode 100644 index 000000000..5678c3af4 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/AbstractWindChargeEntity.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type; + +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.UUID; + +/** + * Note that, as of 1.21, a wind charge entity does not actually implement the thrown item. We're just reusing + * the "hide until far away" aspect. + */ +public class AbstractWindChargeEntity extends ThrowableItemEntity { + public AbstractWindChargeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public void tick() { + super.tick(); + } + + @Override + protected float getDrag() { + // Always, even in water. As of 1.21. + return 1f; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java index 84dfae468..e84b2aae2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java @@ -25,9 +25,6 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.level.particle.Particle; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -35,6 +32,11 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import java.util.UUID; @@ -51,16 +53,17 @@ public class AreaEffectCloudEntity extends Entity { dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_DURATION, Integer.MAX_VALUE); // This disabled client side shrink of the cloud - dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_RADIUS, 0.0f); - dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_CHANGE_RATE, Float.MIN_VALUE); + dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_RADIUS, 3.0f); dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_CHANGE_ON_PICKUP, Float.MIN_VALUE); + //noinspection deprecation - still needed for these to show up + dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_CHANGE_RATE, Float.MIN_VALUE); setFlag(EntityFlag.FIRE_IMMUNE, true); } public void setRadius(FloatEntityMetadata entityMetadata) { // Anything less than 0.5 will cause the cloud to despawn - float value = Math.max(entityMetadata.getPrimitiveValue(), 0.5f); + float value = MathUtils.clamp(entityMetadata.getPrimitiveValue(), 0.5f, 32.0f); dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_RADIUS, value); dirtyMetadata.put(EntityDataTypes.WIDTH, 2.0f * value); } @@ -69,5 +72,9 @@ public class AreaEffectCloudEntity extends Entity { Particle particle = entityMetadata.getValue(); Registries.PARTICLES.map(particle.getType(), p -> p.levelEventType() instanceof ParticleType particleType ? particleType : null).ifPresent(type -> dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_PARTICLE, type)); + + if (particle.getData() instanceof ColorParticleData effectParticleData) { + dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, effectParticleData.getColor()); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TippedArrowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ArrowEntity.java similarity index 68% rename from core/src/main/java/org/geysermc/geyser/entity/type/TippedArrowEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/ArrowEntity.java index 856c4cc66..ba1241434 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TippedArrowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ArrowEntity.java @@ -25,21 +25,18 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.inventory.item.TippedArrowPotion; +import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; -/** - * Internally this is known as TippedArrowEntity but is used with tipped arrows and normal arrows - */ -public class TippedArrowEntity extends AbstractArrowEntity { +public class ArrowEntity extends AbstractArrowEntity { - public TippedArrowEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + public ArrowEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -49,12 +46,7 @@ public class TippedArrowEntity extends AbstractArrowEntity { if (potionColor == -1) { dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) 0); } else { - TippedArrowPotion potion = TippedArrowPotion.getByJavaColor(potionColor); - if (potion != null && potion.getJavaColor() != -1) { - dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) potion.getBedrockId()); - } else { - dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) 0); - } + dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, Potion.toTippedArrowId(potionColor)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 5527e773a..3b77b1eec 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -25,24 +25,24 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; -import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundPaddleBoatPacket; import java.util.UUID; -import java.util.concurrent.TimeUnit; -public class BoatEntity extends Entity { +public class BoatEntity extends Entity implements Leashable, Tickable { /** * Required when IS_BUOYANT is sent in order for boats to work in the water.
@@ -58,23 +58,36 @@ public class BoatEntity extends Entity { private float paddleTimeLeft; private boolean isPaddlingRight; private float paddleTimeRight; + private boolean doTick; /** * Saved for using the "pick" functionality on a boat. */ @Getter - private int variant; + protected final BoatVariant variant; + + private long leashHolderBedrockId = -1; // Looks too fast and too choppy with 0.1f, which is how I believe the Microsoftian client handles it private final float ROWING_SPEED = 0.1f; - public BoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + public BoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, BoatVariant variant) { // Initial rotation is incorrect super(session, entityId, geyserId, uuid, definition, position.add(0d, definition.offset(), 0d), motion, yaw + 90, 0, yaw + 90); + this.variant = variant; + + dirtyMetadata.put(EntityDataTypes.VARIANT, variant.ordinal()); // Required to be able to move on land 1.16.200+ or apply gravity not in the water 1.16.100+ dirtyMetadata.put(EntityDataTypes.IS_BUOYANT, true); - dirtyMetadata.put(EntityDataTypes.BUOYANCY_DATA, BUOYANCY_DATA); + dirtyMetadata.put(EntityDataTypes.BUOYANCY_DATA, BUOYANCY_DATA);; + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // Without this flag you cant stand on boats + setFlag(EntityFlag.COLLIDABLE, true); } @Override @@ -122,51 +135,34 @@ public class BoatEntity extends Entity { moveRelative(0, 0, 0, yaw + 90, 0, 0, isOnGround); } - public void setVariant(IntEntityMetadata entityMetadata) { - variant = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.VARIANT, switch (variant) { - case 6, 7, 8 -> variant - 1; // dark_oak, mangrove, bamboo - case 5 -> 8; // cherry - default -> variant; - }); - } - public void setPaddlingLeft(BooleanEntityMetadata entityMetadata) { isPaddlingLeft = entityMetadata.getPrimitiveValue(); - if (isPaddlingLeft) { - // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing - // This is an asynchronous method that emulates Bedrock rowing until "false" is sent. - paddleTimeLeft = 0f; - if (!this.passengers.isEmpty()) { - // Get the entity by the first stored passenger and convey motion in this manner - Entity entity = this.passengers.get(0); - if (entity != null) { - updateLeftPaddle(session, entity); - } - } - } else { - // Indicate that the row position should be reset + if (!isPaddlingLeft) { + paddleTimeLeft = 0.0f; dirtyMetadata.put(EntityDataTypes.ROW_TIME_LEFT, 0.0f); } } public void setPaddlingRight(BooleanEntityMetadata entityMetadata) { isPaddlingRight = entityMetadata.getPrimitiveValue(); - if (isPaddlingRight) { - paddleTimeRight = 0f; - if (!this.passengers.isEmpty()) { - Entity entity = this.passengers.get(0); - if (entity != null) { - updateRightPaddle(session, entity); - } - } - } else { + if (!isPaddlingRight) { + paddleTimeRight = 0.0f; dirtyMetadata.put(EntityDataTypes.ROW_TIME_RIGHT, 0.0f); } } + @Override + public void setLeashHolderBedrockId(long bedrockId) { + this.leashHolderBedrockId = bedrockId; + dirtyMetadata.put(EntityDataTypes.LEASH_HOLDER, bedrockId); + } + @Override protected InteractiveTag testInteraction(Hand hand) { + InteractiveTag tag = super.testInteraction(hand); + if (tag != InteractiveTag.NONE) { + return tag; + } if (session.isSneaking()) { return InteractiveTag.NONE; } else if (passengers.size() < 2) { @@ -178,6 +174,10 @@ public class BoatEntity extends Entity { @Override public InteractionResult interact(Hand hand) { + InteractionResult result = super.interact(hand); + if (result != InteractionResult.PASS) { + return result; + } if (session.isSneaking()) { return InteractionResult.PASS; } else { @@ -186,30 +186,38 @@ public class BoatEntity extends Entity { } } - private void updateLeftPaddle(GeyserSession session, Entity rower) { + @Override + public void tick() { + // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing + if (session.getPlayerEntity().getVehicle() == this) { + // For packet timing accuracy, we'll send the packets here, as that's what Java Edition 1.21.3 does. + ServerboundPaddleBoatPacket steerPacket = new ServerboundPaddleBoatPacket(session.isSteeringLeft(), session.isSteeringRight()); + session.sendDownstreamGamePacket(steerPacket); + return; + } + doTick = !doTick; // Run every other tick + if (!doTick || passengers.isEmpty()) { + return; + } + + Entity rower = passengers.get(0); + if (rower == null) { + return; + } + if (isPaddlingLeft) { paddleTimeLeft += ROWING_SPEED; - sendAnimationPacket(session, rower, AnimatePacket.Action.ROW_LEFT, paddleTimeLeft); - - session.scheduleInEventLoop(() -> - updateLeftPaddle(session, rower), - 100, - TimeUnit.MILLISECONDS - ); + dirtyMetadata.put(EntityDataTypes.ROW_TIME_LEFT, paddleTimeLeft); + } + if (isPaddlingRight) { + paddleTimeRight += ROWING_SPEED; + dirtyMetadata.put(EntityDataTypes.ROW_TIME_RIGHT, paddleTimeRight); } } - private void updateRightPaddle(GeyserSession session, Entity rower) { - if (isPaddlingRight) { - paddleTimeRight += ROWING_SPEED; - sendAnimationPacket(session, rower, AnimatePacket.Action.ROW_RIGHT, paddleTimeRight); - - session.scheduleInEventLoop(() -> - updateRightPaddle(session, rower), - 100, - TimeUnit.MILLISECONDS - ); - } + @Override + public long leashHolderBedrockId() { + return leashHolderBedrockId; } private void sendAnimationPacket(GeyserSession session, Entity rower, AnimatePacket.Action action, float rowTime) { @@ -219,4 +227,22 @@ public class BoatEntity extends Entity { packet.setRowingTime(rowTime); session.sendUpstreamPacket(packet); } + + /** + * Ordered by Bedrock ordinal + */ + public enum BoatVariant { + OAK, + SPRUCE, + BIRCH, + JUNGLE, + ACACIA, + DARK_OAK, + MANGROVE, + BAMBOO, + CHERRY, + PALE_OAK; + + BoatVariant() {} + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ChestBoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ChestBoatEntity.java index 675e9517d..5475ca772 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ChestBoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ChestBoatEntity.java @@ -25,18 +25,18 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; public class ChestBoatEntity extends BoatEntity { - public ChestBoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { - super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + public ChestBoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, BoatVariant variant) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, variant); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java index 9c7a28f6e..2d0835286 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java index 63b5ff2ab..65b71adcd 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -58,9 +57,13 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { @Override public void setCustomBlock(IntEntityMetadata entityMetadata) { customBlock = entityMetadata.getPrimitiveValue(); + showCustomBlock = entityMetadata.getPrimitiveValue() != 0; if (showCustomBlock) { dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(customBlock)); + dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, customBlockOffset); + } else { + updateDefaultBlockMetadata(); } } @@ -73,18 +76,6 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { } } - @Override - public void setShowCustomBlock(BooleanEntityMetadata entityMetadata) { - if (entityMetadata.getPrimitiveValue()) { - showCustomBlock = true; - dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(customBlock)); - dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, customBlockOffset); - } else { - showCustomBlock = false; - updateDefaultBlockMetadata(); - } - } - public void updateDefaultBlockMetadata() { } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java new file mode 100644 index 000000000..414ed0541 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; + +import java.util.Optional; +import java.util.UUID; + +public class DisplayBaseEntity extends Entity { + + private @Nullable Vector3f baseTranslation; + + public DisplayBaseEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + // Don't allow the display name to be hidden - messes with our armor stand. + // On JE: Hiding the display name still shows the display entity text. + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + // This would usually set EntityDataTypes.NAME, but we are instead using NAME for the text display. + // On JE: custom name does not override text display. + } + + public void setTranslation(EntityMetadata translationMeta){ + this.baseTranslation = translationMeta.getValue(); + if (this.baseTranslation == null) { + return; + } + if (this.vehicle == null) { + this.setRiderSeatPosition(this.baseTranslation); + this.moveRelative(this.baseTranslation.getX(), this.baseTranslation.getY(), this.baseTranslation.getZ(), yaw, pitch, headYaw, false); + } else { + EntityUtils.updateMountOffset(this, this.vehicle, true, true, 0, 1); + this.updateBedrockMetadata(); + } + } + + public Vector3f getTranslation() { + return baseTranslation; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/EnderCrystalEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/EnderCrystalEntity.java index 86436f82b..932864bf7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/EnderCrystalEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/EnderCrystalEntity.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/EnderEyeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/EnderEyeEntity.java new file mode 100644 index 000000000..cc5a58f21 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/EnderEyeEntity.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type; + + +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.UUID; + +public class EnderEyeEntity extends Entity { + public EnderEyeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // Correct sizing + dirtyMetadata.put(EntityDataTypes.SCALE, 0.5f); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index a2b7cc6ec..15584a2be 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -25,59 +25,83 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; +import org.geysermc.geyser.api.entity.property.BatchPropertyUpdater; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.GeyserDirtyMetadata; +import org.geysermc.geyser.entity.properties.GeyserEntityProperties; +import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager; +import org.geysermc.geyser.entity.properties.type.PropertyType; +import org.geysermc.geyser.entity.type.living.MobEntity; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import java.util.Collections; import java.util.EnumSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; @Getter @Setter public class Entity implements GeyserEntity { + private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false")); + protected final GeyserSession session; protected int entityId; protected final long geyserId; protected UUID uuid; + /** + * Do not call this setter directly! + * This will bypass the scoreboard and setting the metadata + */ + @Setter(AccessLevel.NONE) + protected String nametag = ""; protected Vector3f position; protected Vector3f motion; /** * x = Yaw, y = Pitch, z = HeadYaw + * Java: Y = Yaw, X = Pitch */ protected float yaw; protected float pitch; @@ -101,7 +125,7 @@ public class Entity implements GeyserEntity { @Setter(AccessLevel.NONE) private float boundingBoxWidth; @Setter(AccessLevel.NONE) - protected String nametag = ""; + protected String displayName; @Setter(AccessLevel.NONE) protected boolean silent = false; /* Metadata end */ @@ -126,13 +150,16 @@ public class Entity implements GeyserEntity { @Setter(AccessLevel.PROTECTED) // For players private boolean flagsDirty = false; + protected final GeyserEntityPropertyManager propertyManager; + public Entity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { this.session = session; + this.definition = definition; + this.displayName = standardDisplayName(); this.entityId = entityId; this.geyserId = geyserId; this.uuid = uuid; - this.definition = definition; this.motion = motion; this.yaw = yaw; this.pitch = pitch; @@ -140,6 +167,8 @@ public class Entity implements GeyserEntity { this.valid = false; + this.propertyManager = definition.registeredProperties() == null ? null : new GeyserEntityPropertyManager(definition.registeredProperties()); + setPosition(position); setAirSupply(getMaxAir()); @@ -153,11 +182,12 @@ public class Entity implements GeyserEntity { dirtyMetadata.put(EntityDataTypes.SCALE, 1f); dirtyMetadata.put(EntityDataTypes.COLOR, (byte) 0); dirtyMetadata.put(EntityDataTypes.AIR_SUPPLY_MAX, getMaxAir()); - setDimensions(Pose.STANDING); + setDimensionsFromPose(Pose.STANDING); setFlag(EntityFlag.HAS_GRAVITY, true); setFlag(EntityFlag.HAS_COLLISION, true); setFlag(EntityFlag.CAN_SHOW_NAME, true); setFlag(EntityFlag.CAN_CLIMB, true); + setFlag(EntityFlag.HIDDEN_WHEN_INVISIBLE, true); // Let the Java server (or us) supply all sounds for an entity setClientSideSilent(); } @@ -178,14 +208,19 @@ public class Entity implements GeyserEntity { addEntityPacket.setBodyRotation(yaw); // TODO: This should be bodyYaw addEntityPacket.getMetadata().putFlags(flags); dirtyMetadata.apply(addEntityPacket.getMetadata()); + if (propertyManager != null) { + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + propertyManager.applyFloatProperties(addEntityPacket.getProperties().getFloatProperties()); + } addAdditionalSpawnData(addEntityPacket); valid = true; + session.sendUpstreamPacket(addEntityPacket); flagsDirty = false; - if (session.getGeyser().getConfig().isDebugMode()) { + if (session.getGeyser().getConfig().isDebugMode() && PRINT_ENTITY_SPAWN_DEBUG) { EntityType type = definition.entityType(); String name = type != null ? type.name() : getClass().getSimpleName(); session.getGeyser().getLogger().debug("Spawned entity " + name + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); @@ -200,11 +235,9 @@ public class Entity implements GeyserEntity { /** * Despawns the entity - * - * @return can be deleted */ - public boolean despawnEntity() { - if (!valid) return true; + public void despawnEntity() { + if (!valid) return; for (Entity passenger : passengers) { // Make sure all passengers on the despawned entity are updated if (passenger == null) continue; @@ -218,7 +251,6 @@ public class Entity implements GeyserEntity { session.sendUpstreamPacket(removeEntityPacket); valid = false; - return true; } public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, boolean isOnGround) { @@ -344,7 +376,7 @@ public class Entity implements GeyserEntity { * Sends the Bedrock metadata to the client */ public void updateBedrockMetadata() { - if (!valid) { + if (!isValid()) { return; } @@ -356,6 +388,27 @@ public class Entity implements GeyserEntity { flagsDirty = false; } dirtyMetadata.apply(entityDataPacket.getMetadata()); + if (propertyManager != null && propertyManager.hasProperties()) { + propertyManager.applyIntProperties(entityDataPacket.getProperties().getIntProperties()); + propertyManager.applyFloatProperties(entityDataPacket.getProperties().getFloatProperties()); + } + session.sendUpstreamPacket(entityDataPacket); + } + } + + /** + * Sends the Bedrock entity properties to the client + */ + public void updateBedrockEntityProperties() { + if (!valid) { + return; + } + + if (propertyManager != null && propertyManager.hasProperties()) { + SetEntityDataPacket entityDataPacket = new SetEntityDataPacket(); + entityDataPacket.setRuntimeEntityId(geyserId); + propertyManager.applyIntProperties(entityDataPacket.getProperties().getIntProperties()); + propertyManager.applyFloatProperties(entityDataPacket.getProperties().getFloatProperties()); session.sendUpstreamPacket(entityDataPacket); } } @@ -363,12 +416,10 @@ public class Entity implements GeyserEntity { public void setFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); setFlag(EntityFlag.ON_FIRE, ((xd & 0x01) == 0x01) && !getFlag(EntityFlag.FIRE_IMMUNE)); // Otherwise immune entities sometimes flicker onfire - setFlag(EntityFlag.SNEAKING, (xd & 0x02) == 0x02); - setFlag(EntityFlag.SPRINTING, (xd & 0x08) == 0x08); - + setSneaking((xd & 0x02) == 0x02); + setSprinting((xd & 0x08) == 0x08); // Swimming is ignored here and instead we rely on the pose - setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80); - + setGliding((xd & 0x80) == 0x80); setInvisible((xd & 0x20) == 0x20); } @@ -381,6 +432,21 @@ public class Entity implements GeyserEntity { setFlag(EntityFlag.INVISIBLE, value); } + /** + * Set a boolean - whether the entity is gliding + */ + protected void setGliding(boolean value) { + setFlag(EntityFlag.GLIDING, value); + } + + protected void setSprinting(boolean value) { + setFlag(EntityFlag.SPRINTING, value); + } + + protected void setSneaking(boolean value) { + setFlag(EntityFlag.SNEAKING, value); + } + /** * Set an int from 0 - this entity's maximum air - (air / maxAir) represents the percentage of bubbles left */ @@ -396,17 +462,84 @@ public class Entity implements GeyserEntity { return 300; } + public String teamIdentifier() { + // experience orbs were the only known entities that do not send an uuid pre 1.21.5 (even though they do have one), + // but to be safe in the future it's done in the entity class itself instead of the entity specific one. + // All entities without an uuid cannot show up in the scoreboard! + return uuid != null ? uuid.toString() : null; + } + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + // displayName is shown when always display name is enabled. Either with or without team. + // That's why there are both a displayName and a nametag variable. + // Displayname is ignored for players, and is always their username. Optional name = entityMetadata.getValue(); if (name.isPresent()) { - nametag = MessageTranslator.convertMessage(name.get(), session.locale()); - dirtyMetadata.put(EntityDataTypes.NAME, nametag); - } else if (!nametag.isEmpty()) { - // Clear nametag - dirtyMetadata.put(EntityDataTypes.NAME, ""); + String displayName = MessageTranslator.convertMessage(name.get(), session.locale()); + this.displayName = displayName; + setNametag(displayName, true); + return; } + + // if no displayName is set, use entity name (ENDER_DRAGON -> Ender Dragon) + // maybe we can/should use a translatable here instead? + this.displayName = standardDisplayName(); + setNametag(null, true); } + protected String standardDisplayName() { + return EntityUtils.translatedEntityName(definition.entityType(), session); + } + + protected void setNametag(@Nullable String nametag, boolean fromDisplayName) { + // ensure that the team format is used when nametag changes + if (nametag != null && fromDisplayName) { + var team = session.getWorldCache().getScoreboard().getTeamFor(teamIdentifier()); + if (team != null) { + updateNametag(team); + return; + } + } + + if (nametag == null) { + nametag = ""; + } + boolean changed = !Objects.equals(this.nametag, nametag); + this.nametag = nametag; + // we only update metadata if the value has changed + if (!changed) { + return; + } + + dirtyMetadata.put(EntityDataTypes.NAME, nametag); + // if nametag (player with team) is hidden for player, so should the score (belowname) + scoreVisibility(!nametag.isEmpty()); + } + + public void updateNametag(@Nullable Team team) { + // allow LivingEntity+ to have a different visibility check + updateNametag(team, true); + } + + protected void updateNametag(@Nullable Team team, boolean visible) { + if (team != null) { + String newNametag; + // (team) visibility is LivingEntity+, team displayName is Entity+ + if (visible) { + newNametag = team.displayName(getDisplayName()); + } else { + // The name is not visible to the session player; clear name + newNametag = ""; + } + setNametag(newNametag, false); + return; + } + // The name has reset, if it was previously something else + setNametag(null, false); + } + + protected void scoreVisibility(boolean show) {} + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0)); } @@ -424,15 +557,16 @@ public class Entity implements GeyserEntity { */ public void setPose(Pose pose) { setFlag(EntityFlag.SLEEPING, pose.equals(Pose.SLEEPING)); + // FALL_FLYING is instead set via setFlags // Triggered when crawling setFlag(EntityFlag.SWIMMING, pose.equals(Pose.SWIMMING)); - setDimensions(pose); + setDimensionsFromPose(pose); } /** * Set the height and width of the entity's bounding box */ - protected void setDimensions(Pose pose) { + protected void setDimensionsFromPose(Pose pose) { // No flexibility options for basic entities setBoundingBoxHeight(definition.height()); setBoundingBoxWidth(definition.width()); @@ -498,7 +632,7 @@ public class Entity implements GeyserEntity { Entity passenger = passengers.get(i); if (passenger != null) { boolean rider = i == 0; - EntityUtils.updateMountOffset(passenger, this, rider, true, passengers.size() > 1); + EntityUtils.updateMountOffset(passenger, this, rider, true, i, passengers.size()); passenger.updateBedrockMetadata(); } } @@ -510,7 +644,7 @@ public class Entity implements GeyserEntity { protected void updateMountOffset() { if (vehicle != null) { boolean rider = vehicle.getPassengers().get(0) == this; - EntityUtils.updateMountOffset(this, vehicle, rider, true, vehicle.getPassengers().size() > 1); + EntityUtils.updateMountOffset(this, vehicle, rider, true, vehicle.getPassengers().indexOf(this), vehicle.getPassengers().size()); updateBedrockMetadata(); } } @@ -544,6 +678,17 @@ public class Entity implements GeyserEntity { * Should usually mirror {@link #interact(Hand)} without any side effects. */ protected InteractiveTag testInteraction(Hand hand) { + if (isAlive() && this instanceof Leashable leashable) { + if (leashable.leashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) { + // Note this might be client side. Has yet to be an issue though, as of Java 1.21. + return InteractiveTag.REMOVE_LEASH; + } + if (session.getPlayerInventory().getItemInHand(hand).is(Items.LEAD) && leashable.canBeLeashed()) { + // We shall leash + return InteractiveTag.LEASH; + } + } + return InteractiveTag.NONE; } @@ -552,9 +697,48 @@ public class Entity implements GeyserEntity { * to ensure packet parity as well as functionality parity (such as sound effect responses). */ public InteractionResult interact(Hand hand) { + Item itemInHand = session.getPlayerInventory().getItemInHand(hand).asItem(); + if (itemInHand == Items.SHEARS) { + if (hasLeashesToDrop()) { + return InteractionResult.SUCCESS; + } + + if (this instanceof MobEntity mob && !session.isSneaking() && mob.canShearEquipment()) { + return InteractionResult.SUCCESS; + } + } else if (isAlive() && this instanceof Leashable leashable) { + if (leashable.leashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) { + // Note this might also update client side (a theoretical Geyser/client desync and Java parity issue). + // Has yet to be an issue though, as of Java 1.21. + return InteractionResult.SUCCESS; + } + if (session.getPlayerInventory().getItemInHand(hand).is(Items.LEAD) + && !(session.getEntityCache().getEntityByGeyserId(leashable.leashHolderBedrockId()) instanceof PlayerEntity)) { + // We shall leash + return InteractionResult.SUCCESS; + } + } + return InteractionResult.PASS; } + public boolean hasLeashesToDrop() { + BoundingBox searchBB = new BoundingBox(position.getX(), position.getY(), position.getZ(), 32, 32, 32); + List leashedInRange = session.getEntityCache().getEntities().values().stream() + .filter(entity -> entity instanceof Leashable leashablex && leashablex.leashHolderBedrockId() == this.getGeyserId()) + .filter(entity -> { + BoundingBox leashedBB = new BoundingBox(entity.position.toDouble(), entity.boundingBoxWidth, entity.boundingBoxHeight, entity.boundingBoxWidth); + return searchBB.checkIntersection(leashedBB); + }).map(Leashable.class::cast).toList(); + + boolean found = !leashedInRange.isEmpty(); + if (this instanceof Leashable leashable && leashable.isLeashed()) { + found = true; + } + + return found; + } + /** * Simulates interacting with this entity at a specific click point. As of Java Edition 1.18.1, this is only used for armor stands. */ @@ -580,8 +764,41 @@ public class Entity implements GeyserEntity { session.sendUpstreamPacket(packet); } - @SuppressWarnings("unchecked") - public @Nullable I as(Class entityClass) { - return entityClass.isInstance(this) ? (I) this : null; + @Override + public void updatePropertiesBatched(Consumer consumer) { + if (this.propertyManager != null) { + Objects.requireNonNull(consumer); + GeyserEntityProperties propertyDefinitions = definition.registeredProperties(); + consumer.accept(new BatchPropertyUpdater() { + @Override + public void update(@NonNull GeyserEntityProperty property, @Nullable T value) { + Objects.requireNonNull(property, "property must not be null!"); + if (!(property instanceof PropertyType propertyType)) { + throw new IllegalArgumentException("Invalid property implementation! Got: " + property.getClass().getSimpleName()); + } + int index = propertyDefinitions.getPropertyIndex(property.identifier().toString()); + if (index < 0) { + throw new IllegalArgumentException("No property with the name " + property.identifier() + " has been registered."); + } + + var expectedProperty = propertyDefinitions.getProperties().get(index); + if (!expectedProperty.equals(propertyType)) { + throw new IllegalArgumentException("The supplied property was not registered with this entity type!"); + } + + propertyType.apply(propertyManager, value); + } + }); + + if (propertyManager.hasProperties()) { + SetEntityDataPacket packet = new SetEntityDataPacket(); + packet.setRuntimeEntityId(getGeyserId()); + propertyManager.applyFloatProperties(packet.getProperties().getFloatProperties()); + propertyManager.applyIntProperties(packet.getProperties().getIntProperties()); + session.sendUpstreamPacket(packet); + } + } else { + throw new IllegalArgumentException("Given entity has no registered properties!"); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java index 5a79a98b3..8cca969b1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java @@ -27,14 +27,15 @@ package org.geysermc.geyser.entity.type; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; -import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import java.util.UUID; + public class ExpOrbEntity extends Entity { - public ExpOrbEntity(GeyserSession session, int amount, int entityId, long geyserId, Vector3f position) { - super(session, entityId, geyserId, null, EntityDefinitions.EXPERIENCE_ORB, position, Vector3f.ZERO, 0, 0, 0); - - this.dirtyMetadata.put(EntityDataTypes.TRADE_EXPERIENCE, amount); + public ExpOrbEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition entityDefinition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, entityDefinition, position, motion, yaw, pitch, headYaw); + this.dirtyMetadata.put(EntityDataTypes.TRADE_EXPERIENCE, 1); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FallingBlockEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FallingBlockEntity.java index e6d3a5783..4fcec7a63 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FallingBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FallingBlockEntity.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FireballEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FireballEntity.java index 3db032f0f..904596b3a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FireballEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FireballEntity.java @@ -72,6 +72,9 @@ public class FireballEntity extends ThrowableEntity { @Override public void tick() { + if (removedInVoid()) { + return; + } moveAbsoluteImmediate(tickMovement(position), getYaw(), getPitch(), getHeadYaw(), false, false); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java index 7a544f23c..8db92a118 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java @@ -25,31 +25,26 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.ListTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtMapBuilder; -import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.MovementEffectType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; -import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; -import org.geysermc.floodgate.util.DeviceOs; +import org.cloudburstmc.protocol.bedrock.packet.MovementEffectPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.type.player.PlayerEntity; -import org.geysermc.geyser.level.FireworkColor; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.util.MathUtils; +import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import java.util.ArrayList; -import java.util.List; import java.util.OptionalInt; import java.util.UUID; public class FireworkEntity extends Entity { + private boolean attachedToSession; + public FireworkEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -59,103 +54,66 @@ public class FireworkEntity extends Entity { if (item == null) { return; } - CompoundTag tag = item.getNbt(); - - if (tag == null) { + DataComponents components = item.getDataComponentsPatch(); + if (components == null) { return; } - // TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices. - // https://bugs.mojang.com/browse/MCPE-89115 - if (session.getClientData().getDeviceOs() == DeviceOs.XBOX - || session.getClientData().getDeviceOs() == DeviceOs.PS4) { - return; - } - - CompoundTag fireworks = tag.get("Fireworks"); - if (fireworks == null) { - // Thank you Mineplex very cool - return; - } - - NbtMapBuilder fireworksBuilder = NbtMap.builder(); - if (fireworks.get("Flight") != null) { - fireworksBuilder.putByte("Flight", MathUtils.getNbtByte(fireworks.get("Flight").getValue())); - } - - List explosions = new ArrayList<>(); - if (fireworks.get("Explosions") != null) { - for (Tag effect : ((ListTag) fireworks.get("Explosions")).getValue()) { - CompoundTag effectData = (CompoundTag) effect; - NbtMapBuilder effectBuilder = NbtMap.builder(); - - if (effectData.get("Type") != null) { - effectBuilder.putByte("FireworkType", MathUtils.getNbtByte(effectData.get("Type").getValue())); - } - - if (effectData.get("Colors") != null) { - int[] oldColors = (int[]) effectData.get("Colors").getValue(); - byte[] colors = new byte[oldColors.length]; - - int i = 0; - for (int color : oldColors) { - colors[i++] = FireworkColor.fromJavaRGB(color); - } - - effectBuilder.putByteArray("FireworkColor", colors); - } - - if (effectData.get("FadeColors") != null) { - int[] oldColors = (int[]) effectData.get("FadeColors").getValue(); - byte[] colors = new byte[oldColors.length]; - - int i = 0; - for (int color : oldColors) { - colors[i++] = FireworkColor.fromJavaRGB(color); - } - - effectBuilder.putByteArray("FireworkFade", colors); - } - - if (effectData.get("Trail") != null) { - effectBuilder.putByte("FireworkTrail", MathUtils.getNbtByte(effectData.get("Trail").getValue())); - } - - if (effectData.get("Flicker") != null) { - effectBuilder.putByte("FireworkFlicker", MathUtils.getNbtByte(effectData.get("Flicker").getValue())); - } - - explosions.add(effectBuilder.build()); - } - } - - fireworksBuilder.putList("Explosions", NbtType.COMPOUND, explosions); - - NbtMapBuilder builder = NbtMap.builder(); - builder.put("Fireworks", fireworksBuilder.build()); + // TODO this looked the same, so I'm going to assume it is and (keep below comment if true) + // Translate using item methods to get firework NBT for Bedrock + BedrockItemBuilder builder = new BedrockItemBuilder(); + TooltipOptions tooltip = TooltipOptions.fromComponents(components); + Items.FIREWORK_ROCKET.translateComponentsToBedrock(session, components, tooltip, builder); + dirtyMetadata.put(EntityDataTypes.DISPLAY_FIREWORK, builder.build()); } public void setPlayerGliding(EntityMetadata entityMetadata) { - OptionalInt optional = entityMetadata.getValue(); - // Checks if the firework has an entity ID (used when a player is gliding) - // and checks to make sure the player that is gliding is the one getting sent the packet - // or else every player near the gliding player will boost too. - if (optional.isPresent() && optional.getAsInt() == session.getPlayerEntity().getEntityId()) { - PlayerEntity entity = session.getPlayerEntity(); - float yaw = entity.getYaw(); - float pitch = entity.getPitch(); - // Uses math from NukkitX - entity.setMotion(Vector3f.from( - -Math.sin(Math.toRadians(yaw)) * Math.cos(Math.toRadians(pitch)) * 2, - -Math.sin(Math.toRadians(pitch)) * 2, - Math.cos(Math.toRadians(yaw)) * Math.cos(Math.toRadians(pitch)) * 2)); - // Need to update the EntityMotionPacket or else the player won't boost - SetEntityMotionPacket entityMotionPacket = new SetEntityMotionPacket(); - entityMotionPacket.setRuntimeEntityId(entity.getGeyserId()); - entityMotionPacket.setMotion(entity.getMotion()); + session.getAttachedFireworkRockets().remove(this.geyserId); - session.sendUpstreamPacket(entityMotionPacket); + OptionalInt optional = entityMetadata.getValue(); + if (optional.isPresent() && optional.getAsInt() == session.getPlayerEntity().getEntityId()) { + // If we don't send this, the bedrock client will always stop boosting after 20 ticks + // However this is not the case for Java as the player will stop boosting after entity despawn. + // So we let player boost for a really long time and then only stop them when the entity despawn. + // Also doing this allow player to boost simply by having a fireworks rocket attached to them + // and not necessary have to use a rocket (as some plugin do this to boost player) + // You can't really send Integer.MAX_VALUE since Bedrock client doesn't seem to like way too large number very much (as of 1.21.73). + sendElytraBoost(1000000); + this.attachedToSession = true; + + // We need to keep track of the fireworks rockets. + session.getAttachedFireworkRockets().add(this.getGeyserId()); + } else { + // Also ensure player stop boosting in cases like metadata changes. + if (this.attachedToSession && session.getAttachedFireworkRockets().isEmpty()) { + sendElytraBoost(0); + this.attachedToSession = false; + } } } + + @Override + public void despawnEntity() { + session.getAttachedFireworkRockets().remove(this.geyserId); + // We have to ensure that these fireworks is attached to entity and this is the only one that is attached to the player. + // Else player will stop boosting even if the fireworks is not attached to them or there is a fireworks that is boosting them + // and not just this one. + if (this.attachedToSession && session.getAttachedFireworkRockets().isEmpty()) { + // Since we send an effect packet for player to boost really long, we have to stop them when the entity despawn. + sendElytraBoost(0); + this.attachedToSession = false; + } + + super.despawnEntity(); + } + + private void sendElytraBoost(int duration) { + MovementEffectPacket movementEffect = new MovementEffectPacket(); + movementEffect.setDuration(duration); + movementEffect.setEffectType(MovementEffectType.GLIDE_BOOST); + movementEffect.setEntityRuntimeId(session.getPlayerEntity().getGeyserId()); + movementEffect.setTick(session.getClientTicks()); + session.sendUpstreamPacket(movementEffect); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java index bcbff16ce..26a64bcae 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java @@ -25,19 +25,20 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; -import lombok.Getter; import org.geysermc.erosion.util.BlockPositionIterator; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.collision.BlockCollision; import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; @@ -133,6 +134,9 @@ public class FishingHookEntity extends ThrowableEntity { @Override public void tick() { + if (removedInVoid()) { + return; + } if (hooked || !isInAir() && !isInWater() || isOnGround()) { motion = Vector3f.ZERO; return; @@ -159,7 +163,7 @@ public class FishingHookEntity extends ThrowableEntity { */ protected boolean isInAir() { int block = session.getGeyser().getWorldManager().getBlockAt(session, position.toInt()); - return block == BlockStateValues.JAVA_AIR_ID; + return block == Block.JAVA_AIR_ID; } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java index a7a117fff..e33e6d7b6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java @@ -25,14 +25,16 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -51,7 +53,8 @@ public class FurnaceMinecartEntity extends DefaultBlockMinecartEntity { @Override public void updateDefaultBlockMetadata() { - dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(hasFuel ? BlockStateValues.JAVA_FURNACE_LIT_ID : BlockStateValues.JAVA_FURNACE_ID)); + BlockState furnace = Blocks.FURNACE.defaultBlockState().withValue(Properties.LIT, hasFuel); + dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(furnace)); dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, 6); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/HangingEntity.java similarity index 73% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/HangingEntity.java index 164fb1b6c..7c0e8a773 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/HangingEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,26 +23,25 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.type.Item; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import java.util.Set; import java.util.UUID; -public class ChickenEntity extends AnimalEntity { - private static final Set VALID_FOOD = Set.of(Items.WHEAT_SEEDS, Items.MELON_SEEDS, Items.PUMPKIN_SEEDS, Items.BEETROOT_SEEDS); +public abstract class HangingEntity extends Entity { - public ChickenEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + public HangingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public boolean canEat(Item item) { - return VALID_FOOD.contains(item); + public void setDirectionMetadata(EntityMetadata direction) { + setDirection(direction.getValue()); } + + public abstract void setDirection(Direction direction); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java index b18ab4fb3..b6a147297 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java @@ -25,17 +25,24 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; +import net.kyori.adventure.text.Component; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; +import java.util.Optional; import java.util.UUID; public class InteractionEntity extends Entity { @@ -50,6 +57,16 @@ public class InteractionEntity extends Entity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + /** + * On Java Edition, interaction entities can have a nametag. Invisibility hides the name on Bedrock. + * By having a second entity, we can still show the nametag while keeping the interaction entity invisible. + */ + private ArmorStandEntity secondEntity = null; + + private boolean isNameTagVisible = false; + + private boolean isVisible = true; + @Override protected void initializeMetadata() { super.initializeMetadata(); @@ -58,6 +75,19 @@ public class InteractionEntity extends Entity { setFlag(EntityFlag.INVISIBLE, true); } + @Override + protected void setInvisible(boolean value) { + // Always invisible; would reveal the armor stand otherwise + isVisible = value; + this.updateNameTag(); + } + + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + isNameTagVisible = entityMetadata.getPrimitiveValue(); + this.updateNameTag(); + } + @Override public InteractionResult interact(Hand hand) { // these InteractionResults do mirror the java client @@ -75,15 +105,75 @@ public class InteractionEntity extends Entity { return InteractionResult.CONSUME; } + @Override + public void despawnEntity() { + if (secondEntity != null) { + secondEntity.despawnEntity(); + } + super.despawnEntity(); + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + super.setDisplayName(entityMetadata); + this.updateNameTag(); + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + moveAbsolute(position.add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + if (secondEntity != null) { + secondEntity.moveAbsolute(position.up(getBoundingBoxHeight()), yaw, pitch, headYaw, isOnGround, teleported); + } + super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); + } + public void setWidth(FloatEntityMetadata width) { setBoundingBoxWidth(width.getPrimitiveValue()); } public void setHeight(FloatEntityMetadata height) { - setBoundingBoxHeight(height.getPrimitiveValue()); + // Bedrock does *not* like high values being placed here + // https://gist.github.com/Owen1212055/f5d59169d3a6a5c32f0c173d57eb199d recommend(s/ed) using the tactic + // https://github.com/GeyserMC/Geyser/issues/4688 + setBoundingBoxHeight(Math.min(height.getPrimitiveValue(), 64f)); + + if (secondEntity != null) { + secondEntity.moveAbsolute(position.up(getBoundingBoxHeight()), yaw, pitch, onGround, true); + } } public void setResponse(BooleanEntityMetadata response) { this.response = response.getPrimitiveValue(); } + + public void updateNameTag() { + if (this.nametag.isBlank() || !isVisible) { + if (secondEntity != null) { + secondEntity.despawnEntity(); + secondEntity = null; + } + return; + } + + if (this.secondEntity == null) { + secondEntity = new ArmorStandEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(), null, + EntityDefinitions.ARMOR_STAND, position.up(getBoundingBoxHeight()), motion, getYaw(), getPitch(), getHeadYaw()); + } + secondEntity.getDirtyMetadata().put(EntityDataTypes.NAME, nametag); + secondEntity.getDirtyMetadata().put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, isNameTagVisible ? (byte) 1 : (byte) 0); + // Scale to 0 to show nametag + secondEntity.setScale(0f); + // No bounding box as we don't want to interact with this entity + secondEntity.getDirtyMetadata().put(EntityDataTypes.WIDTH, 0.0f); + secondEntity.getDirtyMetadata().put(EntityDataTypes.HEIGHT, 0.0f); + secondEntity.getDirtyMetadata().put(EntityDataTypes.HITBOX, NbtMap.EMPTY); + if (!secondEntity.valid) { // Spawn the entity once + secondEntity.spawnEntity(); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java index bb67a60f6..49eb9ddc4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; @@ -36,8 +34,11 @@ import org.cloudburstmc.protocol.bedrock.packet.AddItemEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.item.ItemTranslator; +import org.geysermc.geyser.translator.item.ItemTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -74,7 +75,7 @@ public class ItemEntity extends ThrowableEntity { @Override public void tick() { - if (isInWater()) { + if (removedInVoid() || isInWater()) { return; } if (!isOnGround() || (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) > 0.00001) { @@ -137,7 +138,7 @@ public class ItemEntity extends ThrowableEntity { protected float getDrag() { if (isOnGround()) { Vector3i groundBlockPos = position.toInt().down(1); - int blockState = session.getGeyser().getWorldManager().getBlockAt(session, groundBlockPos); + BlockState blockState = session.getGeyser().getWorldManager().blockAt(session, groundBlockPos); return BlockStateValues.getSlipperiness(blockState) * 0.98f; } return 0.98f; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java index 295972200..8208f7525 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java @@ -25,12 +25,7 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; +import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; @@ -39,19 +34,24 @@ import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.BlockEntityDataPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; -import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.item.ItemTranslator; +import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.UUID; /** * Item frames are an entity in Java but a block entity in Bedrock. */ -public class ItemFrameEntity extends Entity { +public class ItemFrameEntity extends HangingEntity { /** * Used for getting the Bedrock block position. * Blocks deal with integers whereas entities deal with floats. @@ -60,7 +60,7 @@ public class ItemFrameEntity extends Entity { /** * Specific block 'state' we are emulating in Bedrock. */ - private final BlockDefinition blockDefinition; + private BlockDefinition blockDefinition; /** * Rotation of item in frame. */ @@ -75,22 +75,14 @@ public class ItemFrameEntity extends Entity { @Getter private ItemStack heldItem = null; /** - * Determines if this entity needs updated on the client end/ + * Determines if this entity needs to be updated on the client end. */ private boolean changed = true; - public ItemFrameEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, Direction direction) { + public ItemFrameEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); - NbtMapBuilder blockBuilder = NbtMap.builder() - .putString("name", this.definition.entityType() == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_frame" : "minecraft:frame"); - NbtMapBuilder statesBuilder = NbtMap.builder() - .putInt("facing_direction", direction.ordinal()) - .putByte("item_frame_map_bit", (byte) 0) - .putByte("item_frame_photo_bit", (byte) 0); - blockBuilder.put("states", statesBuilder.build()); - - blockDefinition = session.getBlockMappings().getItemFrame(blockBuilder.build()); + blockDefinition = buildBlockDefinition(Direction.SOUTH); // Default to SOUTH direction, like on Java - entity metadata should correct this when necessary bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ()); session.getItemFrameCache().put(bedrockPosition, this); @@ -109,18 +101,33 @@ public class ItemFrameEntity extends Entity { valid = true; } + @Override + public void setDirection(Direction direction) { + blockDefinition = buildBlockDefinition(direction); + changed = true; + } + public void setItemInFrame(EntityMetadata entityMetadata) { if (entityMetadata.getValue() != null) { this.heldItem = entityMetadata.getValue(); ItemData itemData = ItemTranslator.translateToBedrock(session, heldItem); - String customIdentifier = session.getItemMappings().getCustomIdMappings().get(itemData.getDefinition().getRuntimeId()); NbtMapBuilder builder = NbtMap.builder(); - builder.putByte("Count", (byte) itemData.getCount()); - if (itemData.getTag() != null) { - builder.put("tag", itemData.getTag()); + NbtMap itemDataTag = itemData.getTag(); + if (itemDataTag != null) { + // Remove custom name that Geyser sets for items due to translating non-"custom_name" components + String customName = ItemTranslator.getCustomName(session, heldItem.getDataComponentsPatch(), + session.getItemMappings().getMapping(heldItem), 'f', true, false); + if (customName == null) { + // No custom name found, must modify tag if custom name exists + NbtMapBuilder copy = itemDataTag.toBuilder(); + copy.remove("display"); // Also removes lore, but, should not matter + itemDataTag = copy.build(); + } + + builder.put("tag", itemDataTag); } builder.putShort("Damage", (short) itemData.getDamage()); builder.putString("Name", customIdentifier != null ? customIdentifier : session.getItemMappings().getMapping(entityMetadata.getValue()).getBedrockIdentifier()); @@ -148,7 +155,7 @@ public class ItemFrameEntity extends Entity { } @Override - public boolean despawnEntity() { + public void despawnEntity() { UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); updateBlockPacket.setDataLayer(0); updateBlockPacket.setBlockPosition(bedrockPosition); @@ -161,7 +168,6 @@ public class ItemFrameEntity extends Entity { session.getItemFrameCache().remove(bedrockPosition, this); valid = false; - return true; } private NbtMap getDefaultTag() { @@ -214,6 +220,18 @@ public class ItemFrameEntity extends Entity { return InventoryUtils.isEmpty(heldItem) && session.getPlayerInventory().getItemInHand(hand).isEmpty() ? InteractionResult.PASS : InteractionResult.SUCCESS; } + private BlockDefinition buildBlockDefinition(Direction direction) { + NbtMapBuilder blockBuilder = NbtMap.builder() + .putString("name", this.definition.entityType() == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_frame" : "minecraft:frame"); + NbtMapBuilder statesBuilder = NbtMap.builder() + .putInt("facing_direction", direction.ordinal()) + .putByte("item_frame_map_bit", (byte) 0) + .putByte("item_frame_photo_bit", (byte) 0); + blockBuilder.put("states", statesBuilder.build()); + + return session.getBlockMappings().getItemFrame(blockBuilder.build()); + } + /** * Finds the Java entity ID of an item frame from its Bedrock position. * @param position position of item frame in Bedrock. diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java index 3f0d2ee68..af739297c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java @@ -25,11 +25,11 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java b/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java new file mode 100644 index 000000000..24527c5e7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type; + +/** + * I can haz lead + * (The item, not the mineral) + */ +public interface Leashable { + void setLeashHolderBedrockId(long bedrockId); + + long leashHolderBedrockId(); + + default boolean canBeLeashed() { + return true; + } + + default boolean isLeashed() { + return leashHolderBedrockId() != -1L; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 245b99cef..d6d880d0a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -25,16 +25,6 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.attribute.Attribute; -import com.github.steveice10.mc.protocol.data.game.entity.attribute.AttributeType; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.StringTag; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -45,31 +35,52 @@ import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; -import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.MobArmorEquipmentPacket; import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; +import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Optional; +import java.util.UUID; @Getter @Setter public class LivingEntity extends Entity { - - protected ItemData helmet = ItemData.AIR; - protected ItemData chestplate = ItemData.AIR; - protected ItemData leggings = ItemData.AIR; - protected ItemData boots = ItemData.AIR; - protected ItemData hand = ItemData.AIR; - protected ItemData offHand = ItemData.AIR; + protected EnumMap equipment = new EnumMap<>(EquipmentSlot.class); @Getter(value = AccessLevel.NONE) protected float health = 1f; // The default value in Java Edition before any entity metadata is sent @@ -81,17 +92,116 @@ public class LivingEntity extends Entity { */ private boolean isMaxFrozenState = false; + /** + * The base scale entity data, without attributes applied. Used for such cases as baby variants. + */ + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private float scale; + /** + * The scale sent through the Java attributes packet + */ + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private float attributeScale; + public LivingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + public GeyserItemStack getItemInSlot(EquipmentSlot slot) { + GeyserItemStack stack = equipment.get(slot); + if (stack == null) { + return GeyserItemStack.EMPTY; + } + return stack; + } + + public GeyserItemStack getMainHandItem() { + return getItemInSlot(EquipmentSlot.MAIN_HAND); + } + + public GeyserItemStack getOffHandItem() { + return getItemInSlot(EquipmentSlot.OFF_HAND); + } + + public boolean isHolding(Item item) { + return getMainHandItem().is(item) || getOffHandItem().is(item); + } + + public void setHelmet(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.HELMET, stack); + } + + public void setChestplate(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.CHESTPLATE, stack); + } + + public void setLeggings(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.LEGGINGS, stack); + } + + public void setBoots(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.BOOTS, stack); + } + + public void setBody(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.BODY, stack); + } + + public void setSaddle(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.SADDLE, stack); + + boolean saddled = false; + if (!stack.isEmpty()) { + Equippable equippable = stack.getComponent(DataComponentTypes.EQUIPPABLE); + saddled = equippable != null && equippable.slot() == EquipmentSlot.SADDLE; + } + + updateSaddled(saddled); + } + + public void setHand(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.MAIN_HAND, stack); + } + + public void setOffhand(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.OFF_HAND, stack); + } + + protected void updateSaddled(boolean saddled) { + setFlag(EntityFlag.SADDLED, saddled); + updateBedrockMetadata(); + + // Update the interactive tag, if necessary + Entity mouseoverEntity = session.getMouseoverEntity(); + if (mouseoverEntity != null && mouseoverEntity.getEntityId() == entityId) { + mouseoverEntity.updateInteractiveTag(); + } + } + + public void switchHands() { + GeyserItemStack offhand = this.equipment.get(EquipmentSlot.OFF_HAND); + this.equipment.put(EquipmentSlot.OFF_HAND, this.equipment.get(EquipmentSlot.MAIN_HAND)); + this.equipment.put(EquipmentSlot.MAIN_HAND, offhand); + } + @Override protected void initializeMetadata() { + // Initialize here so overriding classes don't have 0 values + this.scale = 1f; + this.attributeScale = 1f; super.initializeMetadata(); // Matches Bedrock behavior; is always set to this dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1); } + @Override + public void updateNametag(@Nullable Team team) { + // if name not visible, don't mark it as visible + updateNametag(team, team == null || team.isVisibleFor(session.getPlayerEntity().getUsername())); + } + public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); @@ -105,12 +215,16 @@ public class LivingEntity extends Entity { setFlag(EntityFlag.BLOCKING, isUsingItem && isUsingShield); // Riptide spin attack - setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, (xd & 0x04) == 0x04); + setSpinAttack((xd & 0x04) == 0x04); // OptionalPack usage setFlag(EntityFlag.EMERGING, isUsingItem && isUsingOffhand); } + protected void setSpinAttack(boolean value) { + setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, value); + } + public void setHealth(FloatEntityMetadata entityMetadata) { this.health = entityMetadata.getPrimitiveValue(); @@ -121,6 +235,37 @@ public class LivingEntity extends Entity { session.sendUpstreamPacket(attributesPacket); } + // TODO: support all particle types + public void setParticles(ObjectEntityMetadata> entityMetadata) { + List particles = entityMetadata.getValue(); + float r = 0f; + float g = 0f; + float b = 0f; + + int count = 0; + for (Particle particle : particles) { + if (particle.getType() != ParticleType.ENTITY_EFFECT) { + continue; + } + + int color = ((ColorParticleData) particle.getData()).getColor(); + r += ((color >> 16) & 0xFF) / 255f; + g += ((color >> 8) & 0xFF) / 255f; + b += ((color) & 0xFF) / 255f; + count++; + } + + int result = 0; + if (count > 0) { + r = r / count * 255f; + g = g / count * 255f; + b = b / count * 255f; + result = (int) r << 16 | (int) g << 8 | (int) b; + } + + dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, result); + } + public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) { Optional optionalPos = entityMetadata.getValue(); if (optionalPos.isPresent()) { @@ -133,11 +278,10 @@ public class LivingEntity extends Entity { } protected boolean hasShield(boolean offhand) { - ItemMapping shieldMapping = session.getItemMappings().getStoredItems().shield(); if (offhand) { - return offHand.getDefinition().equals(shieldMapping.getBedrockDefinition()); + return getOffHandItem().is(Items.SHIELD); } else { - return hand.getDefinition().equals(shieldMapping.getBedrockDefinition()); + return getMainHandItem().is(Items.SHIELD); } } @@ -147,12 +291,12 @@ public class LivingEntity extends Entity { } @Override - protected void setDimensions(Pose pose) { + protected void setDimensionsFromPose(Pose pose) { if (pose == Pose.SLEEPING) { setBoundingBoxWidth(0.2f); setBoundingBoxHeight(0.2f); } else { - super.setDimensions(pose); + super.setDimensionsFromPose(pose); } } @@ -164,6 +308,21 @@ public class LivingEntity extends Entity { return freezingPercentage; } + protected void setScale(float scale) { + this.scale = scale; + applyScale(); + } + + private void setAttributeScale(float scale) { + this.attributeScale = MathUtils.clamp(scale, GeyserAttributeType.SCALE.getMinimum(), GeyserAttributeType.SCALE.getMaximum()); + applyScale(); + } + + private void applyScale() { + // Take any adjustments Bedrock requires, and compute it alongside the attribute's additional changes + this.dirtyMetadata.put(EntityDataTypes.SCALE, scale * attributeScale); + } + /** * @return a Bedrock health attribute constructed from the data sent from the server */ @@ -181,7 +340,7 @@ public class LivingEntity extends Entity { @Override public InteractionResult interact(Hand hand) { GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand); - if (itemStack.asItem() == Items.NAME_TAG) { + if (itemStack.is(Items.NAME_TAG)) { InteractionResult result = checkInteractWithNameTag(itemStack); if (result.consumesAction()) { return result; @@ -191,50 +350,80 @@ public class LivingEntity extends Entity { return super.interact(hand); } + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + if (this instanceof ClientVehicle clientVehicle) { + if (clientVehicle.isClientControlled()) { + return; + } + clientVehicle.getVehicleComponent().moveRelative(relX, relY, relZ); + } + + super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); + } + + @Override + public boolean setBoundingBoxHeight(float height) { + if (valid && this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setHeight(height); + } + + return super.setBoundingBoxHeight(height); + } + + @Override + public void setBoundingBoxWidth(float width) { + if (valid && this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setWidth(width); + } + + super.setBoundingBoxWidth(width); + } + /** * Checks to see if a nametag interaction would go through. */ + // Implementation note for 1.20.5: this code was moved to the NameTag item. protected final InteractionResult checkInteractWithNameTag(GeyserItemStack itemStack) { - CompoundTag nbt = itemStack.getNbt(); - if (nbt != null && nbt.get("display") instanceof CompoundTag displayTag && displayTag.get("Name") instanceof StringTag) { + if (itemStack.getComponent(DataComponentTypes.CUSTOM_NAME) != null) { // The mob shall be named return InteractionResult.SUCCESS; } return InteractionResult.PASS; } - public void updateArmor(GeyserSession session) { + public void updateArmor() { if (!valid) return; - ItemData helmet = this.helmet; - ItemData chestplate = this.chestplate; + GeyserItemStack helmet = getItemInSlot(EquipmentSlot.HELMET); + GeyserItemStack chestplate = getItemInSlot(EquipmentSlot.CHESTPLATE); // If an entity has a banner on them, it will be in the helmet slot in Java but the chestplate spot in Bedrock // But don't overwrite the chestplate if it isn't empty - ItemMapping banner = session.getItemMappings().getStoredItems().banner(); - if (ItemData.AIR.equals(chestplate) && helmet.getDefinition().equals(banner.getBedrockDefinition())) { - chestplate = this.helmet; - helmet = ItemData.AIR; - } else if (chestplate.getDefinition().equals(banner.getBedrockDefinition())) { + if (chestplate.isEmpty() && helmet.is(session, ItemTag.BANNERS)) { + chestplate = helmet; + helmet = GeyserItemStack.EMPTY; + } else if (chestplate.is(session, ItemTag.BANNERS)) { // Prevent chestplate banners from showing erroneously - chestplate = ItemData.AIR; + chestplate = GeyserItemStack.EMPTY; } MobArmorEquipmentPacket armorEquipmentPacket = new MobArmorEquipmentPacket(); armorEquipmentPacket.setRuntimeEntityId(geyserId); - armorEquipmentPacket.setHelmet(helmet); - armorEquipmentPacket.setChestplate(chestplate); - armorEquipmentPacket.setLeggings(leggings); - armorEquipmentPacket.setBoots(boots); + armorEquipmentPacket.setHelmet(ItemTranslator.translateToBedrock(session, helmet)); + armorEquipmentPacket.setChestplate(ItemTranslator.translateToBedrock(session, chestplate)); + armorEquipmentPacket.setLeggings(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.LEGGINGS))); + armorEquipmentPacket.setBoots(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.BOOTS))); + armorEquipmentPacket.setBody(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.BODY))); session.sendUpstreamPacket(armorEquipmentPacket); } - public void updateMainHand(GeyserSession session) { + public void updateMainHand() { if (!valid) return; MobEquipmentPacket handPacket = new MobEquipmentPacket(); handPacket.setRuntimeEntityId(geyserId); - handPacket.setItem(hand); + handPacket.setItem(ItemTranslator.translateToBedrock(session, getMainHandItem())); handPacket.setHotbarSlot(-1); handPacket.setInventorySlot(0); handPacket.setContainerId(ContainerId.INVENTORY); @@ -242,12 +431,12 @@ public class LivingEntity extends Entity { session.sendUpstreamPacket(handPacket); } - public void updateOffHand(GeyserSession session) { + public void updateOffHand() { if (!valid) return; MobEquipmentPacket offHandPacket = new MobEquipmentPacket(); offHandPacket.setRuntimeEntityId(geyserId); - offHandPacket.setItem(offHand); + offHandPacket.setItem(ItemTranslator.translateToBedrock(session, getOffHandItem())); offHandPacket.setHotbarSlot(-1); offHandPacket.setInventorySlot(0); offHandPacket.setContainerId(ContainerId.OFFHAND); @@ -255,6 +444,15 @@ public class LivingEntity extends Entity { session.sendUpstreamPacket(offHandPacket); } + /** + * Called when a SWING_ARM animation packet is received + * + * @return true if an ATTACK_START event should be used instead + */ + public boolean useArmSwingAttack() { + return false; + } + /** * Attributes are properties of an entity that are generally more runtime-based instead of permanent properties. * Movement speed, current attack damage with a weapon, current knockback resistance. @@ -288,22 +486,80 @@ public class LivingEntity extends Entity { protected void updateAttribute(Attribute javaAttribute, List newAttributes) { if (javaAttribute.getType() instanceof AttributeType.Builtin type) { switch (type) { - case GENERIC_MAX_HEALTH -> { + case MAX_HEALTH -> { // Since 1.18.0, setting the max health to 0 or below causes the entity to die on Bedrock but not on Java // See https://github.com/GeyserMC/Geyser/issues/2971 this.maxHealth = Math.max((float) AttributeUtils.calculateValue(javaAttribute), 1f); newAttributes.add(createHealthAttribute()); } - case GENERIC_ATTACK_DAMAGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.ATTACK_DAMAGE)); - case GENERIC_FLYING_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED)); - case GENERIC_MOVEMENT_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.MOVEMENT_SPEED)); - case GENERIC_FOLLOW_RANGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FOLLOW_RANGE)); - case GENERIC_KNOCKBACK_RESISTANCE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.KNOCKBACK_RESISTANCE)); - case HORSE_JUMP_STRENGTH -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.HORSE_JUMP_STRENGTH)); + case MOVEMENT_SPEED -> { + AttributeData attributeData = calculateAttribute(javaAttribute, GeyserAttributeType.MOVEMENT_SPEED); + newAttributes.add(attributeData); + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setMoveSpeed(attributeData.getValue()); + } + } + case STEP_HEIGHT -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setStepHeight((float) AttributeUtils.calculateValue(javaAttribute)); + } + } + case GRAVITY -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setGravity(AttributeUtils.calculateValue(javaAttribute)); + } + } + case ATTACK_DAMAGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.ATTACK_DAMAGE)); + case FLYING_SPEED -> { + AttributeData attributeData = calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED); + newAttributes.add(attributeData); + if (this instanceof HappyGhastEntity ghast && ghast.getVehicleComponent() instanceof HappyGhastVehicleComponent component) { + component.setFlyingSpeed(attributeData.getValue()); + } + } + case FOLLOW_RANGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FOLLOW_RANGE)); + case KNOCKBACK_RESISTANCE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.KNOCKBACK_RESISTANCE)); + case JUMP_STRENGTH -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.HORSE_JUMP_STRENGTH)); + case SCALE -> { + // Attribute on Java, entity data on Bedrock + setAttributeScale((float) AttributeUtils.calculateValue(javaAttribute)); + updateBedrockMetadata(); + } + case WATER_MOVEMENT_EFFICIENCY -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setWaterMovementEfficiency(AttributeUtils.calculateValue(javaAttribute)); + } + } } } } + protected boolean hasBodyArmor() { + return this.hasValidEquippableItemForSlot(EquipmentSlot.BODY); + } + + private boolean hasValidEquippableItemForSlot(EquipmentSlot slot) { + // MojMap LivingEntity#hasItemInSlot + GeyserItemStack itemInSlot = equipment.get(slot); + if (itemInSlot != null) { + // MojMap LivingEntity#isEquippableInSlot + Equippable equippable = itemInSlot.getComponent(DataComponentTypes.EQUIPPABLE); + if (equippable != null) { + return slot == equippable.slot() && + canUseSlot(slot) && + EntityUtils.equipmentUsableByEntity(session, equippable, this.definition.entityType()); + } else { + return slot == EquipmentSlot.MAIN_HAND && canUseSlot(EquipmentSlot.MAIN_HAND); + } + } + + return false; + } + + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } + /** * Calculates the complete attribute value to send to Bedrock. Will be overriden if attributes need to be cached. */ diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java index ecf67052b..1f69d34e7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java @@ -25,26 +25,48 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.MinecartStep; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundMoveMinecartPacket; +import java.util.LinkedList; +import java.util.List; import java.util.UUID; -public class MinecartEntity extends Entity { +public class MinecartEntity extends Entity implements Tickable { + private static final int POS_ROT_LERP_TICKS = 3; + + private final List lerpSteps = new LinkedList<>(); + private final List currentLerpSteps = new LinkedList<>(); + + private MinecartStep lastCompletedStep = new MinecartStep(Vector3d.ZERO, Vector3d. ZERO, 0.0F, 0.0F, 0.0F); + private float currentStepsTotalWeight = 0.0F; + private int lerpDelay = 0; + + private PartialStep cachedPartialStep; + private int cachedStepDelay; + private float cachedDelta; public MinecartEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position.add(0d, definition.offset(), 0d), motion, yaw, pitch, headYaw); } public void setCustomBlock(IntEntityMetadata entityMetadata) { + // Optional block state -> "0" is air, aka none + // Sets whether the custom block should be enabled + dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) (entityMetadata.getPrimitiveValue() != 0 ? 1 : 0)); dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(entityMetadata.getPrimitiveValue())); } @@ -52,10 +74,129 @@ public class MinecartEntity extends Entity { dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, entityMetadata.getPrimitiveValue()); } - public void setShowCustomBlock(BooleanEntityMetadata entityMetadata) { - // If the custom block should be enabled - // Needs a byte based off of Java's boolean - dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0)); + @Override + public void tick() { + if (!session.isUsingExperimentalMinecartLogic()) { + return; + } + + // All minecart lerp code here and in the methods below has been based off of the code in the Java NewMinecartBehavior class + lerpDelay--; + if (lerpDelay <= 0) { + updateCompletedStep(); + currentLerpSteps.clear(); + if (!lerpSteps.isEmpty()) { + currentLerpSteps.addAll(lerpSteps); + lerpSteps.clear(); + currentStepsTotalWeight = 0.0F; + + for (MinecartStep step : currentLerpSteps) { + currentStepsTotalWeight += step.weight(); + } + + lerpDelay = currentStepsTotalWeight == 0.0F ? 0 : POS_ROT_LERP_TICKS; + } + } + + if (isLerping()) { + float delta = 1.0F; // This is always 1, maybe it should be removed + + Vector3f position = getCurrentLerpPosition(delta).toFloat(); + Vector3f movement = getCurrentLerpMovement(delta).toFloat(); + setPosition(position); + setMotion(movement); + + setYaw(180.0F - getCurrentLerpYaw(delta)); + setPitch(getCurrentLerpPitch(delta)); + + MoveEntityDeltaPacket moveEntityPacket = new MoveEntityDeltaPacket(); + moveEntityPacket.setRuntimeEntityId(geyserId); + + moveEntityPacket.setX(position.getX()); + moveEntityPacket.setY(position.getY() + definition.offset()); + moveEntityPacket.setZ(position.getZ()); + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X); + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); + + moveEntityPacket.setYaw(getYaw()); + moveEntityPacket.setPitch(getPitch()); + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); + + SetEntityMotionPacket entityMotionPacket = new SetEntityMotionPacket(); + entityMotionPacket.setRuntimeEntityId(geyserId); + entityMotionPacket.setMotion(movement); + + session.sendUpstreamPacket(moveEntityPacket); + session.sendUpstreamPacket(entityMotionPacket); + } + } + + public void handleMinecartMovePacket(ClientboundMoveMinecartPacket packet) { + lerpSteps.addAll(packet.getLerpSteps()); + } + + private boolean isLerping() { + return !currentLerpSteps.isEmpty(); + } + + private float getCurrentLerpPitch(float delta) { + PartialStep partialStep = getCurrentLerpStep(delta); + return lerpRotation(partialStep.delta, partialStep.previousStep.xRot(), partialStep.currentStep.xRot()); + } + + private float getCurrentLerpYaw(float delta) { + PartialStep partialStep = getCurrentLerpStep(delta); + return lerpRotation(partialStep.delta, partialStep.previousStep.yRot(), partialStep.currentStep.yRot()); + } + + private Vector3d getCurrentLerpPosition(float delta) { + PartialStep partialStep = getCurrentLerpStep(delta); + return lerp(partialStep.delta, partialStep.previousStep.position(), partialStep.currentStep.position()); + } + + private Vector3d getCurrentLerpMovement(float delta) { + PartialStep partialStep = getCurrentLerpStep(delta); + return lerp(partialStep.delta, partialStep.previousStep.movement(), partialStep.currentStep.movement()); + } + + private PartialStep getCurrentLerpStep(float delta) { + if (cachedDelta != delta || lerpDelay != cachedStepDelay || cachedPartialStep == null) { + float g = ((POS_ROT_LERP_TICKS - lerpDelay) + delta) / POS_ROT_LERP_TICKS; + float totalWeight = 0.0F; + float stepDelta = 1.0F; + boolean foundStep = false; + + int step; + for (step = 0; step < currentLerpSteps.size(); step++) { + float currentWeight = currentLerpSteps.get(step).weight(); + if (!(currentWeight <= 0.0F)) { + totalWeight += currentWeight; + if ((double) totalWeight >= currentStepsTotalWeight * (double) g) { + float h = totalWeight - currentWeight; + stepDelta = (g * currentStepsTotalWeight - h) / currentWeight; + foundStep = true; + break; + } + } + } + + if (!foundStep) { + step = currentLerpSteps.size() - 1; + } + + MinecartStep currentStep = currentLerpSteps.get(step); + MinecartStep previousStep = step > 0 ? currentLerpSteps.get(step - 1) : lastCompletedStep; + cachedPartialStep = new PartialStep(stepDelta, currentStep, previousStep); + cachedStepDelay = lerpDelay; + cachedDelta = delta; + } + return cachedPartialStep; + } + + private void updateCompletedStep() { + lastCompletedStep = new MinecartStep(position.toDouble(), motion.toDouble(), yaw, pitch, 0.0F); } @Override @@ -103,4 +244,19 @@ public class MinecartEntity extends Entity { } } } + + private static Vector3d lerp(double delta, Vector3d start, Vector3d end) { + return Vector3d.from(lerp(delta, start.getX(), end.getX()), lerp(delta, start.getY(), end.getY()), lerp(delta, start.getZ(), end.getZ())); + } + + public static double lerp(double delta, double start, double end) { + return start + delta * (end - start); + } + + private static float lerpRotation(float delta, float start, float end) { + return start + delta * MathUtils.wrapDegrees(end - start); + } + + private record PartialStep(float delta, MinecartStep currentStep, MinecartStep previousStep) { + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java index 4e5fe9d59..ea3600768 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java @@ -25,23 +25,26 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.AddPaintingPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.level.PaintingType; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.PaintingVariant; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; import java.util.UUID; -public class PaintingEntity extends Entity { +public class PaintingEntity extends HangingEntity { private static final double OFFSET = -0.46875; - private final Direction direction; + private int paintingId = -1; // Ideally this would be the default painting Java uses in their metadata, but seems to depend on the current paintings loaded in the registry + private Direction direction = Direction.SOUTH; // Default to SOUTH direction, like on Java - entity metadata should correct this when necessary - public PaintingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, Direction direction) { + public PaintingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); - this.direction = direction; } @Override @@ -49,8 +52,32 @@ public class PaintingEntity extends Entity { // Wait until we get the metadata needed } - public void setPaintingType(ObjectEntityMetadata entityMetadata) { - PaintingType type = PaintingType.getByPaintingType(entityMetadata.getValue()); + @Override + public void setDirection(Direction direction) { + this.direction = direction; + updatePainting(); + } + + public void setPaintingType(ObjectEntityMetadata> entityMetadata) { + if (!entityMetadata.getValue().isId()) { + return; + } + paintingId = entityMetadata.getValue().id(); + updatePainting(); + } + + private void updatePainting() { + if (paintingId == -1) { + return; + } else if (valid) { + despawnEntity(); + } + + PaintingType type = session.getRegistryCache().registry(JavaRegistries.PAINTING_VARIANT).byId(paintingId); + if (type == null) { + return; + } + AddPaintingPacket addPaintingPacket = new AddPaintingPacket(); addPaintingPacket.setUniqueEntityId(geyserId); addPaintingPacket.setRuntimeEntityId(geyserId); @@ -78,8 +105,12 @@ public class PaintingEntity extends Entity { private Vector3f fixOffset(PaintingType paintingName) { Vector3f position = super.position; - position = position.add(0.5, 0.5, 0.5); - double widthOffset = paintingName.getWidth() > 1 ? 0.5 : 0; + // ViaVersion already adds the offset for us on older versions, + // so no need to do it then otherwise it will be spaced + if (session.isEmulatePost1_18Logic()) { + position = position.add(0.5, 0.5, 0.5); + } + double widthOffset = paintingName.getWidth() > 1 && paintingName.getWidth() != 3 ? 0.5 : 0; double heightOffset = paintingName.getHeight() > 1 && paintingName.getHeight() != 3 ? 0.5 : 0; return switch (direction) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/SpawnerMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/SpawnerMinecartEntity.java index 49cfc0081..4d69c8a1e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/SpawnerMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/SpawnerMinecartEntity.java @@ -28,7 +28,7 @@ package org.geysermc.geyser.entity.type; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.session.GeyserSession; import java.util.UUID; @@ -41,7 +41,7 @@ public class SpawnerMinecartEntity extends DefaultBlockMinecartEntity { @Override public void updateDefaultBlockMetadata() { - dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(BlockStateValues.JAVA_SPAWNER_ID)); + dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(Blocks.SPAWNER.defaultBlockState())); dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, 6); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java index 98c2edd00..47d97b8f7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java @@ -25,13 +25,13 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -39,7 +39,17 @@ public class TNTEntity extends Entity implements Tickable { private int currentTick; public TNTEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { - super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + super(session, entityId, geyserId, uuid, definition, position.add(0, definition.offset(), 0), motion, yaw, pitch, headYaw); + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, boolean isOnGround) { + super.moveRelative(relX, relY + definition.offset(), relZ, yaw, pitch, isOnGround); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + super.moveAbsolute(position.add(Vector3f.from(0, definition.offset(), 0)), yaw, pitch, headYaw, isOnGround, teleported); } public void setFuseLength(IntEntityMetadata entityMetadata) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java index 0b6160401..80e5f3d18 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * Copyright (c) 2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,45 +25,59 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import lombok.Getter; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.jetbrains.annotations.Nullable; -import java.util.Optional; import java.util.UUID; // Note: 1.19.4 requires that the billboard is set to something in order to show, on Java Edition -public class TextDisplayEntity extends Entity { +@Getter +public class TextDisplayEntity extends DisplayBaseEntity { + + private int lineCount; + public TextDisplayEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { - super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + super(session, entityId, geyserId, uuid, definition, position.add(0, definition.offset(), 0), motion, yaw, pitch, headYaw); + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, boolean isOnGround) { + super.moveRelative(relX, relY + definition.offset(), relZ, yaw, pitch, isOnGround); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + super.moveAbsolute(position.add(Vector3f.from(0, definition.offset(), 0)), yaw, pitch, headYaw, isOnGround, teleported); } @Override protected void initializeMetadata() { super.initializeMetadata(); - // Remove armor stand body + // Remove armor stand body / hitbox + this.dirtyMetadata.put(EntityDataTypes.HITBOX, NbtMap.EMPTY); this.dirtyMetadata.put(EntityDataTypes.SCALE, 0f); this.dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); } - @Override - public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { - // Don't allow the display name to be hidden - messes with our armor stand. - // On JE: Hiding the display name still shows the display entity text. - } - - @Override - public void setDisplayName(EntityMetadata, ?> entityMetadata) { - // This would usually set EntityDataTypes.NAME, but we are instead using NAME for the text display. - // On JE: custom name does not override text display. - } - public void setText(EntityMetadata entityMetadata) { - this.dirtyMetadata.put(EntityDataTypes.NAME, MessageTranslator.convertMessage(entityMetadata.getValue())); + this.dirtyMetadata.put(EntityDataTypes.NAME, MessageTranslator.convertMessage(entityMetadata.getValue(), session.locale())); + calculateLineCount(entityMetadata.getValue()); + } + + private void calculateLineCount(@Nullable Component text) { + if (text == null) { + lineCount = 0; + return; + } + lineCount = PlainTextComponentSerializer.plainText().serialize(text).split("\n").length; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java new file mode 100644 index 000000000..a32762abe --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type; + +import lombok.Getter; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; + +import java.util.UUID; + +@Getter +public class ThrowableEggEntity extends ThrowableItemEntity { + + // Used for egg break particles + private GeyserItemStack itemStack = GeyserItemStack.of(Items.EGG.javaId(), 1); + + public ThrowableEggEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public void setItem(EntityMetadata entityMetadata) { + GeyserItemStack stack = GeyserItemStack.from(entityMetadata.getValue()); + TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, getVariantOrFallback(session, stack)); + updateBedrockEntityProperties(); + this.itemStack = stack; + } + + private static TemperatureVariantAnimal.BuiltInVariant getVariantOrFallback(GeyserSession session, GeyserItemStack stack) { + Holder holder = stack.getComponent(DataComponentTypes.CHICKEN_VARIANT); + if (holder != null) { + Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id)); + for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) { + if (chickenVariant.asMinimalString().equalsIgnoreCase(variant.name())) { + return variant; + } + } + } + + return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java index 60840e65b..85abc1c40 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -34,6 +33,7 @@ import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import java.util.UUID; @@ -55,6 +55,9 @@ public class ThrowableEntity extends Entity implements Tickable { */ @Override public void tick() { + if (removedInVoid()) { + return; + } moveAbsoluteImmediate(position.add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false); float drag = getDrag(); float gravity = getGravity(); @@ -117,7 +120,7 @@ public class ThrowableEntity extends Entity implements Tickable { protected float getGravity() { if (getFlag(EntityFlag.HAS_GRAVITY)) { switch (definition.entityType()) { - case POTION: + case LINGERING_POTION, SPLASH_POTION: return 0.05f; case EXPERIENCE_BOTTLE: return 0.07f; @@ -143,7 +146,7 @@ public class ThrowableEntity extends Entity implements Tickable { return 0.8f; } else { switch (definition.entityType()) { - case POTION: + case LINGERING_POTION, SPLASH_POTION: case EXPERIENCE_BOTTLE: case SNOWBALL: case EGG: @@ -170,14 +173,14 @@ public class ThrowableEntity extends Entity implements Tickable { } @Override - public boolean despawnEntity() { + public void despawnEntity() { if (definition.entityType() == EntityType.ENDER_PEARL) { LevelEventPacket particlePacket = new LevelEventPacket(); particlePacket.setType(LevelEvent.PARTICLE_TELEPORT); particlePacket.setPosition(position); session.sendUpstreamPacket(particlePacket); } - return super.despawnEntity(); + super.despawnEntity(); } @Override @@ -191,4 +194,17 @@ public class ThrowableEntity extends Entity implements Tickable { moveAbsoluteImmediate(position, yaw, pitch, headYaw, isOnGround, teleported); lastJavaPosition = position; } + + /** + * Removes the entity if it is 64 blocks below the world. + * + * @return true if the entity was removed + */ + public boolean removedInVoid() { + if (position.getY() < session.getDimensionType().minY() - 64) { + session.getEntityCache().removeEntity(this); + return true; + } + return false; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java index 39c8386bd..fbbe2de50 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java @@ -25,12 +25,14 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.UUID; @@ -39,7 +41,7 @@ import java.util.UUID; */ public class ThrowableItemEntity extends ThrowableEntity { /** - * Number of ticks since the entity was spawned by the Java server + * Number of draw ticks since the entity was spawned by the Java server */ private int age; private boolean invisible; @@ -48,29 +50,38 @@ public class ThrowableItemEntity extends ThrowableEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); setFlag(EntityFlag.INVISIBLE, true); invisible = false; - } - - private void checkVisibility() { - if (invisible != getFlag(EntityFlag.INVISIBLE)) { - if (!invisible) { - Vector3f playerPos = session.getPlayerEntity().getPosition(); - // Prevent projectiles from blocking the player's screen - if (age >= 4 || position.distanceSquared(playerPos) > 16) { - setFlag(EntityFlag.INVISIBLE, false); - updateBedrockMetadata(); - } - } else { - setFlag(EntityFlag.INVISIBLE, true); - updateBedrockMetadata(); - } - } - age++; + age = 0; } @Override - public void tick() { + protected void initializeMetadata() { + super.initializeMetadata(); + // Correct sizing + dirtyMetadata.put(EntityDataTypes.SCALE, 0.5f); + } + + private void checkVisibility() { + age++; + + // Prevent projectiles from blocking the player's screen + if (session.isTickingFrozen()) { + // This may seem odd, but it matches java edition + Vector3f playerPos = session.getPlayerEntity().getPosition().down(EntityDefinitions.PLAYER.offset()); + setInvisible(playerPos.distanceSquared(position.add(0, definition.offset(), 0)) < 12.25); + } else { + setInvisible(age < 2); + } + + if (invisible != getFlag(EntityFlag.INVISIBLE)) { + setFlag(EntityFlag.INVISIBLE, invisible); + updateBedrockMetadata(); + } + } + + @Override + public void drawTick() { checkVisibility(); - super.tick(); + super.drawTick(); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java index 1b5c1d2d0..84c2cb731 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java @@ -25,19 +25,19 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.opennbt.tag.builtin.StringTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.item.Potion; -import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; import java.util.EnumSet; import java.util.UUID; @@ -53,25 +53,26 @@ public class ThrownPotionEntity extends ThrowableItemEntity { public void setItem(EntityMetadata entityMetadata) { ItemStack itemStack = entityMetadata.getValue(); if (itemStack == null) { - dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, 0); + dirtyMetadata.put(EntityDataTypes.AUX_VALUE_DATA, (short) 0); setFlag(EntityFlag.ENCHANTED, false); setFlag(EntityFlag.LINGERING, false); } else { // As of Java 1.19.3, the server/client doesn't seem to care of the item is actually a potion? - if (itemStack.getNbt() != null) { - Tag potionTag = itemStack.getNbt().get("Potion"); - if (potionTag instanceof StringTag) { - Potion potion = Potion.getByJavaIdentifier(((StringTag) potionTag).getValue()); + DataComponents components = itemStack.getDataComponentsPatch(); + if (components != null) { + PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); + if (potionContents != null) { + Potion potion = Potion.getByJavaId(potionContents.getPotionId()); if (potion != null) { - dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, (int) potion.getBedrockId()); + dirtyMetadata.put(EntityDataTypes.AUX_VALUE_DATA, potion.getBedrockId()); setFlag(EntityFlag.ENCHANTED, !NON_ENCHANTED_POTIONS.contains(potion)); } else { - dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, 0); - GeyserImpl.getInstance().getLogger().debug("Unknown java potion: " + potionTag.getValue()); + dirtyMetadata.put(EntityDataTypes.AUX_VALUE_DATA, (short) 0); + GeyserImpl.getInstance().getLogger().debug("Unknown java potion: " + potionContents.getPotionId()); } } - boolean isLingering = Registries.JAVA_ITEMS.get().get(itemStack.getId()) == Items.LINGERING_POTION; + boolean isLingering = definition.entityType() == EntityType.LINGERING_POTION; setFlag(EntityFlag.LINGERING, isLingering); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java b/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java index 06bf45b3d..f61ff355f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,8 +26,21 @@ package org.geysermc.geyser.entity.type; /** - * Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds. + * Implemented onto anything that should have code ran every Minecraft tick. + * By default, the Java server runs at 20 TPS, 50 milliseconds for each tick. */ public interface Tickable { + /** + * This function gets called every tick at all times, even when the server requests that + * the game should be frozen. This should be used for updating things that are always + * client side updated on Java, regardless of if the server is frozen or not. + */ + default void drawTick() { + } + + /** + * This function gets called every game tick as long as the + * game tick loop isn't frozen. + */ void tick(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/WitherSkullEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/WitherSkullEntity.java index 637ca4139..8427a8e10 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/WitherSkullEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/WitherSkullEntity.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.entity.type; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java index 9cc3a006e..6ecfa4978 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -34,6 +33,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableEntity.java index 6e2e7a407..8f84e051b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableEntity.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; @@ -44,12 +43,12 @@ public class AgeableEntity extends CreatureEntity { protected void initializeMetadata() { super.initializeMetadata(); // Required as of 1.19.3 Java - dirtyMetadata.put(EntityDataTypes.SCALE, getAdultSize()); + setScale(getAdultSize()); } public void setBaby(BooleanEntityMetadata entityMetadata) { boolean isBaby = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.SCALE, isBaby ? getBabySize() : getAdultSize()); + setScale(isBaby ? getBabySize() : getAdultSize()); setFlag(EntityFlag.BABY, isBaby); setBoundingBoxHeight(definition.height() * (isBaby ? getBabySize() : getAdultSize())); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableWaterEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableWaterEntity.java new file mode 100644 index 000000000..843215674 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AgeableWaterEntity.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living; + +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.UUID; + +public abstract class AgeableWaterEntity extends AgeableEntity { + public AgeableWaterEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public boolean canBeLeashed() { + return false; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java index 48d633e5d..e87e20236 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java @@ -25,17 +25,17 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -60,9 +60,9 @@ public class AllayEntity extends MobEntity { if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) { // Maybe better as another tag? return InteractiveTag.GIVE_ITEM_TO_ALLAY; - } else if (!this.hand.isValid() && !itemInHand.isEmpty()) { + } else if (getMainHandItem().isEmpty() && !itemInHand.isEmpty()) { return InteractiveTag.GIVE_ITEM_TO_ALLAY; - } else if (this.hand.isValid() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) { + } else if (!getMainHandItem().isEmpty() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) { // Seems like there isn't a good tag for this yet return InteractiveTag.GIVE_ITEM_TO_ALLAY; } else { @@ -76,10 +76,10 @@ public class AllayEntity extends MobEntity { if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) { //TOCHECK sound return InteractionResult.SUCCESS; - } else if (!this.hand.isValid() && !itemInHand.isEmpty()) { + } else if (getMainHandItem().isEmpty() && !itemInHand.isEmpty()) { //TODO play sound? return InteractionResult.SUCCESS; - } else if (this.hand.isValid() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) { + } else if (!getMainHandItem().isEmpty() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) { //TOCHECK also play sound here? return InteractionResult.SUCCESS; } else { @@ -88,6 +88,6 @@ public class AllayEntity extends MobEntity { } private boolean isDuplicationItem(GeyserItemStack itemStack) { - return itemStack.asItem() == Items.AMETHYST_SHARD; + return itemStack.is(session, ItemTag.DUPLICATES_ALLAYS); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java index 8f81125d0..f4b80edf1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java @@ -38,7 +38,7 @@ public class AmbientEntity extends MobEntity { } @Override - protected boolean canBeLeashed() { + public boolean canBeLeashed() { return false; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java index c64776f18..956d641bb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java @@ -25,24 +25,27 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import lombok.Getter; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.Optional; import java.util.UUID; @@ -99,11 +102,11 @@ public class ArmorStandEntity extends LivingEntity { } @Override - public boolean despawnEntity() { + public void despawnEntity() { if (secondEntity != null) { secondEntity.despawnEntity(); } - return super.despawnEntity(); + super.despawnEntity(); } @Override @@ -122,6 +125,12 @@ public class ArmorStandEntity extends LivingEntity { this.position = position; } + @Override + public void updateNametag(@Nullable Team team) { + // unlike all other LivingEntities, armor stands are not affected by team nametag visibility + super.updateNametag(team, true); + } + @Override public void setDisplayName(EntityMetadata, ?> entityMetadata) { super.setDisplayName(entityMetadata); @@ -248,7 +257,7 @@ public class ArmorStandEntity extends LivingEntity { @Override public InteractionResult interactAt(Hand hand) { - if (!isMarker && session.getPlayerInventory().getItemInHand(hand).asItem() != Items.NAME_TAG) { + if (!isMarker && !session.getPlayerInventory().getItemInHand(hand).is(Items.NAME_TAG)) { // Java Edition returns SUCCESS if in spectator mode, but this is overridden with an earlier check on the client return InteractionResult.CONSUME; } else { @@ -257,38 +266,38 @@ public class ArmorStandEntity extends LivingEntity { } @Override - public void setHelmet(ItemData helmet) { + public void setHelmet(GeyserItemStack helmet) { super.setHelmet(helmet); updateSecondEntityStatus(true); } @Override - public void setChestplate(ItemData chestplate) { + public void setChestplate(GeyserItemStack chestplate) { super.setChestplate(chestplate); updateSecondEntityStatus(true); } @Override - public void setLeggings(ItemData leggings) { + public void setLeggings(GeyserItemStack leggings) { super.setLeggings(leggings); updateSecondEntityStatus(true); } @Override - public void setBoots(ItemData boots) { + public void setBoots(GeyserItemStack boots) { super.setBoots(boots); updateSecondEntityStatus(true); } @Override - public void setHand(ItemData hand) { + public void setHand(GeyserItemStack hand) { super.setHand(hand); updateSecondEntityStatus(true); } @Override - public void setOffHand(ItemData offHand) { - super.setOffHand(offHand); + public void setOffhand(GeyserItemStack offHand) { + super.setOffhand(offHand); updateSecondEntityStatus(true); } @@ -310,7 +319,7 @@ public class ArmorStandEntity extends LivingEntity { if (!isInvisible) { // The armor stand isn't invisible. We good. setFlag(EntityFlag.INVISIBLE, false); - dirtyMetadata.put(EntityDataTypes.SCALE, getScale()); + setScale(getScale()); updateOffsetRequirement(false); if (secondEntity != null) { @@ -323,10 +332,9 @@ public class ArmorStandEntity extends LivingEntity { return; } boolean isNametagEmpty = nametag.isEmpty(); - if (!isNametagEmpty && (!helmet.equals(ItemData.AIR) || !chestplate.equals(ItemData.AIR) || !leggings.equals(ItemData.AIR) - || !boots.equals(ItemData.AIR) || !hand.equals(ItemData.AIR) || !offHand.equals(ItemData.AIR))) { + if (!isNametagEmpty && hasAnyEquipment()) { // Reset scale of the proper armor stand - this.dirtyMetadata.put(EntityDataTypes.SCALE, getScale()); + setScale(getScale()); // Set the proper armor stand to invisible to show armor setFlag(EntityFlag.INVISIBLE, true); // Update the position of the armor stand @@ -349,7 +357,7 @@ public class ArmorStandEntity extends LivingEntity { // Guarantee this copy is NOT invisible secondEntity.setFlag(EntityFlag.INVISIBLE, false); // Scale to 0 to show nametag - secondEntity.getDirtyMetadata().put(EntityDataTypes.SCALE, 0.0f); + secondEntity.setScale(0f); // No bounding box as we don't want to interact with this entity secondEntity.getDirtyMetadata().put(EntityDataTypes.WIDTH, 0.0f); secondEntity.getDirtyMetadata().put(EntityDataTypes.HEIGHT, 0.0f); @@ -359,7 +367,7 @@ public class ArmorStandEntity extends LivingEntity { } else if (isNametagEmpty) { // We can just make an invisible entity // Reset scale of the proper armor stand - dirtyMetadata.put(EntityDataTypes.SCALE, getScale()); + setScale(getScale()); // Set the proper armor stand to invisible to show armor setFlag(EntityFlag.INVISIBLE, true); // Update offset @@ -373,7 +381,7 @@ public class ArmorStandEntity extends LivingEntity { // Nametag is not empty and there is no armor // We don't need to make a new entity setFlag(EntityFlag.INVISIBLE, false); - dirtyMetadata.put(EntityDataTypes.SCALE, 0.0f); + setScale(0f); // As the above is applied, we need an offset updateOffsetRequirement(!isMarker); @@ -387,6 +395,12 @@ public class ArmorStandEntity extends LivingEntity { } } + private boolean hasAnyEquipment() { + return (!getItemInSlot(EquipmentSlot.HELMET).isEmpty() || !getItemInSlot(EquipmentSlot.CHESTPLATE).isEmpty() + || !getItemInSlot(EquipmentSlot.LEGGINGS).isEmpty() || !getItemInSlot(EquipmentSlot.BOOTS).isEmpty() + || !getMainHandItem().isEmpty() || !getOffHandItem().isEmpty()); + } + @Override public float getBoundingBoxWidth() { // For consistency with getBoundingBoxHeight() diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/BatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/BatEntity.java index 644054e72..bdfc20c88 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/BatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/BatEntity.java @@ -25,11 +25,11 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java new file mode 100644 index 000000000..946d96169 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.CopperGolemState; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.WeatheringCopperState; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; + +import java.util.UUID; + +public class CopperGolemEntity extends GolemEntity { + public static final BooleanProperty HAS_FLOWER_PROPERTY = new BooleanProperty( + IdentifierImpl.of("has_flower"), + false + ); + + public static final EnumProperty CHEST_INTERACTION_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("chest_interaction"), + ChestInteractionState.class, + ChestInteractionState.NONE + ); + + public static final EnumProperty OXIDATION_LEVEL_STATE_ENUM_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("oxidation_level"), + OxidationLevelState.class, + OxidationLevelState.UNOXIDIZED + ); + + public enum ChestInteractionState { + NONE, + TAKE, + TAKE_FAIL, + PUT, + PUT_FAIL + } + + public enum OxidationLevelState { + UNOXIDIZED, + EXPOSED, + WEATHERED, + OXIDIZED + } + + public CopperGolemEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected @NonNull InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (itemInHand.isEmpty() && !getMainHandItem().isEmpty()) { + return InteractiveTag.DROP_ITEM; + } else if (itemInHand.is(Items.SHEARS) && canBeSheared()) { + return InteractiveTag.SHEAR; + } else if (itemInHand.is(Items.HONEYCOMB)) { + return InteractiveTag.WAX_ON; + } else if (itemInHand.is(session, ItemTag.AXES)) { + // There is no way of knowing if the copper golem is waxed or not, + // so just always send a scrape tag :( + return InteractiveTag.SCRAPE; + } + + return super.testMobInteraction(hand, itemInHand); + } + + @Override + protected @NonNull InteractionResult mobInteract(@NonNull Hand usedHand, @NonNull GeyserItemStack itemInHand) { + if ((itemInHand.isEmpty() && !getMainHandItem().isEmpty()) || (itemInHand.is(Items.SHEARS) && canBeSheared())) { + return InteractionResult.SUCCESS; + } + + return InteractionResult.PASS; + } + + private boolean canBeSheared() { + return isAlive() && getItemInSlot(EquipmentSlot.HELMET).is(session, ItemTag.SHEARABLE_FROM_COPPER_GOLEM); + } + + @Override + public void setSaddle(GeyserItemStack stack) { + super.setSaddle(stack); + + // Equipment on Java, entity property on bedrock + HAS_FLOWER_PROPERTY.apply(propertyManager, stack.is(Items.POPPY)); + updateBedrockEntityProperties(); + } + + public void setWeatheringState(EntityMetadata> metadata) { + WeatheringCopperState state = metadata.getValue(); + OXIDATION_LEVEL_STATE_ENUM_PROPERTY.apply(propertyManager, switch (state) { + case UNAFFECTED -> OxidationLevelState.UNOXIDIZED; + case EXPOSED -> OxidationLevelState.EXPOSED; + case WEATHERED -> OxidationLevelState.WEATHERED; + case OXIDIZED -> OxidationLevelState.OXIDIZED; + }); + updateBedrockEntityProperties(); + } + + public void setGolemState(EntityMetadata> metadata) { + CopperGolemState state = metadata.getValue(); + CHEST_INTERACTION_PROPERTY.apply(propertyManager, switch (state) { + case IDLE -> ChestInteractionState.NONE; + case GETTING_ITEM -> ChestInteractionState.TAKE; + case GETTING_NO_ITEM -> ChestInteractionState.TAKE_FAIL; + case DROPPING_ITEM -> ChestInteractionState.PUT; + case DROPPING_NO_ITEM -> ChestInteractionState.PUT_FAIL; + }); + updateBedrockEntityProperties(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java index f0348bcaf..cb300a7c9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java @@ -25,31 +25,32 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class DolphinEntity extends WaterEntity { +public class DolphinEntity extends AgeableWaterEntity { public DolphinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @Override - protected boolean canBeLeashed() { + public boolean canBeLeashed() { return true; } @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (!itemInHand.isEmpty() && session.getTagCache().isFish(itemInHand)) { + if (!itemInHand.isEmpty() && itemInHand.is(session, ItemTag.FISHES)) { return InteractiveTag.FEED; } return super.testMobInteraction(hand, itemInHand); @@ -58,7 +59,7 @@ public class DolphinEntity extends WaterEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (!itemInHand.isEmpty() && session.getTagCache().isFish(itemInHand)) { + if (!itemInHand.isEmpty() && itemInHand.is(session, ItemTag.FISHES)) { // Feed return InteractionResult.SUCCESS; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java index 7afa4b436..8cbed6373 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -35,6 +34,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -54,7 +54,7 @@ public class IronGolemEntity extends GolemEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() == Items.IRON_INGOT) { + if (itemInHand.is(Items.IRON_INGOT)) { if (health < maxHealth) { // Healing the iron golem return InteractionResult.SUCCESS; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java index 0ac81c957..d6c00f694 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java @@ -25,29 +25,34 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; -import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.Leashable; import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.enchantment.EnchantmentComponent; import org.geysermc.geyser.item.type.SpawnEggItem; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import java.util.UUID; -public class MobEntity extends LivingEntity { +public class MobEntity extends LivingEntity implements Leashable { /** * If another mob is holding this mob by a leash, this variable tracks their Bedrock entity ID. */ - @Getter private long leashHolderBedrockId; public MobEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -65,6 +70,7 @@ public class MobEntity extends LivingEntity { setFlag(EntityFlag.NO_AI, (xd & 0x01) == 0x01); } + @Override public void setLeashHolderBedrockId(long bedrockId) { this.leashHolderBedrockId = bedrockId; dirtyMetadata.put(EntityDataTypes.LEASH_HOLDER, bedrockId); @@ -79,10 +85,7 @@ public class MobEntity extends LivingEntity { return InteractiveTag.REMOVE_LEASH; } else { GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand); - if (itemStack.asItem() == Items.LEAD && canBeLeashed()) { - // We shall leash - return InteractiveTag.LEASH; - } else if (itemStack.asItem() == Items.NAME_TAG) { + if (itemStack.is(Items.NAME_TAG)) { InteractionResult result = checkInteractWithNameTag(itemStack); if (result.consumesAction()) { return InteractiveTag.NAME; @@ -99,9 +102,6 @@ public class MobEntity extends LivingEntity { if (!isAlive()) { // dead lol return InteractionResult.PASS; - } else if (leashHolderBedrockId == session.getPlayerEntity().getGeyserId()) { - // TODO looks like the client assumes it will go through and removes the attachment itself? - return InteractionResult.SUCCESS; } else { GeyserItemStack itemInHand = session.getPlayerInventory().getItemInHand(hand); InteractionResult result = checkPriorityInteractions(itemInHand); @@ -114,11 +114,30 @@ public class MobEntity extends LivingEntity { } } + public boolean canShearEquipment() { + if (!passengers.isEmpty()) { + return false; + } + + for (EquipmentSlot slot : EquipmentSlot.values()) { + GeyserItemStack equipped = getItemInSlot(slot); + if (equipped.isEmpty()) { + continue; + } + + Equippable equippable = equipped.getComponent(DataComponentTypes.EQUIPPABLE); + if (equippable != null && equippable.canBeSheared()) { + if (!ItemUtils.hasEffect(session, equipped, EnchantmentComponent.PREVENT_ARMOR_CHANGE) || session.getGameMode() == GameMode.CREATIVE) { + return true; + } + } + } + + return false; + } + private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) { - if (itemInHand.asItem() == Items.LEAD && canBeLeashed()) { - // We shall leash - return InteractionResult.SUCCESS; - } else if (itemInHand.asItem() == Items.NAME_TAG) { + if (itemInHand.is(Items.NAME_TAG)) { InteractionResult result = checkInteractWithNameTag(itemInHand); if (result.consumesAction()) { return result; @@ -143,12 +162,14 @@ public class MobEntity extends LivingEntity { return InteractionResult.PASS; } - protected boolean canBeLeashed() { - return isNotLeashed() && !isEnemy(); + @Override + public boolean canBeLeashed() { + return !isEnemy(); } - protected final boolean isNotLeashed() { - return leashHolderBedrockId == -1L; + @Override + public long leashHolderBedrockId() { + return leashHolderBedrockId; } /** diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java index 1d2eb95bc..3be2db1db 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java @@ -25,11 +25,10 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -39,8 +38,8 @@ public class SlimeEntity extends MobEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - public void setScale(IntEntityMetadata entityMetadata) { - dirtyMetadata.put(EntityDataTypes.SCALE, 0.10f + entityMetadata.getPrimitiveValue()); + public void setSlimeScale(IntEntityMetadata entityMetadata) { + setScale(0.10f + entityMetadata.getPrimitiveValue()); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java index 7f0699415..f766cd402 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -36,6 +34,8 @@ import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -54,7 +54,7 @@ public class SnowGolemEntity extends GolemEntity { @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (Items.SHEARS == itemInHand.asItem() && isAlive() && !getFlag(EntityFlag.SHEARED)) { + if (itemInHand.is(Items.SHEARS) && isAlive() && !getFlag(EntityFlag.SHEARED)) { // Shearing the snow golem return InteractiveTag.SHEAR; } @@ -64,7 +64,7 @@ public class SnowGolemEntity extends GolemEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (Items.SHEARS == itemInHand.asItem() && isAlive() && !getFlag(EntityFlag.SHEARED)) { + if (itemInHand.is(Items.SHEARS) && isAlive() && !getFlag(EntityFlag.SHEARED)) { // Shearing the snow golem return InteractionResult.SUCCESS; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java index 80a5af442..fe3e26a6c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java @@ -36,7 +36,7 @@ import org.geysermc.geyser.session.GeyserSession; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class SquidEntity extends WaterEntity implements Tickable { +public class SquidEntity extends AgeableWaterEntity implements Tickable { private float targetPitch; private float targetYaw; @@ -122,8 +122,8 @@ public class SquidEntity extends WaterEntity implements Tickable { } @Override - protected boolean canBeLeashed() { - return isNotLeashed(); + public boolean canBeLeashed() { + return true; } private void checkInWater() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java index 7094c431e..42ef51f67 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.entity.type.living; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -62,6 +62,6 @@ public class TadpoleEntity extends AbstractFishEntity { } private boolean isFood(GeyserItemStack itemStack) { - return itemStack.asItem() == Items.SLIME_BALL; + return itemStack.is(session, ItemTag.FROG_FOOD); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java index a847c4cd7..ae9d0d659 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java @@ -38,7 +38,7 @@ public class WaterEntity extends CreatureEntity { } @Override - protected boolean canBeLeashed() { + public boolean canBeLeashed() { return false; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java index 7278709ce..d0c097ec8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java @@ -25,39 +25,41 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.AgeableEntity; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class AnimalEntity extends AgeableEntity { +public abstract class AnimalEntity extends AgeableEntity { public AnimalEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - public final boolean canEat(GeyserItemStack itemStack) { - return canEat(itemStack.asItem()); + protected final boolean canEat(GeyserItemStack itemStack) { + Tag tag = getFoodTag(); + if (tag == null) { + return false; + } + return itemStack.is(session, tag); } /** - * @return true if this is a valid item to breed with for this animal. + * @return the tag associated with this animal for eating food. Null for nothing or different behavior. */ - public boolean canEat(Item item) { - // This is what it defaults to. OK. - return item == Items.WHEAT; - } + protected abstract @Nullable Tag getFoodTag(); @NonNull @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java new file mode 100644 index 000000000..0ebd92021 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.ArmadilloState; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class ArmadilloEntity extends AnimalEntity { + + public static final EnumProperty STATE_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("armadillo_state"), + State.class, + State.UNROLLED + ); + + private ArmadilloState armadilloState = ArmadilloState.IDLE; + + public ArmadilloEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, + EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + public void setArmadilloState(ObjectEntityMetadata entityMetadata) { + armadilloState = entityMetadata.getValue(); + + switch (armadilloState) { + case IDLE -> STATE_PROPERTY.apply(propertyManager, State.UNROLLED); + case ROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP); + case SCARED -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING); + case UNROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_UNROLLING); + } + + updateBedrockEntityProperties(); + } + + public void onPeeking() { + // Technically we should wait if not currently scared + if (armadilloState == ArmadilloState.SCARED) { + STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_PEEKING); + updateBedrockEntityProperties(); + + // Needed for consecutive peeks + session.scheduleInEventLoop(() -> { + if (armadilloState == ArmadilloState.SCARED) { + STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING); + updateBedrockEntityProperties(); + } + }, 250, TimeUnit.MILLISECONDS); + } + } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.ARMADILLO_FOOD; + } + + public enum State { + UNROLLED, + ROLLED_UP, + ROLLED_UP_PEEKING, + ROLLED_UP_RELAXING, + ROLLED_UP_UNROLLING + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java index 85b2afc14..0a87f59bc 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java @@ -25,10 +25,8 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -36,8 +34,13 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -60,8 +63,9 @@ public class AxolotlEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return session.getTagCache().isAxolotlTemptItem(item); + @Nullable + protected Tag getFoodTag() { + return ItemTag.AXOLOTL_FOOD; } @Override @@ -70,7 +74,7 @@ public class AxolotlEntity extends AnimalEntity { } @Override - protected boolean canBeLeashed() { + public boolean canBeLeashed() { return true; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java index e05b44cf0..919af7c22 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java @@ -25,21 +25,31 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; public class BeeEntity extends AnimalEntity { + public static final BooleanProperty NECTAR_PROPERTY = new BooleanProperty( + IdentifierImpl.of("has_nectar"), + false + ); + public BeeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -57,7 +67,8 @@ public class BeeEntity extends AnimalEntity { // If the bee has stung dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0); // If the bee has nectar or not - setFlag(EntityFlag.POWERED, (xd & 0x08) == 0x08); + NECTAR_PROPERTY.apply(propertyManager, (xd & 0x08) == 0x08); + updateBedrockEntityProperties(); } public void setAngerTime(IntEntityMetadata entityMetadata) { @@ -66,7 +77,8 @@ public class BeeEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return session.getTagCache().isFlower(item); + @Nullable + protected Tag getFoodTag() { + return ItemTag.BEE_FOOD; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FoxEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FoxEntity.java index 98c73cbce..b140b7956 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FoxEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FoxEntity.java @@ -25,14 +25,17 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -55,7 +58,8 @@ public class FoxEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return session.getTagCache().isFoxFood(item); + @Nullable + protected Tag getFoodTag() { + return ItemTag.FOX_FOOD; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java index 039ef5bf9..8638267c8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java @@ -25,22 +25,25 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; import java.util.OptionalInt; import java.util.UUID; -public class FrogEntity extends AnimalEntity { +public class FrogEntity extends AnimalEntity implements VariantIntHolder { public FrogEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -54,13 +57,14 @@ public class FrogEntity extends AnimalEntity { super.setPose(pose); } - public void setFrogVariant(IntEntityMetadata entityMetadata) { - int variant = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.VARIANT, switch (variant) { - case 1 -> 2; // White - case 2 -> 1; // Green - default -> variant; - }); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.FROG_VARIANT; + } + + @Override + public void setBedrockVariantId(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } public void setTongueTarget(ObjectEntityMetadata entityMetadata) { @@ -76,7 +80,16 @@ public class FrogEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.SLIME_BALL; + @Nullable + protected Tag getFoodTag() { + return ItemTag.FROG_FOOD; + } + + // Ordered by bedrock id + // TODO: are these ordered correctly? + public enum BuiltInVariant implements BuiltIn { + TEMPERATE, + COLD, + WARM } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java index 9ed94f96f..02d7c62de 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java @@ -25,10 +25,8 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -36,8 +34,14 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -59,12 +63,12 @@ public class GoatEntity extends AnimalEntity { } @Override - protected void setDimensions(Pose pose) { + protected void setDimensionsFromPose(Pose pose) { if (pose == Pose.LONG_JUMPING) { setBoundingBoxWidth(LONG_JUMPING_WIDTH); setBoundingBoxHeight(LONG_JUMPING_HEIGHT); } else { - super.setDimensions(pose); + super.setDimensionsFromPose(pose); } } @@ -73,7 +77,7 @@ public class GoatEntity extends AnimalEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (!getFlag(EntityFlag.BABY) && itemInHand.asItem() == Items.BUCKET) { + if (!getFlag(EntityFlag.BABY) && itemInHand.is(Items.BUCKET)) { session.playSoundEvent(isScreamer ? SoundEvent.MILK_SCREAMER : SoundEvent.MILK, position); return InteractionResult.SUCCESS; } else { @@ -94,4 +98,10 @@ public class GoatEntity extends AnimalEntity { private void setHornCount() { dirtyMetadata.put(EntityDataTypes.GOAT_HORN_COUNT, (hasLeftHorn ? 1 : 0) + (hasRightHorn ? 1 : 0)); } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.GOAT_FOOD; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java new file mode 100644 index 000000000..51f61c6bf --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.TrigMath; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.util.AttributeUtils; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; + +import java.util.List; +import java.util.UUID; + +public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { + + public static final float[] X_OFFSETS = {0.0F, -1.7F, 0.0F, 1.7F}; + public static final float[] Z_OFFSETS = {1.7F, 0.0F, -1.7F, 0.0F}; + + public static final BooleanProperty CAN_MOVE_PROPERTY = new BooleanProperty( + IdentifierImpl.of("can_move"), + true + ); + + private final HappyGhastVehicleComponent vehicleComponent = new HappyGhastVehicleComponent(this, 0.0f); + private boolean staysStill; + + public HappyGhastEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // BDS 1.21.90 + setFlag(EntityFlag.CAN_FLY, true); + setFlag(EntityFlag.CAN_WALK, true); + setFlag(EntityFlag.TAMED, true); + setFlag(EntityFlag.BODY_ROTATION_ALWAYS_FOLLOWS_HEAD, true); + setFlag(EntityFlag.COLLIDABLE, true); + + setFlag(EntityFlag.WASD_AIR_CONTROLLED, true); + setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true); + } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.HAPPY_GHAST_FOOD; + } + + @Override + protected float getBabySize() { + return 0.2375f; + } + + @Override + protected float getAdultSize() { + // Make collision slightly larger to stop bedrock client from clipping into it + // This value will not work at very large coordinates + return 1.001f; + } + + @Override + public void setBaby(BooleanEntityMetadata entityMetadata) { + super.setBaby(entityMetadata); + // Players can only stand on grown up happy ghasts + setFlag(EntityFlag.COLLIDABLE, !entityMetadata.getPrimitiveValue()); + } + + public void setStaysStill(BooleanEntityMetadata entityMetadata) { + staysStill = entityMetadata.getPrimitiveValue(); + CAN_MOVE_PROPERTY.apply(propertyManager, !entityMetadata.getPrimitiveValue()); + updateBedrockEntityProperties(); + } + + @NonNull + @Override + protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (this.isBaby()) { + return super.testMobInteraction(hand, itemInHand); + } else { + if (!itemInHand.isEmpty()) { + if (itemInHand.is(session, ItemTag.HARNESSES)) { + if (getItemInSlot(EquipmentSlot.BODY).isEmpty()) { + // Harnesses the ghast + return InteractiveTag.EQUIP_HARNESS; + } + } else if (itemInHand.is(Items.SHEARS)) { + if (this.canShearEquipment() && !session.isSneaking()) { + // Shears the harness off of the ghast + return InteractiveTag.REMOVE_HARNESS; + } + } + } + + if (!getItemInSlot(EquipmentSlot.BODY).isEmpty() && !session.isSneaking()) { + // Rides happy ghast + return InteractiveTag.RIDE_HORSE; + } else { + return super.testMobInteraction(hand, itemInHand); + } + } + } + + @NonNull + @Override + protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (this.isBaby()) { + return super.mobInteract(hand, itemInHand); + } else { + if (!itemInHand.isEmpty()) { + if (itemInHand.is(session, ItemTag.HARNESSES)) { + if (getItemInSlot(EquipmentSlot.BODY).isEmpty()) { + // Harnesses the ghast + return InteractionResult.SUCCESS; + } + } else if (itemInHand.is(Items.SHEARS)) { + if (this.canShearEquipment() && !session.isSneaking()) { + // Shears the harness off of the ghast + return InteractionResult.SUCCESS; + } + } + } + + if (!getItemInSlot(EquipmentSlot.BODY).isEmpty() && !session.isSneaking()) { + // Rides happy ghast + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(hand, itemInHand); + } + } + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector3f getRiddenInput(Vector2f input) { + float x = input.getX(); + float y = 0.0f; + float z = 0.0f; + + if (input.getY() != 0.0f) { + float pitch = session.getPlayerEntity().getPitch(); + z = TrigMath.cos(pitch * TrigMath.DEG_TO_RAD); + y = -TrigMath.sin(pitch * TrigMath.DEG_TO_RAD); + if (input.getY() < 0.0f) { + z *= -0.5f; + y *= -0.5f; + } + } + + if (session.getInputCache().wasJumping()) { + y += 0.5f; + } + + return Vector3f.from(x, y, z).mul(3.9f * vehicleComponent.getFlyingSpeed()); + } + + @Override + public float getVehicleSpeed() { + return 0.0f; // Not used + } + + @Override + public boolean isClientControlled() { + if (!hasBodyArmor() || staysStill) { + return false; + } + + return getFirstPassenger() instanceof SessionPlayerEntity; + } + + private Entity getFirstPassenger() { + return passengers.isEmpty() ? null : passengers.get(0); + } + + @Override + protected void updateAttribute(Attribute javaAttribute, List newAttributes) { + super.updateAttribute(javaAttribute, newAttributes); + if (javaAttribute.getType() instanceof AttributeType.Builtin type) { + if (type == AttributeType.Builtin.CAMERA_DISTANCE) { + vehicleComponent.setCameraDistance((float) AttributeUtils.calculateValue(javaAttribute)); + } + } + } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.BODY ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java index 154c2f688..9ea70fa78 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java @@ -25,13 +25,16 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; @@ -40,6 +43,8 @@ public class HoglinEntity extends AnimalEntity { public HoglinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + dirtyMetadata.put(EntityDataTypes.TARGET_EID, session.getPlayerEntity().getGeyserId()); + setFlag(EntityFlag.SHAKING, isShaking()); } public void setImmuneToZombification(BooleanEntityMetadata entityMetadata) { @@ -54,17 +59,23 @@ public class HoglinEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.CRIMSON_FUNGUS; + @Nullable + protected Tag getFoodTag() { + return ItemTag.HOGLIN_FOOD; } @Override - protected boolean canBeLeashed() { - return isNotLeashed(); + public boolean canBeLeashed() { + return true; } @Override protected boolean isEnemy() { return true; } + + @Override + public boolean useArmSwingAttack() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index 1c347bf31..78f2ea025 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -25,41 +25,48 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.type.FlowerItem; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class MooshroomEntity extends AnimalEntity { +public class MooshroomEntity extends CowEntity { private boolean isBrown = false; public MooshroomEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - public void setVariant(ObjectEntityMetadata entityMetadata) { - isBrown = entityMetadata.getValue().equals("brown"); - dirtyMetadata.put(EntityDataTypes.VARIANT, isBrown ? 1 : 0); + public void setMooshroomVariant(IntEntityMetadata metadata) { + isBrown = metadata.getPrimitiveValue() == 1; + dirtyMetadata.put(EntityDataTypes.VARIANT, metadata.getPrimitiveValue()); + } + + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + // There are no variants for mooshroom cows, so far } @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { if (!isBaby()) { - if (itemInHand.asItem() == Items.BOWL) { + if (itemInHand.is(Items.BOWL)) { // Stew return InteractiveTag.MOOSHROOM_MILK_STEW; - } else if (isAlive() && itemInHand.asItem() == Items.SHEARS) { + } else if (isAlive() && itemInHand.is(Items.SHEARS)) { // Shear items return InteractiveTag.MOOSHROOM_SHEAR; } @@ -71,13 +78,13 @@ public class MooshroomEntity extends AnimalEntity { @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { boolean isBaby = isBaby(); - if (!isBaby && itemInHand.asItem() == Items.BOWL) { + if (!isBaby && itemInHand.is(Items.BOWL)) { // Stew return InteractionResult.SUCCESS; - } else if (!isBaby && isAlive() && itemInHand.asItem() == Items.SHEARS) { + } else if (!isBaby && isAlive() && itemInHand.is(Items.SHEARS)) { // Shear items return InteractionResult.SUCCESS; - } else if (isBrown && session.getTagCache().isSmallFlower(itemInHand) && itemInHand.asItem() instanceof FlowerItem) { + } else if (isBrown && itemInHand.is(session, ItemTag.SMALL_FLOWERS)) { // ? return InteractionResult.SUCCESS; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java index c115ebcdc..c45092b02 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java @@ -25,17 +25,19 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -46,8 +48,9 @@ public class OcelotEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.COD || item == Items.SALMON; + @Nullable + protected Tag getFoodTag() { + return ItemTag.OCELOT_FOOD; } @NonNull diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java index d2ef36932..358643b0e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java @@ -25,9 +25,6 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; @@ -37,11 +34,15 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -62,7 +63,8 @@ public class PandaEntity extends AnimalEntity { EntityEventPacket packet = new EntityEventPacket(); packet.setRuntimeEntityId(geyserId); packet.setType(EntityEventType.EATING_ITEM); - packet.setData(session.getItemMappings().getStoredItems().bamboo().getBedrockDefinition().getRuntimeId() << 16); + // As of 1.20.5 - pandas can eat cake + packet.setData(session.getItemMappings().getMapping(getMainHandItem()).getBedrockDefinition().getRuntimeId() << 16); session.sendUpstreamPacket(packet); } } @@ -89,8 +91,9 @@ public class PandaEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.BAMBOO; + @Nullable + protected Tag getFoodTag() { + return ItemTag.PANDA_FOOD; } @NonNull @@ -122,7 +125,7 @@ public class PandaEntity extends AnimalEntity { } @Override - protected boolean canBeLeashed() { + public boolean canBeLeashed() { return false; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PolarBearEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PolarBearEntity.java index 1d7777cdb..900878dbd 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PolarBearEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PolarBearEntity.java @@ -25,10 +25,12 @@ package org.geysermc.geyser.entity.type.living.animal; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.Tag; import java.util.UUID; @@ -39,7 +41,8 @@ public class PolarBearEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return false; + @Nullable + protected Tag getFoodTag() { + return null; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PufferFishEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PufferFishEntity.java index d0d119593..6f0063474 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PufferFishEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PufferFishEntity.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.AbstractFishEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/RabbitEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/RabbitEntity.java index 1efa87ec8..dca6fe5ff 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/RabbitEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/RabbitEntity.java @@ -25,18 +25,23 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; public class RabbitEntity extends AnimalEntity { + private boolean isKillerBunny; public RabbitEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -46,7 +51,7 @@ public class RabbitEntity extends AnimalEntity { int variant = entityMetadata.getPrimitiveValue(); // Change the killer bunny to display as white since it only exists on Java Edition - boolean isKillerBunny = variant == 99; + isKillerBunny = variant == 99; if (isKillerBunny) { variant = 1; } @@ -56,6 +61,14 @@ public class RabbitEntity extends AnimalEntity { dirtyMetadata.put(EntityDataTypes.VARIANT, variant); } + @Override + protected String standardDisplayName() { + if (isKillerBunny) { + return EntityUtils.translatedEntityName(Key.key("killer_bunny"), session); + } + return super.standardDisplayName(); + } + @Override protected float getAdultSize() { return 0.55f; @@ -67,7 +80,8 @@ public class RabbitEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.DANDELION || item == Items.CARROT || item == Items.GOLDEN_CARROT; + @Nullable + protected Tag getFoodTag() { + return ItemTag.RABBIT_FOOD; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java index 13059244a..35863071c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java @@ -25,9 +25,8 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -35,9 +34,14 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.DyeItem; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -55,10 +59,16 @@ public class SheepEntity extends AnimalEntity { dirtyMetadata.put(EntityDataTypes.COLOR, (byte) color); } + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.SHEEP_FOOD; + } + @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() == Items.SHEARS) { + if (itemInHand.is(Items.SHEARS)) { return InteractiveTag.SHEAR; } else { InteractiveTag tag = super.testMobInteraction(hand, itemInHand); @@ -76,7 +86,7 @@ public class SheepEntity extends AnimalEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() == Items.SHEARS) { + if (itemInHand.is(Items.SHEARS)) { return InteractionResult.CONSUME; } else { InteractionResult superResult = super.mobInteract(hand, itemInHand); @@ -95,4 +105,4 @@ public class SheepEntity extends AnimalEntity { private boolean canDye(GeyserItemStack item) { return item.asItem() instanceof DyeItem dyeItem && dyeItem.dyeColor() != this.color && !getFlag(EntityFlag.SHEARED); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java index ba67cf058..935be72c5 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java @@ -25,9 +25,7 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.SnifferState; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; @@ -39,6 +37,11 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.SnifferState; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; import java.util.UUID; @@ -61,18 +64,19 @@ public class SnifferEntity extends AnimalEntity implements Tickable { } @Override - protected void setDimensions(Pose pose) { + protected void setDimensionsFromPose(Pose pose) { if (getFlag(EntityFlag.DIGGING)) { setBoundingBoxHeight(DIGGING_HEIGHT); setBoundingBoxWidth(definition.width()); } else { - super.setDimensions(pose); + super.setDimensionsFromPose(pose); } } @Override - public boolean canEat(Item item) { - return session.getTagCache().isSnifferFood(item); + @Nullable + protected Tag getFoodTag() { + return ItemTag.SNIFFER_FOOD; } public void setSnifferState(ObjectEntityMetadata entityMetadata) { @@ -86,7 +90,7 @@ public class SnifferEntity extends AnimalEntity implements Tickable { setFlag(EntityFlag.DIGGING, snifferState == SnifferState.DIGGING); setFlag(EntityFlag.RISING, snifferState == SnifferState.RISING); - setDimensions(pose); + setDimensionsFromPose(pose); if (getFlag(EntityFlag.DIGGING)) { digTicks = DIG_END; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java index 39a55fa1e..186ecd2dd 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java @@ -25,25 +25,37 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.Tickable; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class StriderEntity extends AnimalEntity { +public class StriderEntity extends AnimalEntity implements Tickable, ClientVehicle { + private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f); private boolean isCold = false; public StriderEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -57,10 +69,6 @@ public class StriderEntity extends AnimalEntity { isCold = entityMetadata.getPrimitiveValue(); } - public void setSaddled(BooleanEntityMetadata entityMetadata) { - setFlag(EntityFlag.SADDLED, entityMetadata.getPrimitiveValue()); - } - @Override public void updateBedrockMetadata() { // Make sure they are not shaking when riding another entity @@ -94,8 +102,9 @@ public class StriderEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.WARPED_FUNGUS; + @Nullable + protected Tag getFoodTag() { + return ItemTag.STRIDER_FOOD; } @NonNull @@ -130,4 +139,64 @@ public class StriderEntity extends AnimalEntity { } } } + + public void setBoost(IntEntityMetadata entityMetadata) { + vehicleComponent.startBoost(entityMetadata.getPrimitiveValue()); + } + + @Override + public void tick() { + PlayerEntity player = getPlayerPassenger(); + if (player == null) { + return; + } + + if (player == session.getPlayerEntity()) { + if (session.getPlayerInventory().isHolding(Items.WARPED_FUNGUS_ON_A_STICK)) { + vehicleComponent.tickBoost(); + } + } else { // getHand() for session player seems to always return air + if (player.isHolding(Items.WARPED_FUNGUS_ON_A_STICK)) { + vehicleComponent.tickBoost(); + } + } + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector3f getRiddenInput(Vector2f input) { + return Vector3f.UNIT_Z; + } + + @Override + public float getVehicleSpeed() { + return vehicleComponent.getMoveSpeed() * (isCold ? 0.35f : 0.55f) * vehicleComponent.getBoostMultiplier(); + } + + private @Nullable PlayerEntity getPlayerPassenger() { + if (getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) instanceof PlayerEntity playerEntity) { + return playerEntity; + } + + return null; + } + + @Override + public boolean isClientControlled() { + return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.WARPED_FUNGUS_ON_A_STICK); + } + + @Override + public boolean canWalkOnLava() { + return true; + } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java index b18e55a48..182bb176f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java @@ -25,14 +25,14 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.ints.IntList; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; -import it.unimi.dsi.fastutil.ints.IntList; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.AbstractFishEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.List; import java.util.UUID; @@ -61,6 +61,10 @@ public class TropicalFishEntity extends AbstractFishEntity { dirtyMetadata.put(EntityDataTypes.COLOR_2, getPatternColor(varNumber)); // Pattern color 0-15 } + public static int getPackedVariant(int pattern, int baseColor, int patternColor) { + return pattern & 65535 | (baseColor & 0xFF) << 16 | (patternColor & 0xFF) << 24; + } + public static int getShape(int variant) { return Math.min(variant & 0xFF, 1); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java index 870ded193..1f700f12b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java @@ -25,13 +25,15 @@ package org.geysermc.geyser.entity.type.living.animal; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; @@ -50,12 +52,24 @@ public class TurtleEntity extends AnimalEntity { } @Override - public boolean canEat(Item item) { - return item == Items.SEAGRASS; + @Nullable + protected Tag getFoodTag() { + return ItemTag.TURTLE_FOOD; } @Override - protected boolean canBeLeashed() { + protected float getAdultSize() { + return super.getAdultSize() * 0.7f; + } + + @Override + protected float getBabySize() { + // 0.3f is Java scale, plus Bedrock difference + return 0.3f * 0.5f; + } + + @Override + public boolean canBeLeashed() { return false; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java new file mode 100644 index 000000000..c47480563 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal; + +import net.kyori.adventure.key.Key; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.RegistryCache; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; + +import java.util.Locale; + +/** + * Interface to help set up data-driven entity variants for mobs. + * + *

Data-driven variants are sent as an int ID of their variant registry by Java, but can be a metadata ID or entity property on bedrock. + * This interface helps translate data-driven variants to built-in bedrock ones.

+ * + *

Implementations usually have to implement {@link VariantHolder#variantRegistry()} and {@link VariantHolder#setBedrockVariant(BuiltIn)}, and should also + * have an enum with built-in variants on bedrock (implementing {@link BuiltIn}).

+ * + * @param the enum of Bedrock variants. + */ +public interface VariantHolder { + + default void setVariant(IntEntityMetadata variant) { + setVariantFromJavaId(variant.getPrimitiveValue()); + } + + /** + * Sets the variant of the entity. + */ + default void setVariantFromJavaId(int variant) { + setBedrockVariant(variantRegistry().value(getSession(), variant)); + } + + GeyserSession getSession(); + + /** + * The registry in {@link org.geysermc.geyser.session.cache.registry.JavaRegistries} for this mob's variants. The registry can utilise the {@link VariantHolder#reader(Class, Enum)} method + * to create a reader to be used in {@link org.geysermc.geyser.session.cache.RegistryCache}. + */ + JavaRegistryKey variantRegistry(); + + /** + * Should set the variant for bedrock. + */ + void setBedrockVariant(BedrockVariant bedrockVariant); + + /** + * Creates a registry reader for this mob's variants. + * + *

This reader simply matches the identifiers of registry entries with built-in variants. If no built-in variant matches, the fallback/default is returned.

+ */ + static > RegistryCache.RegistryReader reader(Class clazz, BuiltInVariant fallback) { + BuiltInVariant[] variants = clazz.getEnumConstants(); + if (variants == null) { + throw new IllegalArgumentException("Class is not an enum"); + } + return context -> { + for (BuiltInVariant variant : variants) { + if (((BuiltIn) variant).javaIdentifier().equals(context.id())) { + return variant; + } + } + return fallback; + }; + } + + /** + * Should be implemented on an enum within the entity class. The enum lists vanilla variants that can appear on bedrock. + * + *

The enum constants should be named the same as their Java identifiers.

+ */ + interface BuiltIn { + + String name(); + + default Key javaIdentifier() { + return MinecraftKey.key(name().toLowerCase(Locale.ROOT)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java new file mode 100644 index 000000000..f1d45a447 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal; + +/** + * Extension to {@link VariantHolder} to make it easier to implement on mobs that use bedrock's metadata system to set their variants, which are quite common. + * + * @see VariantHolder + */ +public interface VariantIntHolder extends VariantHolder { + + @Override + default void setBedrockVariant(BuiltIn variant) { + setBedrockVariantId(variant.ordinal()); + } + + /** + * Should set the variant on bedrock's metadata. The bedrock ID has already been checked and is always valid. + */ + void setBedrockVariantId(int bedrockId); + + /** + * The enum constants should be ordered in the order of their bedrock network ID. + * + * @see org.geysermc.geyser.entity.type.living.animal.VariantHolder.BuiltIn + */ + interface BuiltIn extends VariantHolder.BuiltIn { + + int ordinal(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java new file mode 100644 index 000000000..4ee7175de --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal.farm; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; + +import java.util.UUID; + +public class ChickenEntity extends TemperatureVariantAnimal { + + public ChickenEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.CHICKEN_FOOD; + } + + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.CHICKEN_VARIANT; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java similarity index 72% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index cdcf534a3..8c1a27b95 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,23 +23,30 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CowEntity extends AnimalEntity { +public class CowEntity extends TemperatureVariantAnimal { + public CowEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -47,7 +54,7 @@ public class CowEntity extends AnimalEntity { @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (getFlag(EntityFlag.BABY) || itemInHand.asItem() != Items.BUCKET) { + if (getFlag(EntityFlag.BABY) || !itemInHand.is(Items.BUCKET)) { return super.testMobInteraction(hand, itemInHand); } @@ -57,11 +64,22 @@ public class CowEntity extends AnimalEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (getFlag(EntityFlag.BABY) || itemInHand.asItem() != Items.BUCKET) { + if (getFlag(EntityFlag.BABY) || !itemInHand.is(Items.BUCKET)) { return super.mobInteract(hand, itemInHand); } session.playSoundEvent(SoundEvent.MILK, position); return InteractionResult.SUCCESS; } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.COW_FOOD; + } + + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.COW_VARIANT; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java similarity index 51% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index 2bc02cd55..bb629c3d7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,32 +23,47 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.Tickable; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class PigEntity extends AnimalEntity { +public class PigEntity extends TemperatureVariantAnimal implements Tickable, ClientVehicle { + private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f); public PigEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @Override - public boolean canEat(Item item) { - return item == Items.CARROT || item == Items.POTATO || item == Items.BEETROOT; + @Nullable + protected Tag getFoodTag() { + return ItemTag.PIG_FOOD; } @NonNull @@ -83,4 +98,64 @@ public class PigEntity extends AnimalEntity { } } } + + public void setBoost(IntEntityMetadata entityMetadata) { + vehicleComponent.startBoost(entityMetadata.getPrimitiveValue()); + } + + @Override + public void tick() { + PlayerEntity player = getPlayerPassenger(); + if (player == null) { + return; + } + + if (player == session.getPlayerEntity()) { + if (session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK)) { + vehicleComponent.tickBoost(); + } + } else { // getHand() for session player seems to always return air + if (player.isHolding(Items.CARROT_ON_A_STICK)) { + vehicleComponent.tickBoost(); + } + } + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector3f getRiddenInput(Vector2f input) { + return Vector3f.UNIT_Z; + } + + @Override + public float getVehicleSpeed() { + return vehicleComponent.getMoveSpeed() * 0.225f * vehicleComponent.getBoostMultiplier(); + } + + private @Nullable PlayerEntity getPlayerPassenger() { + if (getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) instanceof PlayerEntity playerEntity) { + return playerEntity; + } + + return null; + } + + @Override + public boolean isClientControlled() { + return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK); + } + + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.PIG_VARIANT; + } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java new file mode 100644 index 000000000..9d772c8a2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal.farm; + +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; +import org.geysermc.geyser.entity.type.living.animal.VariantHolder; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.RegistryCache; + +import java.util.UUID; + +public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { + + public static final EnumProperty TEMPERATE_VARIANT_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("climate_variant"), + BuiltInVariant.class, + BuiltInVariant.TEMPERATE + ); + + public static final RegistryCache.RegistryReader VARIANT_READER = VariantHolder.reader(BuiltInVariant.class, BuiltInVariant.TEMPERATE); + + public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, + Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public void setBedrockVariant(BuiltInVariant variant) { + TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, variant); + updateBedrockEntityProperties(); + } + + public enum BuiltInVariant implements VariantHolder.BuiltIn { + TEMPERATE, + WARM, + COLD; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java index faa495487..65e2ed051 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java @@ -25,9 +25,8 @@ package org.geysermc.geyser.entity.type.living.animal.horse; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; @@ -42,19 +41,17 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import java.util.Set; import java.util.UUID; public class AbstractHorseEntity extends AnimalEntity { - /** - * A list of all foods a horse/donkey can eat on Java Edition. - * Used to display interactive tag if needed. - */ - private static final Set DONKEY_AND_HORSE_FOODS = Set.of(Items.GOLDEN_APPLE, Items.ENCHANTED_GOLDEN_APPLE, - Items.GOLDEN_CARROT, Items.SUGAR, Items.APPLE, Items.WHEAT, Items.HAY_BLOCK); public AbstractHorseEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -83,12 +80,17 @@ public class AbstractHorseEntity extends AnimalEntity { session.sendUpstreamPacket(attributesPacket); } + @Override + public void updateSaddled(boolean saddled) { + // Shows the jump meter + setFlag(EntityFlag.CAN_POWER_JUMP, saddled); + super.updateSaddled(saddled); + } + public void setHorseFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); boolean tamed = (xd & 0x02) == 0x02; - boolean saddled = (xd & 0x04) == 0x04; setFlag(EntityFlag.TAMED, tamed); - setFlag(EntityFlag.SADDLED, saddled); setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10); setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20); @@ -118,14 +120,12 @@ public class AbstractHorseEntity extends AnimalEntity { // Set container type if tamed dirtyMetadata.put(EntityDataTypes.CONTAINER_TYPE, tamed ? (byte) ContainerType.HORSE.getId() : (byte) 0); - - // Shows the jump meter - setFlag(EntityFlag.CAN_POWER_JUMP, saddled); } @Override - public boolean canEat(Item item) { - return DONKEY_AND_HORSE_FOODS.contains(item); + @Nullable + protected Tag getFoodTag() { + return ItemTag.HORSE_FOOD; } @NonNull @@ -165,7 +165,7 @@ public class AbstractHorseEntity extends AnimalEntity { return InteractiveTag.ATTACH_CHEST; } - if (additionalTestForInventoryOpen(itemInHand) || !isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.asItem() == Items.SADDLE) { + if (additionalTestForInventoryOpen(itemInHand) || !isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.is(Items.SADDLE)) { // Will open the inventory to be saddled return InteractiveTag.OPEN_CONTAINER; } @@ -221,7 +221,7 @@ public class AbstractHorseEntity extends AnimalEntity { } // Note: yes, this code triggers for llamas too. lol (as of Java Edition 1.18.1) - if (additionalTestForInventoryOpen(itemInHand) || (!isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.asItem() == Items.SADDLE)) { + if (additionalTestForInventoryOpen(itemInHand) || (!isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.is(Items.SADDLE))) { // Will open the inventory to be saddled return InteractionResult.SUCCESS; } @@ -245,6 +245,7 @@ public class AbstractHorseEntity extends AnimalEntity { } protected boolean additionalTestForInventoryOpen(@NonNull GeyserItemStack itemInHand) { + // TODO this doesn't seem right anymore... (as of Java 1.21.9) return itemInHand.asItem().javaIdentifier().endsWith("_horse_armor"); } @@ -260,7 +261,7 @@ public class AbstractHorseEntity extends AnimalEntity { } else if (!passengers.isEmpty()) { return testHorseInteraction(hand, itemInHand); } else { - if (Items.SADDLE == itemInHand.asItem()) { + if (itemInHand.is(Items.SADDLE)) { return InteractiveTag.OPEN_CONTAINER; } @@ -287,4 +288,13 @@ public class AbstractHorseEntity extends AnimalEntity { return InteractionResult.SUCCESS; } } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + if (slot != EquipmentSlot.SADDLE) { + return super.canUseSlot(slot); + } else { + return isAlive() && !isBaby() && getFlag(EntityFlag.TAMED); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java index 8c79f663f..9926d6247 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java @@ -25,26 +25,38 @@ package org.geysermc.geyser.entity.type.living.animal.horse; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.cloudburstmc.math.vector.Vector2f; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.vehicle.CamelVehicleComponent; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.LongEntityMetadata; import java.util.UUID; -public class CamelEntity extends AbstractHorseEntity { - +public class CamelEntity extends AbstractHorseEntity implements ClientVehicle { public static final float SITTING_HEIGHT_DIFFERENCE = 1.43F; + private final CamelVehicleComponent vehicleComponent = new CamelVehicleComponent(this); + public CamelEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -56,8 +68,6 @@ public class CamelEntity extends AbstractHorseEntity { public void setHorseFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); - boolean saddled = (xd & 0x04) == 0x04; - setFlag(EntityFlag.SADDLED, saddled); setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10); setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20); @@ -86,12 +96,12 @@ public class CamelEntity extends AbstractHorseEntity { } // Shows the dash meter - setFlag(EntityFlag.CAN_DASH, saddled); + // setFlag(EntityFlag.CAN_DASH, saddled); } @Override - public boolean canEat(Item item) { - return item == Items.CACTUS; + protected @Nullable Tag getFoodTag() { + return ItemTag.CAMEL_FOOD; } @Override @@ -101,15 +111,69 @@ public class CamelEntity extends AbstractHorseEntity { } @Override - protected void setDimensions(Pose pose) { + protected void setDimensionsFromPose(Pose pose) { if (pose == Pose.SITTING) { setBoundingBoxHeight(definition.height() - SITTING_HEIGHT_DIFFERENCE); setBoundingBoxWidth(definition.width()); } else { - super.setDimensions(pose); + super.setDimensionsFromPose(pose); } } public void setDashing(BooleanEntityMetadata entityMetadata) { + // Java sends true to show dash animation and start the dash cooldown, + // false ends the dash animation, not the cooldown. + // Bedrock shows dash animation if HAS_DASH_COOLDOWN is set and the camel is above ground + if (entityMetadata.getPrimitiveValue()) { + setFlag(EntityFlag.HAS_DASH_COOLDOWN, true); + vehicleComponent.startDashCooldown(); + } else if (!isClientControlled()) { // Don't remove dash cooldown prematurely if client is controlling + setFlag(EntityFlag.HAS_DASH_COOLDOWN, false); + } + } + + public void setLastPoseTick(LongEntityMetadata entityMetadata) { + // Tick is based on world time. If negative, the camel is sitting. + // Must be compared to world time to know if the camel is fully standing/sitting or transitioning. + vehicleComponent.setLastPoseTick(entityMetadata.getPrimitiveValue()); + } + + @Override + protected AttributeData calculateAttribute(Attribute javaAttribute, GeyserAttributeType type) { + AttributeData attributeData = super.calculateAttribute(javaAttribute, type); + if (javaAttribute.getType() == AttributeType.Builtin.JUMP_STRENGTH) { + vehicleComponent.setHorseJumpStrength(attributeData.getValue()); + } + return attributeData; + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector3f getRiddenInput(Vector2f input) { + input = input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f); + return Vector3f.from(input.getX(), 0.0, input.getY()); + } + + @Override + public boolean isClientControlled() { + return getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) == session.getPlayerEntity(); + } + + @Override + public float getVehicleSpeed() { + float moveSpeed = vehicleComponent.getMoveSpeed(); + if (!getFlag(EntityFlag.HAS_DASH_COOLDOWN) && session.getPlayerEntity().getFlag(EntityFlag.SPRINTING)) { + return moveSpeed + 0.1f; + } + return moveSpeed; + } + + @Override + public boolean canClimb() { + return false; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java index fc8584de3..491ca02df 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java @@ -54,7 +54,7 @@ public class ChestedHorseEntity extends AbstractHorseEntity { @Override protected boolean testForChest(@NonNull GeyserItemStack itemInHand) { - return itemInHand.asItem() == Items.CHEST && !getFlag(EntityFlag.CHESTED); + return itemInHand.is(Items.CHEST) && !getFlag(EntityFlag.CHESTED); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java index dfa6ef30a..478f89aeb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java @@ -25,11 +25,12 @@ package org.geysermc.geyser.entity.type.living.animal.horse; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -44,4 +45,9 @@ public class HorseEntity extends AbstractHorseEntity { dirtyMetadata.put(EntityDataTypes.VARIANT, value & 255); dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (value >> 8) % 5); } -} \ No newline at end of file + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java index a32d7b1b5..6ef1cd57a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java @@ -25,19 +25,27 @@ package org.geysermc.geyser.entity.type.living.animal.horse; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; -import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; -import org.cloudburstmc.protocol.bedrock.packet.MobArmorEquipmentPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; public class LlamaEntity extends ChestedHorseEntity { + /** + * Used to calculate inventory size + */ + @Getter + private int strength = 1; public LlamaEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -45,32 +53,18 @@ public class LlamaEntity extends ChestedHorseEntity { dirtyMetadata.put(EntityDataTypes.CONTAINER_STRENGTH_MODIFIER, 3); // Presumably 3 slots for every 1 strength } - /** - * Color equipped on the llama - */ - public void setCarpetedColor(IntEntityMetadata entityMetadata) { - // Bedrock treats llama decoration as armor - MobArmorEquipmentPacket equipmentPacket = new MobArmorEquipmentPacket(); - equipmentPacket.setRuntimeEntityId(geyserId); - // -1 means no armor - int carpetIndex = entityMetadata.getPrimitiveValue(); - if (carpetIndex > -1 && carpetIndex <= 15) { - // The damage value is the dye color that Java sends us, for pre-1.16.220 - // The item is always going to be a carpet - equipmentPacket.setChestplate(session.getItemMappings().getCarpets().get(carpetIndex)); - } else { - equipmentPacket.setChestplate(ItemData.AIR); - } - // Required to fill out the rest of the equipment or Bedrock ignores it, including above else statement if removing armor - equipmentPacket.setBoots(ItemData.AIR); - equipmentPacket.setHelmet(ItemData.AIR); - equipmentPacket.setLeggings(ItemData.AIR); - - session.sendUpstreamPacket(equipmentPacket); + public void setStrength(IntEntityMetadata entityMetadata) { + strength = MathUtils.constrain(entityMetadata.getPrimitiveValue(), 1, 5); + this.dirtyMetadata.put(EntityDataTypes.STRENGTH, strength); } @Override - public boolean canEat(Item item) { - return item == Items.WHEAT || item == Items.HAY_BLOCK; + protected @Nullable Tag getFoodTag() { + return ItemTag.LLAMA_FOOD; + } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return true; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java index 7080f9f75..d74913c31 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living.animal.horse; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; @@ -33,6 +32,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java index 3275712fc..9e77daebc 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living.animal.horse; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; @@ -33,6 +32,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java index 412157b5d..e90cb3f1c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java @@ -25,27 +25,32 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CatEntity extends TameableEntity { +public class CatEntity extends TameableEntity implements VariantIntHolder { - private byte collarColor; + private byte collarColor = 14; // Red - default public CatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -76,23 +81,17 @@ public class CatEntity extends TameableEntity { @Override public void setTameableFlags(ByteEntityMetadata entityMetadata) { super.setTameableFlags(entityMetadata); - // Update collar color if tamed - if (getFlag(EntityFlag.TAMED)) { - dirtyMetadata.put(EntityDataTypes.COLOR, collarColor); - } + updateCollarColor(); } - public void setCatVariant(IntEntityMetadata entityMetadata) { - // Different colors in Java and Bedrock for some reason - int metadataValue = entityMetadata.getPrimitiveValue(); - int variantColor = switch (metadataValue) { - case 0 -> 8; - case 8 -> 0; - case 9 -> 10; - case 10 -> 9; - default -> metadataValue; - }; - dirtyMetadata.put(EntityDataTypes.VARIANT, variantColor); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.CAT_VARIANT; + } + + @Override + public void setBedrockVariantId(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } public void setResting(BooleanEntityMetadata entityMetadata) { @@ -101,6 +100,10 @@ public class CatEntity extends TameableEntity { public void setCollarColor(IntEntityMetadata entityMetadata) { collarColor = (byte) entityMetadata.getPrimitiveValue(); + updateCollarColor(); + } + + private void updateCollarColor() { // Needed or else wild cats are a red color if (getFlag(EntityFlag.TAMED)) { dirtyMetadata.put(EntityDataTypes.COLOR, collarColor); @@ -108,8 +111,8 @@ public class CatEntity extends TameableEntity { } @Override - public boolean canEat(Item item) { - return item == Items.COD || item == Items.SALMON; + protected @Nullable Tag getFoodTag() { + return ItemTag.CAT_FOOD; } @NonNull @@ -135,4 +138,20 @@ public class CatEntity extends TameableEntity { return !canEat(itemInHand) || health >= maxHealth && tamed ? InteractionResult.PASS : InteractionResult.SUCCESS; } } + + // Ordered by bedrock id + // TODO: are these ordered correctly? + public enum BuiltInVariant implements BuiltIn { + WHITE, + BLACK, + RED, + SIAMESE, + BRITISH_SHORTHAIR, + CALICO, + PERSIAN, + RAGDOLL, + TABBY, + ALL_BLACK, + JELLIE + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java index 4c4b6a222..ef6121456 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java @@ -25,49 +25,48 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import java.util.Set; import java.util.UUID; public class ParrotEntity extends TameableEntity { - // Note: is the same as chicken. Reuse? - private static final Set TAMING_FOOD = Set.of(Items.WHEAT_SEEDS, Items.MELON_SEEDS, Items.PUMPKIN_SEEDS, Items.BEETROOT_SEEDS); - public ParrotEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @Override - public boolean canEat(Item item) { - return false; + @Nullable + protected Tag getFoodTag() { + return null; } - private boolean isTameFood(Item item) { - return TAMING_FOOD.contains(item); + private boolean isTameFood(GeyserItemStack item) { + return item.is(session, ItemTag.PARROT_FOOD); } - private boolean isPoisonousFood(Item item) { - return item == Items.COOKIE; + private boolean isPoisonousFood(GeyserItemStack item) { + return item.is(session, ItemTag.PARROT_POISONOUS_FOOD); } @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { boolean tame = getFlag(EntityFlag.TAMED); - if (!tame && isTameFood(itemInHand.asItem())) { + if (!tame && isTameFood(itemInHand)) { return InteractiveTag.FEED; - } else if (isPoisonousFood(itemInHand.asItem())) { + } else if (isPoisonousFood(itemInHand)) { return InteractiveTag.FEED; } else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { // Sitting/standing @@ -80,9 +79,9 @@ public class ParrotEntity extends TameableEntity { @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { boolean tame = getFlag(EntityFlag.TAMED); - if (!tame && isTameFood(itemInHand.asItem())) { + if (!tame && isTameFood(itemInHand)) { return InteractionResult.SUCCESS; - } else if (isPoisonousFood(itemInHand.asItem())) { + } else if (isPoisonousFood(itemInHand)) { return InteractionResult.SUCCESS; } else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { // Sitting/standing diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java index 5fc8c459d..a24eb1755 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java @@ -25,21 +25,21 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.Optional; import java.util.UUID; -public class TameableEntity extends AnimalEntity { +public abstract class TameableEntity extends AnimalEntity { /** * Used in the interactive tag manager to track if the session player owns this entity */ @@ -84,7 +84,7 @@ public class TameableEntity extends AnimalEntity { } @Override - protected boolean canBeLeashed() { - return isNotLeashed(); + public boolean canBeLeashed() { + return true; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index 0f5b36ec3..eee7a0052 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -25,36 +25,60 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.StringEnumProperty; +import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.enchantment.EnchantmentComponent; import org.geysermc.geyser.item.type.DyeItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; -import java.util.Set; +import java.util.Collections; +import java.util.List; import java.util.UUID; -public class WolfEntity extends TameableEntity { - /** - * A list of all foods a wolf can eat on Java Edition. - * Used to display interactive tag or particles if needed. - * TODO generate - */ - private static final Set WOLF_FOODS = Set.of(Items.PUFFERFISH, Items.TROPICAL_FISH, Items.CHICKEN, Items.COOKED_CHICKEN, - Items.PORKCHOP, Items.BEEF, Items.RABBIT, Items.COOKED_PORKCHOP, Items.COOKED_BEEF, Items.ROTTEN_FLESH, Items.MUTTON, Items.COOKED_MUTTON, - Items.COOKED_RABBIT); +public class WolfEntity extends TameableEntity implements VariantIntHolder { - private byte collarColor; + public static final StringEnumProperty SOUND_VARIANT = new StringEnumProperty( + IdentifierImpl.of("sound_variant"), + List.of( + "default", + "big", + "cute", + "grumpy", + "mad", + "puglin", + "sad" + ), + null + ); + + private byte collarColor = 14; // Red - default + private HolderSet repairableItems = null; + private boolean isCurseOfBinding = false; public WolfEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -64,19 +88,27 @@ public class WolfEntity extends TameableEntity { public void setTameableFlags(ByteEntityMetadata entityMetadata) { super.setTameableFlags(entityMetadata); // Reset wolf color - byte xd = entityMetadata.getPrimitiveValue(); - boolean angry = (xd & 0x02) == 0x02; - if (angry) { + if (getFlag(EntityFlag.ANGRY)) { dirtyMetadata.put(EntityDataTypes.COLOR, (byte) 0); + } else if (getFlag(EntityFlag.TAMED)) { + updateCollarColor(); + + // This fixes tail angle when taming + UpdateAttributesPacket packet = new UpdateAttributesPacket(); + packet.setRuntimeEntityId(geyserId); + packet.setAttributes(Collections.singletonList(createHealthAttribute())); + session.sendUpstreamPacket(packet); } } public void setCollarColor(IntEntityMetadata entityMetadata) { collarColor = (byte) entityMetadata.getPrimitiveValue(); - if (getFlag(EntityFlag.ANGRY)) { - return; + if (!getFlag(EntityFlag.ANGRY) && getFlag(EntityFlag.TAMED)) { + updateCollarColor(); } + } + private void updateCollarColor() { dirtyMetadata.put(EntityDataTypes.COLOR, collarColor); if (ownerBedrockId == 0) { // If a color is set and there is no owner entity ID, set one. @@ -93,14 +125,31 @@ public class WolfEntity extends TameableEntity { } @Override - public boolean canEat(Item item) { - // Cannot be a baby to eat these foods - return WOLF_FOODS.contains(item) && !isBaby(); + public JavaRegistryKey variantRegistry() { + return JavaRegistries.WOLF_VARIANT; } @Override - protected boolean canBeLeashed() { - return !getFlag(EntityFlag.ANGRY) && super.canBeLeashed(); + public void setBedrockVariantId(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); + } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.WOLF_FOOD; + } + + @Override + public void setBody(GeyserItemStack stack) { + super.setBody(stack); + isCurseOfBinding = ItemUtils.hasEffect(session, stack, EnchantmentComponent.PREVENT_ARMOR_CHANGE); + repairableItems = stack.getComponent(DataComponentTypes.REPAIRABLE); + } + + @Override + public boolean canBeLeashed() { + return !getFlag(EntityFlag.ANGRY); } @NonNull @@ -109,19 +158,32 @@ public class WolfEntity extends TameableEntity { if (getFlag(EntityFlag.ANGRY)) { return InteractiveTag.NONE; } - if (itemInHand.asItem() == Items.BONE && !getFlag(EntityFlag.TAMED)) { + if (itemInHand.is(Items.BONE) && !getFlag(EntityFlag.TAMED)) { // Bone and untamed - can tame return InteractiveTag.TAME; - } else { - if (itemInHand.asItem() instanceof DyeItem item) { + } + if (getFlag(EntityFlag.TAMED) && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { + if (itemInHand.asItem() instanceof DyeItem dyeItem) { // If this fails, as of Java Edition 1.18.1, you cannot toggle sit/stand - if (item.dyeColor() != this.collarColor) { + if (dyeItem.dyeColor() != this.collarColor) { return InteractiveTag.DYE; + } else { + return super.testMobInteraction(hand, itemInHand); } - } else if (getFlag(EntityFlag.TAMED) && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - return getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; } + if (itemInHand.is(Items.WOLF_ARMOR) && !getItemInSlot(EquipmentSlot.BODY).isEmpty() && !getFlag(EntityFlag.BABY)) { + return InteractiveTag.EQUIP_WOLF_ARMOR; + } + if (itemInHand.is(Items.SHEARS) && !getItemInSlot(EquipmentSlot.BODY).isEmpty() + && (!isCurseOfBinding || session.getGameMode().equals(GameMode.CREATIVE))) { + return InteractiveTag.REMOVE_WOLF_ARMOR; + } + if (getFlag(EntityFlag.SITTING) && itemInHand.is(session, repairableItems) && + !getItemInSlot(EquipmentSlot.BODY).isEmpty() && getItemInSlot(EquipmentSlot.BODY).isDamaged()) { + return InteractiveTag.REPAIR_WOLF_ARMOR; + } + // Tamed and owned by player - can sit/stand + return getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; } return super.testMobInteraction(hand, itemInHand); } @@ -130,11 +192,24 @@ public class WolfEntity extends TameableEntity { @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { if (ownerBedrockId == session.getPlayerEntity().getGeyserId() || getFlag(EntityFlag.TAMED) - || itemInHand.asItem() == Items.BONE && !getFlag(EntityFlag.ANGRY)) { + || itemInHand.is(Items.BONE) && !getFlag(EntityFlag.ANGRY)) { // Sitting toggle or feeding; not angry return InteractionResult.CONSUME; } else { return InteractionResult.PASS; } } + + // Ordered by bedrock id + public enum BuiltInVariant implements BuiltIn { + PALE, + ASHEN, + BLACK, + CHESTNUT, + RUSTY, + SNOWY, + SPOTTED, + STRIPED, + WOODS + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java index c7b29130f..ecc517489 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living.merchant; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -37,6 +36,7 @@ import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -47,14 +47,14 @@ public class AbstractMerchantEntity extends AgeableEntity { } @Override - protected boolean canBeLeashed() { + public boolean canBeLeashed() { return false; } @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() != Items.VILLAGER_SPAWN_EGG + if (!itemInHand.is(Items.VILLAGER_SPAWN_EGG) && (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING) && ((VillagerEntity) this).isCanTradeWith())) { // An additional check we know cannot work if (!isBaby()) { @@ -67,7 +67,7 @@ public class AbstractMerchantEntity extends AgeableEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() != Items.VILLAGER_SPAWN_EGG + if (!itemInHand.is(Items.VILLAGER_SPAWN_EGG) && (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING)) && (definition != EntityDefinitions.WANDERING_TRADER || !getFlag(EntityFlag.BABY))) { // Trading time diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java index 9b0f50050..d7efa9f1d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.entity.type.living.merchant; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData; import lombok.Getter; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; @@ -35,9 +33,12 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.registry.BlockRegistries; -import org.geysermc.geyser.registry.type.BlockMapping; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BedBlock; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.VillagerData; import java.util.Optional; import java.util.UUID; @@ -119,28 +120,31 @@ public class VillagerEntity extends AbstractMerchantEntity { } // The bed block - int blockId = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition); - String fullIdentifier = BlockRegistries.JAVA_BLOCKS.getOrDefault(blockId, BlockMapping.DEFAULT).getJavaIdentifier(); + BlockState state = session.getGeyser().getWorldManager().blockAt(session, bedPosition); // Set the correct position offset and rotation when sleeping int bedRotation = 0; float xOffset = 0; float zOffset = 0; - if (fullIdentifier.contains("facing=south")) { - // bed is facing south - bedRotation = 180; - zOffset = -.5f; - } else if (fullIdentifier.contains("facing=east")) { - // bed is facing east - bedRotation = 90; - xOffset = -.5f; - } else if (fullIdentifier.contains("facing=west")) { - // bed is facing west - bedRotation = 270; - xOffset = .5f; - } else if (fullIdentifier.contains("facing=north")) { - // rotation does not change because north is 0 - zOffset = .5f; + if (state.block() instanceof BedBlock) { + switch (state.getValue(Properties.HORIZONTAL_FACING)) { + case SOUTH -> { + bedRotation = 180; + zOffset = -.5f; + } + case EAST -> { + bedRotation = 90; + xOffset = -.5f; + } + case WEST -> { + bedRotation = 270; + xOffset = .5f; + } + case NORTH -> { + // rotation does not change because north is 0 + zOffset = .5f; + } + } } setYaw(yaw); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java index 04b3bba1b..16b5cc01c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java @@ -25,11 +25,13 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; @@ -45,5 +47,16 @@ public class AbstractSkeletonEntity extends MonsterEntity { byte xd = entityMetadata.getPrimitiveValue(); // A bit of a loophole so the hands get raised - set the target ID to its own ID dirtyMetadata.put(EntityDataTypes.TARGET_EID, ((xd & 4) == 4) ? geyserId : 0); + + if ((xd & 4) == 4) { + setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, isHolding(Items.BOW)); + } else { + setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, false); + } + } + + @Override + public boolean useArmSwingAttack() { + return true; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java index 5f2647b7a..9258cd3b8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java @@ -25,11 +25,14 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import java.util.UUID; @@ -38,6 +41,16 @@ public class BasePiglinEntity extends MonsterEntity { public BasePiglinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + // Both TARGET_EID and BLOCK are needed for melee attack animation + dirtyMetadata.put(EntityDataTypes.BLOCK, session.getBlockMappings().getDefinition(1)); + setFlag(EntityFlag.SHAKING, isShaking()); + } + + @Override + public void setMobFlags(ByteEntityMetadata entityMetadata) { + super.setMobFlags(entityMetadata); + byte xd = entityMetadata.getPrimitiveValue(); + dirtyMetadata.put(EntityDataTypes.TARGET_EID, (xd & 4) == 4 ? session.getPlayerEntity().getGeyserId() : 0); } public void setImmuneToZombification(BooleanEntityMetadata entityMetadata) { @@ -50,4 +63,9 @@ public class BasePiglinEntity extends MonsterEntity { protected boolean isShaking() { return (!isImmuneToZombification && !session.getDimensionType().piglinSafe()) || super.isShaking(); } + + @Override + public boolean useArmSwingAttack() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BlazeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BlazeEntity.java index 43d78f468..5b26d7bd1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BlazeEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BlazeEntity.java @@ -25,11 +25,11 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java new file mode 100644 index 000000000..c4d8eeff3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.monster; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; + +import java.util.UUID; + +public class BoggedEntity extends AbstractSkeletonEntity { + private boolean sheared = false; + + public BoggedEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + public void setSheared(BooleanEntityMetadata entityMetadata) { + this.sheared = entityMetadata.getPrimitiveValue(); + setFlag(EntityFlag.SHEARED, this.sheared); + } + + @Override + protected @NonNull InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (itemInHand.is(Items.SHEARS) && readyForShearing()) { + return InteractiveTag.SHEAR; + } + return super.testMobInteraction(hand, itemInHand); + } + + @Override + protected @NonNull InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (itemInHand.is(Items.SHEARS) && readyForShearing()) { + return InteractionResult.SUCCESS; + } + return super.mobInteract(hand, itemInHand); + } + + private boolean readyForShearing() { + return !this.sheared && this.isAlive(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BreezeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BreezeEntity.java new file mode 100644 index 000000000..251a77fb9 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BreezeEntity.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.monster; + +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; + +import java.util.UUID; + +public class BreezeEntity extends MonsterEntity { + public BreezeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public void setPose(Pose pose) { + // TODO Test + setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, pose == Pose.SHOOTING); + setFlag(EntityFlag.JUMP_GOAL_JUMP, pose == Pose.INHALING); + super.setPose(pose); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java new file mode 100644 index 000000000..dc9755a2b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.monster; + +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.entity.properties.type.IntProperty; +import org.geysermc.geyser.impl.IdentifierImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; + +import java.util.Optional; +import java.util.UUID; + +public class CreakingEntity extends MonsterEntity { + + public static final EnumProperty STATE_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("creaking_state"), + CreakingState.class, + CreakingState.NEUTRAL + ); + + // also, the creaking seems to have this minecraft:creaking_swaying_ticks thingy + // which i guess is responsible for some animation? + // it's sent over the network, all 6 "stages" 50ms in between of each other. + // no clue what it's used for tbh, so i'm not gonna bother implementing it + // - chris + // update: this still holds true, even a refactor later :( + public static final IntProperty SWAYING_TICKS_PROPERTY = new IntProperty( + IdentifierImpl.of("creaking_swaying_ticks"), + 6, 0, 0 + ); + + private Vector3i homePosition; + + public CreakingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + setFlag(EntityFlag.FIRE_IMMUNE, true); + } + + public void setCanMove(EntityMetadata> booleanEntityMetadata) { + setFlag(EntityFlag.BODY_ROTATION_BLOCKED, !booleanEntityMetadata.getValue()); + STATE_PROPERTY.apply(propertyManager, booleanEntityMetadata.getValue() ? CreakingState.HOSTILE_UNOBSERVED : CreakingState.HOSTILE_OBSERVED); + updateBedrockEntityProperties(); + } + + public void setActive(EntityMetadata> booleanEntityMetadata) { + if (!booleanEntityMetadata.getValue()) { + STATE_PROPERTY.apply(propertyManager, CreakingState.NEUTRAL); + } + } + + public void setIsTearingDown(EntityMetadata> booleanEntityMetadata) { + if (booleanEntityMetadata.getValue()) { + STATE_PROPERTY.apply(propertyManager, CreakingState.CRUMBLING); + updateBedrockEntityProperties(); + } + } + + public void setHomePos(EntityMetadata,? extends MetadataType>> optionalEntityMetadata) { + if (optionalEntityMetadata.getValue().isPresent()) { + this.homePosition = optionalEntityMetadata.getValue().get(); + } else { + this.homePosition = null; + } + } + + public void createParticleBeam() { + if (this.homePosition != null) { + LevelEventGenericPacket levelEventGenericPacket = new LevelEventGenericPacket(); + levelEventGenericPacket.setType(LevelEvent.PARTICLE_CREAKING_HEART_TRIAL); + levelEventGenericPacket.setTag( + NbtMap.builder() + .putInt("CreakingAmount", 20) + .putFloat("CreakingX", position.getX()) + .putFloat("CreakingY", position.getY()) + .putFloat("CreakingZ", position.getZ()) + .putInt("HeartAmount", 20) + .putFloat("HeartX", homePosition.getX()) + .putFloat("HeartY", homePosition.getY()) + .putFloat("HeartZ", homePosition.getZ()) + .build() + ); + + session.sendUpstreamPacket(levelEventGenericPacket); + } + } + + public enum CreakingState { + NEUTRAL, + HOSTILE_OBSERVED, + HOSTILE_UNOBSERVED, + TWITCHING, + CRUMBLING + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java index 33ea4e544..cc58aa7aa 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java @@ -25,9 +25,6 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; @@ -35,8 +32,12 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -65,7 +66,7 @@ public class CreeperEntity extends MonsterEntity { @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (session.getTagCache().isCreeperIgniter(itemInHand.asItem())) { + if (itemInHand.is(session, ItemTag.CREEPER_IGNITERS)) { return InteractiveTag.IGNITE_CREEPER; } else { return super.testMobInteraction(hand, itemInHand); @@ -75,7 +76,7 @@ public class CreeperEntity extends MonsterEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (session.getTagCache().isCreeperIgniter(itemInHand.asItem())) { + if (itemInHand.is(session, ItemTag.CREEPER_IGNITERS)) { // Ignite creeper - as of 1.19.3 session.playSoundEvent(SoundEvent.IGNITE, position); return InteractionResult.SUCCESS; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java index bb09a23f4..04044fcb4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import lombok.Data; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.ParticleType; @@ -42,6 +40,8 @@ import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.living.MobEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.DimensionUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.Optional; import java.util.Random; @@ -148,11 +148,11 @@ public class EnderDragonEntity extends MobEntity implements Tickable { } @Override - public boolean despawnEntity() { + public void despawnEntity() { for (EnderDragonPartEntity part : allParts) { part.despawnEntity(); } - return super.despawnEntity(); + super.despawnEntity(); } @Override @@ -264,7 +264,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable { // so we need to manually spawn particles for (int i = 0; i < 8; i++) { SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket(); - spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); + spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session)); spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f)); spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire"); spawnParticleEffectPacket.setMolangVariablesJson(Optional.empty()); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java index 5b8e23f8b..94ff657d2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java @@ -25,16 +25,16 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -57,7 +57,7 @@ public class EndermanEntity extends MonsterEntity { //TODO see if Bedrock controls this differently // Java Edition this controls which ambient sound is used if (entityMetadata.getPrimitiveValue()) { - LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + LevelSoundEventPacket packet = new LevelSoundEventPacket(); packet.setSound(SoundEvent.STARE); packet.setPosition(this.position); packet.setExtraData(-1); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java index f7b9d17b8..984aab642 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.FlyingEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GiantEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GiantEntity.java index e98c8f120..6bef3ae3e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GiantEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GiantEntity.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.entity.type.living.monster; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; @@ -36,7 +35,11 @@ public class GiantEntity extends MonsterEntity { public GiantEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } - dirtyMetadata.put(EntityDataTypes.SCALE, 6f); + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + setScale(6f); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GuardianEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GuardianEntity.java index 92e50d207..40793522e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GuardianEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GuardianEntity.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java index 915e34e79..18b7f6ae1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.FlyingEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -46,7 +45,7 @@ public class PhantomEntity extends FlyingEntity { setBoundingBoxWidth(boundsScale * definition.width()); setBoundingBoxHeight(boundsScale * definition.height()); - dirtyMetadata.put(EntityDataTypes.SCALE, modelScale); + setScale(modelScale); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java index 450c546ec..b35e6a17f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java @@ -25,18 +25,23 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; +import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; +import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import java.util.UUID; @@ -48,14 +53,16 @@ public class PiglinEntity extends BasePiglinEntity { public void setBaby(BooleanEntityMetadata entityMetadata) { boolean isBaby = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.SCALE, isBaby? .55f : 1f); + setScale(isBaby? .55f : 1f); setFlag(EntityFlag.BABY, isBaby); updateMountOffset(); } public void setChargingCrossbow(BooleanEntityMetadata entityMetadata) { - setFlag(EntityFlag.CHARGING, entityMetadata.getPrimitiveValue()); + boolean charging = entityMetadata.getPrimitiveValue(); + setFlag(EntityFlag.CHARGING, charging); + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, charging ? (byte) 64 : (byte) 0); // TODO: gradually increase } public void setDancing(BooleanEntityMetadata entityMetadata) { @@ -63,12 +70,57 @@ public class PiglinEntity extends BasePiglinEntity { } @Override - public void updateOffHand(GeyserSession session) { + public void setHand(GeyserItemStack stack) { + boolean toCrossbow = stack != null && stack.is(Items.CROSSBOW); + + if (toCrossbow ^ getMainHandItem().is(Items.CROSSBOW)) { // If switching to/from crossbow + dirtyMetadata.put(EntityDataTypes.BLOCK, session.getBlockMappings().getDefinition(toCrossbow ? 0 : 1)); + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0); + setFlag(EntityFlag.CHARGED, false); + setFlag(EntityFlag.USING_ITEM, false); + updateBedrockMetadata(); + + if (!getMainHandItem().isEmpty()) { + MobEquipmentPacket mobEquipmentPacket = new MobEquipmentPacket(); + mobEquipmentPacket.setRuntimeEntityId(geyserId); + mobEquipmentPacket.setContainerId(ContainerId.INVENTORY); + mobEquipmentPacket.setInventorySlot(0); + mobEquipmentPacket.setHotbarSlot(-1); + mobEquipmentPacket.setItem(ItemData.AIR); + session.sendUpstreamPacket(mobEquipmentPacket); + } + } + + super.setHand(stack); + } + + @Override + public void updateMainHand() { + super.updateMainHand(); + + if (getMainHandItem().is(Items.CROSSBOW)) { + if (getMainHandItem().getComponent(DataComponentTypes.CHARGED_PROJECTILES) != null) { + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE); + setFlag(EntityFlag.CHARGING, false); + setFlag(EntityFlag.CHARGED, true); + setFlag(EntityFlag.USING_ITEM, true); + } else if (getFlag(EntityFlag.CHARGED)) { + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0); + setFlag(EntityFlag.CHARGED, false); + setFlag(EntityFlag.USING_ITEM, false); + } + } + + updateBedrockMetadata(); + } + + @Override + public void updateOffHand() { // Check if the Piglin is holding Gold and set the ADMIRING flag accordingly so its pose updates - setFlag(EntityFlag.ADMIRING, session.getTagCache().shouldPiglinAdmire(session.getItemMappings().getMapping(this.offHand).getJavaItem())); + setFlag(EntityFlag.ADMIRING, getOffHandItem().is(session, ItemTag.PIGLIN_LOVED)); super.updateBedrockMetadata(); - super.updateOffHand(session); + super.updateOffHand(); } @NonNull @@ -94,6 +146,6 @@ public class PiglinEntity extends BasePiglinEntity { } private boolean canGiveGoldTo(@NonNull GeyserItemStack itemInHand) { - return !getFlag(EntityFlag.BABY) && itemInHand.asItem() == Items.GOLD_INGOT && !getFlag(EntityFlag.ADMIRING); + return !getFlag(EntityFlag.BABY) && itemInHand.is(Items.GOLD_INGOT) && !getFlag(EntityFlag.ADMIRING); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java index 27dd45f40..d8266f29d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.GolemEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; import java.util.UUID; @@ -52,6 +52,12 @@ public class ShulkerEntity extends GolemEntity { // As of 1.19.4, it seems Java no longer sends the shulker color if it's the default color on initial spawn // We still need the special case for 16 color in setShulkerColor though as it will send it for an entity metadata update dirtyMetadata.put(EntityDataTypes.VARIANT, 16); + + setFlag(EntityFlag.COLLIDABLE, true); + + // This is vanilla behaviour yes (BDS does this), without this as of 1.21.93 entity became fully invisible. + // Doing this allow the invisible parity support inside GeyserOptionalPack to works again. + setFlag(EntityFlag.RENDER_WHEN_INVISIBLE, true); } public void setAttachedFace(EntityMetadata entityMetadata) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SkeletonEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SkeletonEntity.java index da11b2759..a6343e256 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SkeletonEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SkeletonEntity.java @@ -25,11 +25,11 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SpiderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SpiderEntity.java index 03e234911..4a4527cef 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SpiderEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/SpiderEntity.java @@ -25,11 +25,11 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/VexEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/VexEntity.java index 56a0975ae..840f5b3b4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/VexEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/VexEntity.java @@ -25,11 +25,11 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java index 7a0c5e040..2341b8c32 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -36,6 +34,8 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WitherEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WitherEntity.java index 3abb7f122..19c1a457b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WitherEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WitherEntity.java @@ -25,13 +25,13 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java index 6e40573ba..e382777ff 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; @@ -38,12 +38,13 @@ public class ZoglinEntity extends MonsterEntity { public ZoglinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + dirtyMetadata.put(EntityDataTypes.TARGET_EID, session.getPlayerEntity().getGeyserId()); } public void setBaby(BooleanEntityMetadata entityMetadata) { boolean isBaby = entityMetadata.getPrimitiveValue(); if (isBaby != getFlag(EntityFlag.BABY)) { - dirtyMetadata.put(EntityDataTypes.SCALE, isBaby ? .55f : 1f); + setScale(isBaby ? .55f : 1f); setFlag(EntityFlag.BABY, isBaby); updatePassengerOffsets(); @@ -57,12 +58,17 @@ public class ZoglinEntity extends MonsterEntity { } @Override - protected boolean canBeLeashed() { - return isNotLeashed(); + public boolean canBeLeashed() { + return true; } @Override protected boolean isEnemy() { return true; } + + @Override + public boolean useArmSwingAttack() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieEntity.java index af6a30a10..b07afd742 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieEntity.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import java.util.UUID; @@ -43,7 +42,7 @@ public class ZombieEntity extends MonsterEntity { public void setZombieBaby(BooleanEntityMetadata entityMetadata) { boolean isBaby = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.SCALE, isBaby ? .55f : 1.0f); + setScale(isBaby ? .55f : 1.0f); setFlag(EntityFlag.BABY, isBaby); updateMountOffset(); @@ -58,4 +57,9 @@ public class ZombieEntity extends MonsterEntity { protected boolean isShaking() { return convertingToDrowned || super.isShaking(); } + + @Override + public boolean useArmSwingAttack() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java index 32e45507a..e09568102 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java @@ -25,10 +25,6 @@ package org.geysermc.geyser.entity.type.living.monster; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -40,6 +36,10 @@ import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.VillagerData; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -70,7 +70,7 @@ public class ZombieVillagerEntity extends ZombieEntity { @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() == Items.GOLDEN_APPLE) { + if (itemInHand.is(Items.GOLDEN_APPLE)) { return InteractiveTag.CURE; } else { return super.testMobInteraction(hand, itemInHand); @@ -80,7 +80,7 @@ public class ZombieVillagerEntity extends ZombieEntity { @NonNull @Override protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { - if (itemInHand.asItem() == Items.GOLDEN_APPLE) { + if (itemInHand.is(Items.GOLDEN_APPLE)) { // The client doesn't know if the entity has weakness as that's not usually sent over the network return InteractionResult.CONSUME; } else { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java index d2f8377d3..e73052286 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java @@ -26,10 +26,14 @@ package org.geysermc.geyser.entity.type.living.monster.raid; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import java.util.UUID; @@ -39,29 +43,49 @@ public class PillagerEntity extends AbstractIllagerEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void updateMainHand(GeyserSession session) { //TODO - checkForCrossbow(); - - super.updateMainHand(session); + public void setChargingCrossbow(BooleanEntityMetadata entityMetadata) { + boolean charging = entityMetadata.getPrimitiveValue(); + setFlag(EntityFlag.CHARGING, charging); + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, charging ? (byte) 64 : (byte) 0); // TODO: gradually increase } @Override - public void updateOffHand(GeyserSession session) { - checkForCrossbow(); + public void updateMainHand() { + updateCrossbow(); - super.updateOffHand(session); + super.updateMainHand(); + } + + @Override + public void updateOffHand() { + updateCrossbow(); + + super.updateOffHand(); } /** * Check for a crossbow in either the mainhand or offhand. If one exists, indicate that the pillager should be posing */ - protected void checkForCrossbow() { - ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow(); - boolean hasCrossbow = this.hand.getDefinition() == crossbow.getBedrockDefinition() - || this.offHand.getDefinition() == crossbow.getBedrockDefinition(); - setFlag(EntityFlag.USING_ITEM, hasCrossbow); - setFlag(EntityFlag.CHARGED, hasCrossbow); + protected void updateCrossbow() { + GeyserItemStack activeCrossbow = null; + if (getMainHandItem().is(Items.CROSSBOW)) { + activeCrossbow = getMainHandItem(); + } else if (getOffHandItem().is(Items.CROSSBOW)) { + activeCrossbow = getOffHandItem(); + } + + if (activeCrossbow != null) { + if (activeCrossbow.getComponent(DataComponentTypes.CHARGED_PROJECTILES) != null) { + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE); + setFlag(EntityFlag.CHARGING, false); + setFlag(EntityFlag.CHARGED, true); + setFlag(EntityFlag.USING_ITEM, true); + } else if (getFlag(EntityFlag.CHARGED)) { + dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0); + setFlag(EntityFlag.CHARGED, false); + setFlag(EntityFlag.USING_ITEM, false); + } + } updateBedrockMetadata(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/RavagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/RavagerEntity.java new file mode 100644 index 000000000..6190bae10 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/RavagerEntity.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.monster.raid; + +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class RavagerEntity extends RaidParticipantEntity { + + public RavagerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public boolean useArmSwingAttack() { + setFlag(EntityFlag.DELAYED_ATTACK, false); + updateBedrockMetadata(); + + session.scheduleInEventLoop(() -> { + setFlag(EntityFlag.DELAYED_ATTACK, true); + updateBedrockMetadata(); + }, 75, TimeUnit.MILLISECONDS); + + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/SpellcasterIllagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/SpellcasterIllagerEntity.java index f083437ae..8d4b3c44e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/SpellcasterIllagerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/SpellcasterIllagerEntity.java @@ -25,13 +25,13 @@ package org.geysermc.geyser.entity.type.living.monster.raid; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/VindicatorEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/VindicatorEntity.java index ad99dda50..a2557e75a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/VindicatorEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/VindicatorEntity.java @@ -25,11 +25,12 @@ package org.geysermc.geyser.entity.type.living.monster.raid; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import java.util.UUID; @@ -37,6 +38,7 @@ public class VindicatorEntity extends AbstractIllagerEntity { public VindicatorEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + dirtyMetadata.put(EntityDataTypes.TARGET_EID, session.getPlayerEntity().getGeyserId()); } @Override @@ -46,4 +48,9 @@ public class VindicatorEntity extends AbstractIllagerEntity { byte xd = entityMetadata.getPrimitiveValue(); setFlag(EntityFlag.ANGRY, (xd & 4) == 4); } + + @Override + public boolean useArmSwingAttack() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java new file mode 100644 index 000000000..f8bb7f658 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.player; + +import lombok.Getter; +import lombok.Setter; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.Ability; +import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; +import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; +import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; +import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.geyser.skin.SkullSkinManager; +import org.geysermc.geyser.translator.item.ItemTranslator; +import org.geysermc.geyser.util.ChunkUtils; +import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +public class AvatarEntity extends LivingEntity { + public static final float SNEAKING_POSE_HEIGHT = 1.5f; + protected static final List BASE_ABILITY_LAYER; + + @Getter + protected String username; + + /** + * The textures property from the GameProfile. + */ + @Getter + @Setter + @Nullable + protected String texturesProperty; // TODO no direct setter, rather one that updates the skin + + private String cachedScore = ""; + private boolean scoreVisible = true; + + @Getter + @Nullable + private Vector3i bedPosition; + + static { + AbilityLayer abilityLayer = new AbilityLayer(); + abilityLayer.setLayerType(AbilityLayer.Type.BASE); + Ability[] abilities = Ability.values(); + Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with + Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with + BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer); + } + + public AvatarEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, + Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + this.username = username; + this.nametag = username; + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior + dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, 0xff); + } + + @Override + public void spawnEntity() { + AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); + addPlayerPacket.setUuid(uuid); + addPlayerPacket.setUsername(username); + addPlayerPacket.setRuntimeEntityId(geyserId); + addPlayerPacket.setUniqueEntityId(geyserId); + addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0)); + addPlayerPacket.setRotation(getBedrockRotation()); + addPlayerPacket.setMotion(motion); + addPlayerPacket.setHand(ItemTranslator.translateToBedrock(session, getMainHandItem())); + addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); + addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); + addPlayerPacket.setDeviceId(""); + addPlayerPacket.setPlatformChatId(""); + addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO + addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing + addPlayerPacket.getMetadata().putFlags(flags); + dirtyMetadata.apply(addPlayerPacket.getMetadata()); + + setFlagsDirty(false); + + valid = true; + session.sendUpstreamPacket(addPlayerPacket); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + setPosition(position); + setYaw(yaw); + setPitch(pitch); + setHeadYaw(headYaw); + + setOnGround(isOnGround); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(geyserId); + movePlayerPacket.setPosition(this.position); + movePlayerPacket.setRotation(getBedrockRotation()); + movePlayerPacket.setOnGround(isOnGround); + movePlayerPacket.setMode(this instanceof SessionPlayerEntity || teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); + if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) { + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + } + + session.sendUpstreamPacket(movePlayerPacket); + + if (teleported && !(this instanceof SessionPlayerEntity)) { + // As of 1.19.0, head yaw seems to be ignored during teleports, also don't do this for session player. + updateHeadLookRotation(headYaw); + } + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + setYaw(yaw); + setPitch(pitch); + setHeadYaw(headYaw); + this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ); + + setOnGround(isOnGround); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(geyserId); + movePlayerPacket.setPosition(position); + movePlayerPacket.setRotation(getBedrockRotation()); + movePlayerPacket.setOnGround(isOnGround); + movePlayerPacket.setMode(this instanceof SessionPlayerEntity ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); + // If the player is moved while sleeping, we have to adjust their y, so it appears + // correctly on Bedrock. This fixes GSit's lay. + if (getFlag(EntityFlag.SLEEPING)) { + if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) { + // Force the player movement by using a teleport + movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ())); + movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); + } + } + + if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) { + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + } + + session.sendUpstreamPacket(movePlayerPacket); + } + + @Override + public void setPosition(Vector3f position) { + if (this.bedPosition != null) { + // As of Bedrock 1.21.22 and Fabric 1.21.1 + // Messes with Bedrock if we send this to the client itself, though. + super.setPosition(position.up(0.2f)); + } else { + super.setPosition(position.add(0, definition.offset(), 0)); + } + } + + @Override + public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) { + bedPosition = super.setBedPosition(entityMetadata); + if (bedPosition != null) { + // Required to sync position of entity to bed + // Fixes https://github.com/GeyserMC/Geyser/issues/3595 on vanilla 1.19.3 servers - did not happen on Paper + this.setPosition(bedPosition.toFloat()); + + // TODO evaluate if needed + int bed = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition); + // Bed has to be updated, or else player is floating in the air + ChunkUtils.updateBlock(session, bed, bedPosition); + + // Indicate that the player should enter the sleep cycle + // Has to be a byte or it does not work + // (Bed position is what actually triggers sleep - "pose" is only optional) + dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 2); + } else { + // Player is no longer sleeping + dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 0); + return null; + } + return bedPosition; + } + + public void setSkin(ResolvableProfile profile, boolean cape, Runnable after) { + SkinManager.resolveProfile(profile).thenAccept(resolved -> setSkin(resolved, cape, after)); + } + + public void setSkin(GameProfile profile, boolean cape, Runnable after) { + GameProfile.Property textures = profile.getProperty("textures"); + if (textures != null) { + setSkin(textures.getValue(), cape, after); + } else { + setSkin((String) null, cape, after); + } + } + + public void setSkin(String texturesProperty, boolean cape, Runnable after) { + if (Objects.equals(texturesProperty, this.texturesProperty)) { + return; + } + + this.texturesProperty = texturesProperty; + if (cape) { + SkinManager.requestAndHandleSkinAndCape(this, session, skin -> after.run()); + } else { + SkullSkinManager.requestAndHandleSkin(this, session, skin -> after.run()); + } + } + + public void setSkinVisibility(ByteEntityMetadata entityMetadata) { + // OptionalPack usage for toggling skin bits + // In Java Edition, a bit being set means that part should be enabled + // However, to ensure that the pack still works on other servers, we invert the bit so all values by default + // are true (0). + dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff); + } + + @Override + public String getDisplayName() { + return username; + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + // Doesn't do anything for players + if (!(this instanceof PlayerEntity)) { + super.setDisplayName(entityMetadata); + } + } + + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + // Doesn't do anything for players + if (!(this instanceof PlayerEntity)) { + super.setDisplayNameVisible(entityMetadata); + } + } + + public void setBelowNameText(String text) { + if (text == null) { + text = ""; + } + + boolean changed = !Objects.equals(cachedScore, text); + cachedScore = text; + if (scoreVisible && changed) { + dirtyMetadata.put(EntityDataTypes.SCORE, text); + } + } + + @Override + protected void scoreVisibility(boolean show) { + boolean visibilityChanged = scoreVisible != show; + scoreVisible = show; + if (!visibilityChanged) { + return; + } + // if the player has no cachedScore, we never have to change the score. + // hide = set to "" (does nothing), show = change from "" (does nothing) + if (cachedScore.isEmpty()) { + return; + } + dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : ""); + } + + @Override + public void setPose(Pose pose) { + super.setPose(pose); + setFlag(EntityFlag.SWIMMING, false); + setFlag(EntityFlag.CRAWLING, false); + + if (pose == Pose.SWIMMING) { + // This is just for, so we know if player is swimming or crawling. + // TODO test, changed from position (field) to position() (method), which adds offset + if (session.getGeyser().getWorldManager().blockAt(session, position.toInt()).is(Blocks.WATER)) { + setFlag(EntityFlag.SWIMMING, true); + } else { + setFlag(EntityFlag.CRAWLING, true); + // Look at https://github.com/GeyserMC/Geyser/issues/5316, we're fixing this by spoofing player pitch to 0. + updateRotation(this.yaw, 0, this.onGround); + } + } + } + + @Override + public void setPitch(float pitch) { + super.setPitch(getFlag(EntityFlag.CRAWLING) ? 0 : pitch); + } + + @Override + public void setDimensionsFromPose(Pose pose) { + float height; + float width; + switch (pose) { + case SNEAKING -> { + height = SNEAKING_POSE_HEIGHT; + width = definition.width(); + } + case FALL_FLYING, SPIN_ATTACK, SWIMMING -> { + height = 0.6f; + width = definition.width(); + } + case DYING -> { + height = 0.2f; + width = 0.2f; + } + default -> { + super.setDimensionsFromPose(pose); + return; + } + } + setBoundingBoxWidth(width); + setBoundingBoxHeight(height); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java new file mode 100644 index 000000000..c45221a1a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.player; + +import net.kyori.adventure.text.Component; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile; + +import java.util.Optional; +import java.util.UUID; + +public class MannequinEntity extends AvatarEntity { + + public MannequinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw, ""); + } + + public void setProfile(EntityMetadata entityMetadata) { + setSkin(entityMetadata.getValue(), true, () -> {}); + } + + @Override + public String getDisplayName() { + return displayName; + } + + public void setDescription(EntityMetadata, ?> entityMetadata) { + // TODO + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index 9e3888138..4e1eb2dcb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -25,182 +25,92 @@ package org.geysermc.geyser.entity.type.player; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition; -import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import lombok.Getter; import lombok.Setter; -import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.math.vector.Vector3i; -import org.cloudburstmc.protocol.bedrock.data.Ability; -import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; -import org.cloudburstmc.protocol.bedrock.data.AttributeData; -import org.cloudburstmc.protocol.bedrock.data.GameType; -import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; -import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; -import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity; -import org.geysermc.geyser.scoreboard.Objective; -import org.geysermc.geyser.scoreboard.Score; -import org.geysermc.geyser.scoreboard.Team; -import org.geysermc.geyser.scoreboard.UpdateType; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.ChunkUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.OptionalInt; import java.util.UUID; import java.util.concurrent.TimeUnit; @Getter @Setter -public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { - public static final float SNEAKING_POSE_HEIGHT = 1.5f; - protected static final List BASE_ABILITY_LAYER; - - static { - AbilityLayer abilityLayer = new AbilityLayer(); - abilityLayer.setLayerType(AbilityLayer.Type.BASE); - Ability[] abilities = Ability.values(); - Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with - Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with - BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer); - } - - private String username; - - /** - * The textures property from the GameProfile. - */ - @Nullable - private String texturesProperty; - - @Nullable - private Vector3i bedPosition; +public class PlayerEntity extends AvatarEntity implements GeyserPlayerEntity { /** * Saves the parrot currently on the player's left shoulder; otherwise null */ - private ParrotEntity leftParrot; + private @Nullable ParrotEntity leftParrot; /** * Saves the parrot currently on the player's right shoulder; otherwise null */ - private ParrotEntity rightParrot; + private @Nullable ParrotEntity rightParrot; + + /** + * Whether this player is currently listed. + */ + private boolean listed = false; public PlayerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username, @Nullable String texturesProperty) { - super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw); - - this.username = username; - this.nametag = username; + super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw, username); this.texturesProperty = texturesProperty; } @Override protected void initializeMetadata() { super.initializeMetadata(); - // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior - dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, 0xff); - } - - @Override - public void spawnEntity() { - // Check to see if the player should have a belowname counterpart added - Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME); - if (objective != null) { - setBelowNameText(objective); - } - - // Update in case this entity has been despawned, then respawned - this.nametag = this.username; - // The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now - updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username)); - - AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); - addPlayerPacket.setUuid(uuid); - addPlayerPacket.setUsername(username); - addPlayerPacket.setRuntimeEntityId(geyserId); - addPlayerPacket.setUniqueEntityId(geyserId); - addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0)); - addPlayerPacket.setRotation(getBedrockRotation()); - addPlayerPacket.setMotion(motion); - addPlayerPacket.setHand(hand); - addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); - addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); - addPlayerPacket.setDeviceId(""); - addPlayerPacket.setPlatformChatId(""); - addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO - addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing - addPlayerPacket.getMetadata().putFlags(flags); // Since 1.20.60, the nametag does not show properly if this is not set :/ // The nametag does disappear properly when the player is invisible though. dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); - dirtyMetadata.apply(addPlayerPacket.getMetadata()); + } - setFlagsDirty(false); + @Override + public void despawnEntity() { + super.despawnEntity(); - valid = true; - session.sendUpstreamPacket(addPlayerPacket); + // Since we re-use player entities: Clear flags, held item, etc + this.resetMetadata(); + this.nametag = username; + + this.equipment.clear(); + } + + public void resetMetadata() { + // Reset all metadata to their default values + // This is used when a player respawns + this.flags.clear(); + this.initializeMetadata(); + + // Explicitly reset all metadata not handled by initializeMetadata + setParrot(OptionalInt.empty(), true); + setParrot(OptionalInt.empty(), false); } public void sendPlayer() { if (session.getEntityCache().getPlayerEntity(uuid) == null) return; - if (session.getEntityCache().getEntityByGeyserId(geyserId) == null) { - session.getEntityCache().spawnEntity(this); - } else { - spawnEntity(); - } + session.getEntityCache().spawnEntity(this); } @Override public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { - setPosition(position); - setYaw(yaw); - setPitch(pitch); - setHeadYaw(headYaw); - - setOnGround(isOnGround); - - MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); - movePlayerPacket.setRuntimeEntityId(geyserId); - movePlayerPacket.setPosition(this.position); - movePlayerPacket.setRotation(getBedrockRotation()); - movePlayerPacket.setOnGround(isOnGround); - movePlayerPacket.setMode(teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); - - if (teleported) { - movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.UNKNOWN); - } - - session.sendUpstreamPacket(movePlayerPacket); - - if (teleported) { - // As of 1.19.0, head yaw seems to be ignored during teleports. - updateHeadLookRotation(headYaw); - } - + super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); if (leftParrot != null) { leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported); } @@ -211,30 +121,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { @Override public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { - setYaw(yaw); - setPitch(pitch); - setHeadYaw(headYaw); - this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ); - - setOnGround(isOnGround); - - MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); - movePlayerPacket.setRuntimeEntityId(geyserId); - movePlayerPacket.setPosition(position); - movePlayerPacket.setRotation(getBedrockRotation()); - movePlayerPacket.setOnGround(isOnGround); - movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL); - // If the player is moved while sleeping, we have to adjust their y, so it appears - // correctly on Bedrock. This fixes GSit's lay. - if (getFlag(EntityFlag.SLEEPING)) { - if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) { - // Force the player movement by using a teleport - movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ())); - movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); - movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.UNKNOWN); - } - } - session.sendUpstreamPacket(movePlayerPacket); + super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); if (leftParrot != null) { leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true); } @@ -243,63 +130,21 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { } } - public void updateRotation(float yaw, float pitch, float headYaw, boolean isOnGround) { - moveRelative(0, 0, 0, yaw, pitch, headYaw, isOnGround); - } - - @Override - public void setPosition(Vector3f position) { - super.setPosition(position.add(0, definition.offset(), 0)); - } - - @Override - public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) { - bedPosition = super.setBedPosition(entityMetadata); - if (bedPosition != null) { - // Required to sync position of entity to bed - // Fixes https://github.com/GeyserMC/Geyser/issues/3595 on vanilla 1.19.3 servers - did not happen on Paper - this.setPosition(bedPosition.toFloat()); - - // TODO evaluate if needed - int bed = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition); - // Bed has to be updated, or else player is floating in the air - ChunkUtils.updateBlock(session, bed, bedPosition); - - // Indicate that the player should enter the sleep cycle - // Has to be a byte or it does not work - // (Bed position is what actually triggers sleep - "pose" is only optional) - dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 2); - } else { - // Player is no longer sleeping - dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 0); - return null; - } - return bedPosition; - } - public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) { // Extra hearts - is not metadata but an attribute on Bedrock UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); attributesPacket.setRuntimeEntityId(geyserId); // Setting to a higher maximum since plugins/datapacks can probably extend the Bedrock soft limit attributesPacket.setAttributes(Collections.singletonList( - new AttributeData("minecraft:absorption", 0.0f, 1024f, entityMetadata.getPrimitiveValue(), 0.0f))); + GeyserAttributeType.ABSORPTION.getAttribute(entityMetadata.getPrimitiveValue()))); session.sendUpstreamPacket(attributesPacket); } - public void setSkinVisibility(ByteEntityMetadata entityMetadata) { - // OptionalPack usage for toggling skin bits - // In Java Edition, a bit being set means that part should be enabled - // However, to ensure that the pack still works on other servers, we invert the bit so all values by default - // are true (0). - dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff); - } - - public void setLeftParrot(EntityMetadata entityMetadata) { + public void setLeftParrot(EntityMetadata entityMetadata) { setParrot(entityMetadata.getValue(), true); } - public void setRightParrot(EntityMetadata entityMetadata) { + public void setRightParrot(EntityMetadata entityMetadata) { setParrot(entityMetadata.getValue(), false); } @@ -307,17 +152,17 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { * Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just * spawns it from the NBT data provided */ - private void setParrot(CompoundTag tag, boolean isLeft) { - if (tag != null && !tag.isEmpty()) { + protected void setParrot(OptionalInt variant, boolean isLeft) { + if (variant.isPresent()) { if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) { // No need to update a parrot's data when it already exists return; } - // The parrot is a separate entity in Bedrock, but part of the player entity in Java //TODO is a UUID provided in NBT? + // The parrot is a separate entity in Bedrock, but part of the player entity in Java ParrotEntity parrot = new ParrotEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(), null, EntityDefinitions.PARROT, position, motion, getYaw(), getPitch(), getHeadYaw()); parrot.spawnEntity(); - parrot.getDirtyMetadata().put(EntityDataTypes.VARIANT, (Integer) tag.get("Variant").getValue()); + parrot.getDirtyMetadata().put(EntityDataTypes.VARIANT, variant.getAsInt()); // Different position whether the parrot is left or right float offset = isLeft ? 0.4f : -0.4f; parrot.getDirtyMetadata().put(EntityDataTypes.SEAT_OFFSET, Vector3f.from(offset, -0.22, -0.1)); @@ -325,7 +170,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { parrot.updateBedrockMetadata(); SetEntityLinkPacket linkPacket = new SetEntityLinkPacket(); EntityLinkData.Type type = isLeft ? EntityLinkData.Type.RIDER : EntityLinkData.Type.PASSENGER; - linkPacket.setEntityLink(new EntityLinkData(geyserId, parrot.getGeyserId(), type, false, false)); + linkPacket.setEntityLink(new EntityLinkData(geyserId, parrot.getGeyserId(), type, false, false, 0f)); // Delay, or else spawned-in players won't get the link // TODO: Find a better solution. session.scheduleInEventLoop(() -> session.sendUpstreamPacket(linkPacket), 500, TimeUnit.MILLISECONDS); @@ -348,95 +193,24 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { } @Override - public void setDisplayName(EntityMetadata, ?> entityMetadata) { - // Doesn't do anything for players - } - - //todo this will become common entity logic once UUID support is implemented for them - public void updateDisplayName(@Nullable Team team) { - boolean needsUpdate; - if (team != null) { - String newDisplayName; - if (team.isVisibleFor(session.getPlayerEntity().getUsername())) { - TeamColor color = team.getColor(); - String chatColor = MessageTranslator.toChatColor(color); - // We have to emulate what modern Java text already does for us and add the color to each section - String prefix = team.getCurrentData().getPrefix(); - String suffix = team.getCurrentData().getSuffix(); - newDisplayName = chatColor + prefix + chatColor + this.username + chatColor + suffix; - } else { - // The name is not visible to the session player; clear name - newDisplayName = ""; - } - needsUpdate = !newDisplayName.equals(this.nametag); - this.nametag = newDisplayName; - } else { - // The name has reset, if it was previously something else - needsUpdate = !this.nametag.equals(this.username); - this.nametag = this.username; - } - - if (needsUpdate) { - dirtyMetadata.put(EntityDataTypes.NAME, this.nametag); - } + public String teamIdentifier() { + return username; } + // TODO test mannequins @Override - public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { - // Doesn't do anything for players + protected void setNametag(@Nullable String nametag, boolean fromDisplayName) { + // when fromDisplayName, LivingEntity will call scoreboard code. After that + // setNametag is called again with fromDisplayName on false + if (nametag == null && !fromDisplayName) { + // nametag = null means reset, so reset it back to username + nametag = username; + } + super.setNametag(nametag, fromDisplayName); } - @Override - protected void setDimensions(Pose pose) { - float height; - float width; - switch (pose) { - case SNEAKING -> { - height = SNEAKING_POSE_HEIGHT; - width = definition.width(); - } - case FALL_FLYING, SPIN_ATTACK, SWIMMING -> { - height = 0.6f; - width = definition.width(); - } - case DYING -> { - height = 0.2f; - width = 0.2f; - } - default -> { - super.setDimensions(pose); - return; - } - } - setBoundingBoxWidth(width); - setBoundingBoxHeight(height); - } - - public void setBelowNameText(Objective objective) { - if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) { - int amount; - Score score = objective.getScores().get(username); - if (score != null) { - amount = score.getCurrentData().getScore(); - } else { - amount = 0; - } - String displayString = amount + " " + objective.getDisplayName(); - - if (valid) { - // Already spawned - we still need to run the rest of this code because the spawn packet will be - // providing the information - SetEntityDataPacket packet = new SetEntityDataPacket(); - packet.setRuntimeEntityId(geyserId); - packet.getMetadata().put(EntityDataTypes.SCORE, displayString); - session.sendUpstreamPacket(packet); - } - } else if (valid) { - SetEntityDataPacket packet = new SetEntityDataPacket(); - packet.setRuntimeEntityId(geyserId); - packet.getMetadata().put(EntityDataTypes.SCORE, ""); - session.sendUpstreamPacket(packet); - } + public void setUsername(String username) { + this.username = username; } /** @@ -448,6 +222,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { @Override public Vector3f position() { - return this.position.clone(); + return this.position.down(definition.offset()); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index 751a24871..a60dd0b78 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -25,25 +25,46 @@ package org.geysermc.geyser.entity.type.player; -import com.github.steveice10.mc.protocol.data.game.entity.attribute.Attribute; -import com.github.steveice10.mc.protocol.data.game.entity.attribute.AttributeType; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.GlobalPos; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; +import lombok.Setter; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.type.BoatEntity; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.block.type.TrapDoorBlock; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.BlockTag; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.DimensionUtils; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.GlobalPos; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import java.util.Collections; import java.util.List; @@ -59,15 +80,54 @@ public class SessionPlayerEntity extends PlayerEntity { */ @Getter protected final Map attributes = new Object2ObjectOpenHashMap<>(); + /** - * Whether to check for updated speed after all entity metadata has been processed + * Java-only attributes */ - private boolean refreshSpeed = false; + @Getter + private double blockInteractionRange = GeyserAttributeType.BLOCK_INTERACTION_RANGE.getDefaultValue(); + @Getter + private double miningEfficiency = GeyserAttributeType.MINING_EFFICIENCY.getDefaultValue(); + @Getter + private double blockBreakSpeed = GeyserAttributeType.BLOCK_BREAK_SPEED.getDefaultValue(); + @Getter + private double submergedMiningSpeed = GeyserAttributeType.SUBMERGED_MINING_SPEED.getDefaultValue(); + /** * Used in PlayerInputTranslator for movement checks. */ @Getter private boolean isRidingInFront; + /** + * Used when emulating client-side vehicles + */ + @Getter + private Vector2f vehicleInput = Vector2f.ZERO; + /** + * Used when emulating client-side vehicles + */ + @Getter + private int vehicleJumpStrength; + + private int lastAirSupply = getMaxAir(); + + @Getter @Setter + private boolean insideScaffolding = false; + + /** + * The client last tick end velocity, used for calculating player onGround. + */ + @Getter @Setter + private Vector3f lastTickEndVelocity = Vector3f.ZERO; + + /** + * The client claimed interact rotation, intended for touch (pocket) user. + */ + @Getter @Setter + private Vector2f bedrockInteractRotation = Vector2f.ZERO; + + @Getter @Setter + private float javaYaw; public SessionPlayerEntity(GeyserSession session) { super(session, -1, 1, null, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, null, null); @@ -75,6 +135,14 @@ public class SessionPlayerEntity extends PlayerEntity { valid = true; } + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + + // This allows player to be slowly push towards the closet space when stuck inside block instead of instantly moved out. + setFlag(EntityFlag.PUSH_TOWARDS_CLOSEST_SPACE, true); + } + @Override protected void setClientSideSilent() { // Do nothing, since we want the session player to hear their own footstep sounds for example. @@ -85,6 +153,12 @@ public class SessionPlayerEntity extends PlayerEntity { // Already logged in } + @Override + public void setYaw(float yaw) { + super.setYaw(yaw); + this.javaYaw = yaw; + } + @Override public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); @@ -95,19 +169,57 @@ public class SessionPlayerEntity extends PlayerEntity { public void setPosition(Vector3f position) { if (valid) { // Don't update during session init session.getCollisionManager().updatePlayerBoundingBox(position); + + if (session.isNoClip() && position.getY() >= session.getBedrockDimension().minY() - 5) { + session.setNoClip(false); + } } - super.setPosition(position); + this.position = position.add(0, definition.offset(), 0); + } + + /** + * Special method used only when updating the session player's rotation. + * For some reason, Mode#NORMAL ignored rotation. Yay. + * @param yaw the new yaw + * @param pitch the new pitch + * @param headYaw the head yaw + */ + public void updateOwnRotation(float yaw, float pitch, float headYaw) { + setYaw(yaw); + setPitch(pitch); + setHeadYaw(headYaw); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(geyserId); + movePlayerPacket.setPosition(position); + movePlayerPacket.setRotation(getBedrockRotation()); + movePlayerPacket.setOnGround(isOnGround()); + movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + + session.sendUpstreamPacket(movePlayerPacket); + + // We're just setting rotation, player shouldn't lose motion, send motion packet to account for that. + SetEntityMotionPacket entityMotionPacket = new SetEntityMotionPacket(); + entityMotionPacket.setRuntimeEntityId(geyserId); + entityMotionPacket.setMotion(motion); + session.sendUpstreamPacket(entityMotionPacket); } /** * Set the player's position without applying an offset or moving the bounding box - * This is used in BedrockMovePlayerTranslator which receives the player's position + * This is used in BedrockMovePlayer which receives the player's position * with the offset pre-applied * * @param position the new position of the Bedrock player */ public void setPositionManual(Vector3f position) { this.position = position; + + // Player is "above" the void so they're not supposed to no clip. + if (session.isNoClip() && position.getY() - EntityDefinitions.PLAYER.offset() >= session.getBedrockDimension().minY() - 5) { + session.setNoClip(false); + } } /** @@ -119,9 +231,27 @@ public class SessionPlayerEntity extends PlayerEntity { // TODO: proper fix, BDS somehow does it? https://paste.gg/p/anonymous/3adfb7612f1540be80fa03a2281f93dc (BDS 1.20.13) if (!this.session.getGameMode().equals(GameMode.SPECTATOR)) { super.setFlags(entityMetadata); - session.setSwimmingInWater((entityMetadata.getPrimitiveValue() & 0x10) == 0x10 && getFlag(EntityFlag.SPRINTING)); } - refreshSpeed = true; + } + + @Override + protected void setGliding(boolean value) { + session.setGliding(value); + } + + @Override + protected void setSneaking(boolean value) { + if (value) { + session.startSneaking(false); + } else { + session.setShouldSendSneak(false); + session.stopSneaking(false); + } + } + + @Override + protected void setSpinAttack(boolean value) { + session.setSpinAttack(value); } /** @@ -149,7 +279,6 @@ public class SessionPlayerEntity extends PlayerEntity { public void setPose(Pose pose) { super.setPose(pose); session.setPose(pose); - refreshSpeed = true; } public float getMaxHealth() { @@ -166,11 +295,12 @@ public class SessionPlayerEntity extends PlayerEntity { @Override protected void setAirSupply(int amount) { - if (amount == getMaxAir()) { - super.setAirSupply(0); // Hide the bubble counter from the UI for the player - } else { - super.setAirSupply(amount); - } + // Seemingly required to be sent as of Bedrock 1.21. Otherwise, bubbles will appear as empty + // Also, this changes how the air bubble graphics/sounds are presented. Breathing on means sound effects and + // the bubbles visually pop + setFlag(EntityFlag.BREATHING, amount >= this.lastAirSupply); + this.lastAirSupply = amount; + super.setAirSupply(amount); } @Override @@ -192,58 +322,62 @@ public class SessionPlayerEntity extends PlayerEntity { protected boolean hasShield(boolean offhand) { // Must be overridden to point to the player's inventory cache if (offhand) { - return session.getPlayerInventory().getOffhand().asItem() == Items.SHIELD; + return session.getPlayerInventory().getOffhand().is(Items.SHIELD); } else { - return session.getPlayerInventory().getItemInHand().asItem() == Items.SHIELD; - } - } - - @Override - public void updateBedrockMetadata() { - super.updateBedrockMetadata(); - if (refreshSpeed) { - AttributeData speedAttribute = session.adjustSpeed(); - if (speedAttribute != null) { - UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); - attributesPacket.setRuntimeEntityId(geyserId); - attributesPacket.setAttributes(Collections.singletonList(speedAttribute)); - session.sendUpstreamPacket(attributesPacket); - } - refreshSpeed = false; + return session.getPlayerInventory().getItemInHand().is(Items.SHIELD); } } @Override protected void updateAttribute(Attribute javaAttribute, List newAttributes) { - if (javaAttribute.getType() == AttributeType.Builtin.GENERIC_ATTACK_SPEED) { - session.setAttackSpeed(AttributeUtils.calculateValue(javaAttribute)); - } else { - super.updateAttribute(javaAttribute, newAttributes); + if (javaAttribute.getType() instanceof AttributeType.Builtin type) { + switch (type) { + case ATTACK_SPEED -> { + session.setAttackSpeed(AttributeUtils.calculateValue(javaAttribute)); + } + case BLOCK_INTERACTION_RANGE -> { + this.blockInteractionRange = AttributeUtils.calculateValue(javaAttribute); + } + case MINING_EFFICIENCY -> { + this.miningEfficiency = AttributeUtils.calculateValue(javaAttribute); + } + case BLOCK_BREAK_SPEED -> { + this.blockBreakSpeed = AttributeUtils.calculateValue(javaAttribute); + } + case SUBMERGED_MINING_SPEED -> { + this.submergedMiningSpeed = AttributeUtils.calculateValue(javaAttribute); + } + default -> { + super.updateAttribute(javaAttribute, newAttributes); + } + } } } @Override protected AttributeData calculateAttribute(Attribute javaAttribute, GeyserAttributeType type) { AttributeData attributeData = super.calculateAttribute(javaAttribute, type); - - if (javaAttribute.getType() == AttributeType.Builtin.GENERIC_MOVEMENT_SPEED) { - session.setOriginalSpeedAttribute(attributeData.getValue()); - AttributeData speedAttribute = session.adjustSpeed(); - if (speedAttribute != null) { - // Overwrite the attribute with our own - this.attributes.put(type, speedAttribute); - return speedAttribute; - } - } - this.attributes.put(type, attributeData); return attributeData; } + /** + * This will ONLY include attributes that have a Bedrock equivalent!!! + * see {@link LivingEntity#updateAttribute(Attribute, List)} + */ + public float attributeOrDefault(GeyserAttributeType type) { + var attribute = this.attributes.get(type); + if (attribute == null) { + return type.getDefaultValue(); + } + + return attribute.getValue(); + } + public void setLastDeathPosition(@Nullable GlobalPos pos) { if (pos != null) { dirtyMetadata.put(EntityDataTypes.PLAYER_LAST_DEATH_POS, pos.getPosition()); - dirtyMetadata.put(EntityDataTypes.PLAYER_LAST_DEATH_DIMENSION, DimensionUtils.javaToBedrock(pos.getDimension())); + dirtyMetadata.put(EntityDataTypes.PLAYER_LAST_DEATH_DIMENSION, DimensionUtils.javaToBedrock(pos.getDimension().asString())); dirtyMetadata.put(EntityDataTypes.PLAYER_HAS_DIED, true); } else { dirtyMetadata.put(EntityDataTypes.PLAYER_HAS_DIED, false); @@ -255,16 +389,169 @@ public class SessionPlayerEntity extends PlayerEntity { return session.getAuthData().uuid(); } + @Override + public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) { + // The bedrock client can glitch when sending a health and absorption attribute in the same tick + // This can happen when switching servers. Resending the absorption attribute fixes the issue + attributes.put(GeyserAttributeType.ABSORPTION, GeyserAttributeType.ABSORPTION.getAttribute(entityMetadata.getPrimitiveValue())); + super.setAbsorptionHearts(entityMetadata); + } + + @Override + public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) { + super.setLivingEntityFlags(entityMetadata); + + // Forcefully update flags since we're not tracking thing like using item properly. + // For eg: when player start using item client-sided (and the USING_ITEM flag is false on geyser side) + // If the server disagree with the player using item state, it will send a metadata set USING_ITEM flag to false + // But since it never got set to true, nothing changed, causing the client to not receive the USING_ITEM flag they're supposed to. + this.forceFlagUpdate(); + } + + @Override public void resetMetadata() { - // Reset all metadata to their default values - // This is used when a player respawns - this.initializeMetadata(); + super.resetMetadata(); // Reset air this.resetAir(); + + // Absorption is metadata in java edition + attributes.remove(GeyserAttributeType.ABSORPTION); + UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); + attributesPacket.setRuntimeEntityId(geyserId); + attributesPacket.setAttributes(Collections.singletonList( + GeyserAttributeType.ABSORPTION.getAttribute(0f))); + session.sendUpstreamPacket(attributesPacket); + + dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, 0); + dirtyMetadata.put(EntityDataTypes.EFFECT_AMBIENCE, (byte) 0); + dirtyMetadata.put(EntityDataTypes.FREEZING_EFFECT_STRENGTH, 0f); + + silent = false; + } + + public void resetAttributes() { + attributes.clear(); + maxHealth = GeyserAttributeType.MAX_HEALTH.getDefaultValue(); + blockInteractionRange = GeyserAttributeType.BLOCK_INTERACTION_RANGE.getDefaultValue(); + + UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); + attributesPacket.setRuntimeEntityId(geyserId); + attributesPacket.setAttributes(Collections.singletonList( + GeyserAttributeType.MOVEMENT_SPEED.getAttribute())); + session.sendUpstreamPacket(attributesPacket); } public void resetAir() { this.setAirSupply(getMaxAir()); } + + public void setVehicleInput(Vector2f vehicleInput) { + this.vehicleInput = Vector2f.from( + MathUtils.clamp(vehicleInput.getX(), -1.0f, 1.0f), + MathUtils.clamp(vehicleInput.getY(), -1.0f, 1.0f) + ); + } + + public void setVehicleJumpStrength(int vehicleJumpStrength) { + this.vehicleJumpStrength = MathUtils.constrain(vehicleJumpStrength, 0, 100); + } + + @Override + public void setVehicle(Entity entity) { + // For boats, we send width = 0.6 and height = 1.6 since there is otherwise a problem with player "clipping" into the boat when standing on it or running into it. + // Having a wide bounding box fixed that, however, it is technically incorrect and creates certain problems + // when you're actually riding the boat (https://github.com/GeyserMC/Geyser/issues/3106), since the box is way too big + // the boat's motion stops right before the block is hit and doesn't let the actual bounding clip collide into the block, + // causing the issues. So to fix this, everytime player enter a boat we send the java bounding box and only send the + // definition box when player is not riding the boat. + if (entity instanceof BoatEntity) { + // These bounding box values are based off 1.21.7 + entity.setBoundingBoxWidth(1.375F); + entity.setBoundingBoxHeight(0.5625F); + entity.updateBedrockMetadata(); + } else if (entity == null && this.vehicle instanceof BoatEntity) { + this.vehicle.setBoundingBoxWidth(this.vehicle.getDefinition().width()); + this.vehicle.setBoundingBoxHeight(this.vehicle.getDefinition().height()); + this.vehicle.updateBedrockMetadata(); + } + + super.setVehicle(entity); + } + + /** + * Used to calculate player jumping velocity for ground status calculation. + */ + public float getJumpVelocity() { + float velocity = 0.42F; + + if (session.getGeyser().getWorldManager().blockAt(session, this.getPosition().sub(0, EntityDefinitions.PLAYER.offset() + 0.1F, 0).toInt()).is(Blocks.HONEY_BLOCK)) { + velocity *= 0.6F; + } + + return velocity + 0.1F * session.getEffectCache().getJumpPower(); + } + + public boolean isOnClimbableBlock() { + if (session.getGameMode() == GameMode.SPECTATOR) { + return false; + } + Vector3i pos = getPosition().down(EntityDefinitions.PLAYER.offset()).toInt(); + BlockState state = session.getGeyser().getWorldManager().blockAt(session, pos); + if (state.block().is(session, BlockTag.CLIMBABLE)) { + return true; + } + + if (state.block() instanceof TrapDoorBlock) { + if (!state.getValue(Properties.OPEN)) { + return false; + } else { + BlockState belowState = session.getGeyser().getWorldManager().blockAt(session, pos.down()); + return belowState.is(Blocks.LADDER) && belowState.getValue(Properties.HORIZONTAL_FACING) == state.getValue(Properties.HORIZONTAL_FACING); + } + } + return false; + } + + public boolean canStartGliding() { + // You can't start gliding when levitation is applied + if (session.getEffectCache().getEntityEffects().contains(Effect.LEVITATION)) { + return false; + } + + if (this.isOnClimbableBlock() || session.getPlayerEntity().isOnGround()) { + return false; + } + + if (session.getCollisionManager().isPlayerTouchingWater()) { + return false; + } + + // Unfortunately gliding is still client-side, so we cannot force the client to glide even + // if we wanted to. However, we still need to check that gliding is possible even with, say, + // an elytra that does not have the glider component. + for (Map.Entry entry : session.getPlayerInventory().getEquipment().entrySet()) { + if (entry.getValue().getComponent(DataComponentTypes.GLIDER) != null) { + Equippable equippable = entry.getValue().getComponent(DataComponentTypes.EQUIPPABLE); + if (equippable != null && equippable.slot() == entry.getKey() && !entry.getValue().nextDamageWillBreak()) { + return true; + } + } + + // Bedrock will NOT allow flight when not wearing an elytra; even if it doesn't have a glider component + if (entry.getKey() == EquipmentSlot.CHESTPLATE && !entry.getValue().is(Items.ELYTRA)) { + return false; + } + } + + return false; + } + + public void forceFlagUpdate() { + setFlagsDirty(true); + } + + public boolean isGliding() { + return getFlag(EntityFlag.GLIDING); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java index 939e4721d..2819d9486 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.entity.type.player; +import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.GameType; @@ -33,11 +34,15 @@ import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; -import lombok.Getter; -import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.block.type.WallSkullBlock; +import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.SkullSkinManager; +import org.geysermc.geyser.translator.item.ItemTranslator; import java.util.Objects; import java.util.UUID; @@ -47,7 +52,7 @@ import java.util.concurrent.TimeUnit; * A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no * custom player skulls in Bedrock. */ -public class SkullPlayerEntity extends PlayerEntity { +public class SkullPlayerEntity extends AvatarEntity { @Getter private UUID skullUUID; @@ -56,7 +61,7 @@ public class SkullPlayerEntity extends PlayerEntity { private Vector3i skullPosition; public SkullPlayerEntity(GeyserSession session, long geyserId) { - super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null); + super(session, 0, geyserId, UUID.randomUUID(), EntityDefinitions.PLAYER, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, ""); } @Override @@ -70,62 +75,20 @@ public class SkullPlayerEntity extends PlayerEntity { setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded } - /** - * Overwritten so each entity doesn't check for a linked entity - */ - @Override - public void spawnEntity() { - AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); - addPlayerPacket.setUuid(getUuid()); - addPlayerPacket.setUsername(getUsername()); - addPlayerPacket.setRuntimeEntityId(geyserId); - addPlayerPacket.setUniqueEntityId(geyserId); - addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0)); - addPlayerPacket.setRotation(getBedrockRotation()); - addPlayerPacket.setMotion(motion); - addPlayerPacket.setHand(hand); - addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); - addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); - addPlayerPacket.setDeviceId(""); - addPlayerPacket.setPlatformChatId(""); - addPlayerPacket.setGameType(GameType.SURVIVAL); - addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); - addPlayerPacket.getMetadata().putFlags(flags); - dirtyMetadata.apply(addPlayerPacket.getMetadata()); - - setFlagsDirty(false); - - valid = true; - session.sendUpstreamPacket(addPlayerPacket); - } - - /** - * Hide the player entity so that it can be reused for a different skull. - */ - public void free() { - setFlag(EntityFlag.INVISIBLE, true); - updateBedrockMetadata(); - - // Move skull entity out of the way - moveAbsolute(session.getPlayerEntity().getPosition().up(128), 0, 0, 0, false, true); - } - public void updateSkull(SkullCache.Skull skull) { skullPosition = skull.getPosition(); - if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) { + if (!Objects.equals(skull.getTexturesProperty(), texturesProperty) || !Objects.equals(skullUUID, skull.getUuid())) { // Make skull invisible as we change skins setFlag(EntityFlag.INVISIBLE, true); updateBedrockMetadata(); skullUUID = skull.getUuid(); - setTexturesProperty(skull.getTexturesProperty()); - - SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { + setSkin(skull.getTexturesProperty(), false, () -> session.scheduleInEventLoop(() -> { // Delay to minimize split-second "player" pop-in setFlag(EntityFlag.INVISIBLE, false); updateBedrockMetadata(); - }, 250, TimeUnit.MILLISECONDS))); + }, 250, TimeUnit.MILLISECONDS)); } else { // Just a rotation/position change setFlag(EntityFlag.INVISIBLE, false); @@ -137,20 +100,19 @@ public class SkullPlayerEntity extends PlayerEntity { float z = skull.getPosition().getZ() + .5f; float rotation; - int blockState = skull.getBlockState(); - byte floorRotation = BlockStateValues.getSkullRotation(blockState); - if (floorRotation == -1) { - // Wall skull + BlockState blockState = skull.getBlockState(); + if (blockState.block() instanceof WallSkullBlock) { y += 0.25f; - rotation = BlockStateValues.getSkullWallDirections().get(blockState); - switch ((int) rotation) { - case 180 -> z += 0.24f; // North - case 0 -> z -= 0.24f; // South - case 90 -> x += 0.24f; // West - case 270 -> x -= 0.24f; // East + Direction direction = blockState.getValue(Properties.HORIZONTAL_FACING); + rotation = WallSkullBlock.getDegrees(direction); + switch (direction) { + case NORTH -> z += 0.24f; + case SOUTH -> z -= 0.24f; + case WEST -> x += 0.24f; + case EAST -> x -= 0.24f; } } else { - rotation = (180f + (floorRotation * 22.5f)) % 360; + rotation = (180f + blockState.getValue(Properties.ROTATION_16, 0) * 22.5f) % 360; } moveAbsolute(Vector3f.from(x, y, z), rotation, 0, rotation, true, true); diff --git a/core/src/main/java/org/geysermc/geyser/item/DyeableLeatherItem.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java similarity index 58% rename from core/src/main/java/org/geysermc/geyser/item/DyeableLeatherItem.java rename to core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java index e0eec767f..41224012d 100644 --- a/core/src/main/java/org/geysermc/geyser/item/DyeableLeatherItem.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java @@ -23,34 +23,38 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item; +package org.geysermc.geyser.entity.vehicle; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.IntTag; +import org.cloudburstmc.math.TrigMath; +import org.geysermc.geyser.entity.type.LivingEntity; -public interface DyeableLeatherItem { +public class BoostableVehicleComponent extends VehicleComponent { + private int boostLength; + private int boostTicks = 1; - static void translateNbtToBedrock(CompoundTag tag) { - CompoundTag displayTag = tag.get("display"); - if (displayTag == null) { - return; - } - IntTag color = displayTag.remove("color"); - if (color != null) { - tag.put(new IntTag("customColor", color.getValue())); - } + public BoostableVehicleComponent(T vehicle, float stepHeight) { + super(vehicle, stepHeight); } - static void translateNbtToJava(CompoundTag tag) { - IntTag color = tag.get("customColor"); - if (color == null) { - return; + public void startBoost(int boostLength) { + this.boostLength = boostLength; + this.boostTicks = 1; + } + + public float getBoostMultiplier() { + if (isBoosting()) { + return 1.0f + 1.15f * TrigMath.sin((float) boostTicks / (float) boostLength * TrigMath.PI); } - CompoundTag displayTag = tag.get("display"); - if (displayTag == null) { - displayTag = new CompoundTag("display"); + return 1.0f; + } + + public boolean isBoosting() { + return boostTicks <= boostLength; + } + + public void tickBoost() { + if (isBoosting()) { + boostTicks++; } - displayTag.put(color); - tag.remove("customColor"); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java new file mode 100644 index 000000000..1ed9328f0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.vehicle; + +import lombok.Setter; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; + +public class CamelVehicleComponent extends VehicleComponent { + private static final int STANDING_TICKS = 52; + private static final int DASH_TICKS = 55; + + @Setter + private float horseJumpStrength = 0.42f; // Not sent by vanilla Java server when spawned + + @Setter + private long lastPoseTick; + + private int dashTick; + private int effectJumpBoost; + + public CamelVehicleComponent(CamelEntity vehicle) { + super(vehicle, 1.5f); + } + + public void startDashCooldown() { + // tickVehicle is only called while the vehicle is mounted. Use session ticks to keep + // track of time instead of counting down + this.dashTick = vehicle.getSession().getTicks() + DASH_TICKS; + } + + @Override + public void tickVehicle() { + if (this.dashTick != 0) { + if (vehicle.getSession().getTicks() > this.dashTick) { + vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, false); + this.dashTick = 0; + } else { + vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, true); + } + } + + vehicle.setFlag(EntityFlag.CAN_DASH, vehicle.getFlag(EntityFlag.SADDLED) && !isStationary()); + vehicle.updateBedrockMetadata(); + super.tickVehicle(); + } + + @Override + public void onDismount() { + // Prevent camel from getting stuck in dash animation + vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, false); + vehicle.updateBedrockMetadata(); + super.onDismount(); + } + + @Override + protected boolean travel(VehicleContext ctx, float speed) { + if (vehicle.isOnGround() && isStationary()) { + vehicle.setMotion(vehicle.getMotion().mul(0, 1, 0)); + } + + return super.travel(ctx, speed); + } + + @Override + protected Vector3f getInputVector(VehicleContext ctx, float speed, Vector3f input) { + if (isStationary()) { + return Vector3f.ZERO; + } + + SessionPlayerEntity player = vehicle.getSession().getPlayerEntity(); + Vector3f inputVelocity = super.getInputVector(ctx, speed, input); + float jumpStrength = player.getVehicleJumpStrength(); + + if (jumpStrength > 0) { + player.setVehicleJumpStrength(0); + + if (jumpStrength >= 90) { + jumpStrength = 1.0f; + } else { + jumpStrength = 0.4f + 0.4f * jumpStrength / 90.0f; + } + + return inputVelocity.add(Vector3f.createDirectionDeg(0, -player.getYaw()) + .mul(22.2222f * jumpStrength * this.moveSpeed * getVelocityMultiplier(ctx)) + .up(1.4285f * jumpStrength * (this.horseJumpStrength * getJumpVelocityMultiplier(ctx) + (this.effectJumpBoost * 0.1f)))); + } + + return inputVelocity; + } + + @Override + protected Vector2f getRiddenRotation() { + if (isStationary()) { + return Vector2f.from(vehicle.getYaw(), vehicle.getPitch()); + } + return super.getRiddenRotation(); + } + + /** + * Checks if the camel is sitting + * or transitioning to standing pose. + */ + private boolean isStationary() { + // Java checks if sitting using lastPoseTick + return this.lastPoseTick < 0 || vehicle.getSession().getWorldTicks() < this.lastPoseTick + STANDING_TICKS; + } + + @Override + public void setEffect(Effect effect, int effectAmplifier) { + if (effect == Effect.JUMP_BOOST) { + effectJumpBoost = effectAmplifier + 1; + } else { + super.setEffect(effect, effectAmplifier); + } + } + + @Override + public void removeEffect(Effect effect) { + if (effect == Effect.JUMP_BOOST) { + effectJumpBoost = 0; + } else { + super.removeEffect(effect); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java new file mode 100644 index 000000000..c0480ee90 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.vehicle; + +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; + +public interface ClientVehicle { + VehicleComponent getVehicleComponent(); + + // MojMap LivingEntity#getRiddenInput + Vector3f getRiddenInput(Vector2f input); + + // MojMap LivingEntity#getRiddenSpeed + float getVehicleSpeed(); + + // MojMap Mob#getControllingPassenger + boolean isClientControlled(); + + default boolean canWalkOnLava() { + return false; + } + + default boolean canClimb() { + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java new file mode 100644 index 000000000..b332ff5ff --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.vehicle; + +import lombok.Getter; +import lombok.Setter; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.erosion.util.BlockPositionIterator; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.Fluid; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; + +@Setter +@Getter +public class HappyGhastVehicleComponent extends VehicleComponent { + + private float flyingSpeed; + private float cameraDistance; + + public HappyGhastVehicleComponent(HappyGhastEntity vehicle, float stepHeight) { + super(vehicle, stepHeight); + // Happy Ghast has different defaults + flyingSpeed = 0.05f; + moveSpeed = 0.05f; + cameraDistance = 8.0f; + } + + @Override + protected void updateRotation() { + float yaw = vehicle.getYaw() + MathUtils.wrapDegrees(getRiddenRotation().getX() - vehicle.getYaw()) * 0.08f; + vehicle.setYaw(yaw); + vehicle.setHeadYaw(yaw); + } + + @Override + public void onMount() { + super.onMount(); + SessionPlayerEntity playerEntity = vehicle.getSession().getPlayerEntity(); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_LOCK_RIDER_ROTATION, false); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_LOCK_RIDER_ROTATION_DEGREES, 181f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_THIRD_PERSON_CAMERA_RADIUS, cameraDistance); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_CAMERA_RELAX_DISTANCE_SMOOTHING, cameraDistance * 0.75f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.CONTROLLING_RIDER_SEAT_INDEX, (byte) 0); + } + + @Override + public void onDismount() { + super.onDismount(); + SessionPlayerEntity playerEntity = vehicle.getSession().getPlayerEntity(); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_THIRD_PERSON_CAMERA_RADIUS, (float) AttributeType.Builtin.CAMERA_DISTANCE.getDef()); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_CAMERA_RELAX_DISTANCE_SMOOTHING, cameraDistance * 0.75f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.CONTROLLING_RIDER_SEAT_INDEX, (byte) 0); + } + + /** + * Called every session tick while the player is mounted on the vehicle. + */ + public void tickVehicle() { + if (!vehicle.isClientControlled()) { + return; + } + + VehicleContext ctx = new VehicleContext(); + ctx.loadSurroundingBlocks(); + + // LivingEntity#travelFlying + Fluid fluid = checkForFluid(ctx); + float drag = switch (fluid) { + case WATER -> 0.8f; + case LAVA -> 0.5f; + case EMPTY -> 0.91f; + }; + // HappyGhast#travel + travel(ctx, flyingSpeed * 5.0f / 3.0f); + vehicle.setMotion(vehicle.getMotion().mul(drag)); + } + + private Fluid checkForFluid(VehicleContext ctx) { + Fluid result = Fluid.EMPTY; + + BoundingBox box = boundingBox.clone(); + box.expand(-0.001); + + Vector3d min = box.getMin(); + Vector3d max = box.getMax(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + for (iter.reset(); iter.hasNext(); iter.next()) { + BlockState blockState = ctx.getBlock(iter); + if (blockState.is(Blocks.WATER)) { + return Fluid.WATER; // Water takes priority over lava + } + if (blockState.is(Blocks.LAVA)) { + result = Fluid.LAVA; + } + } + + return result; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java new file mode 100644 index 000000000..64796d052 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java @@ -0,0 +1,980 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.vehicle; + +import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import lombok.Getter; +import lombok.Setter; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.TrigMath; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; +import org.geysermc.erosion.util.BlockPositionIterator; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.Fluid; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BedBlock; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.block.type.TrapDoorBlock; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.cache.tags.BlockTag; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.SolidCollision; +import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket; + +public class VehicleComponent { + private static final ObjectDoublePair EMPTY_FLUID_PAIR = ObjectDoublePair.of(Fluid.EMPTY, 0.0); + private static final float MAX_LOGICAL_FLUID_HEIGHT = 8.0f / BlockStateValues.NUM_FLUID_LEVELS; + private static final float BASE_SLIPPERINESS_CUBED = 0.6f * 0.6f * 0.6f; + private static final float MIN_VELOCITY = 0.003f; + + protected final T vehicle; + @Getter + protected final BoundingBox boundingBox; + + protected float stepHeight; + @Getter @Setter + protected float moveSpeed; + protected double gravity; + @Getter @Setter + protected double waterMovementEfficiency; + protected int effectLevitation; + protected boolean effectSlowFalling; + protected boolean effectWeaving; + + public VehicleComponent(T vehicle, float stepHeight) { + this.vehicle = vehicle; + this.stepHeight = stepHeight; + this.moveSpeed = (float) AttributeType.Builtin.MOVEMENT_SPEED.getDef(); + this.gravity = AttributeType.Builtin.GRAVITY.getDef(); + this.waterMovementEfficiency = AttributeType.Builtin.WATER_MOVEMENT_EFFICIENCY.getDef(); + + double width = vehicle.getBoundingBoxWidth(); + double height = vehicle.getBoundingBoxHeight(); + this.boundingBox = new BoundingBox( + vehicle.getPosition().getX(), + vehicle.getPosition().getY() + height / 2, + vehicle.getPosition().getZ(), + width, height, width + ); + } + + public void setWidth(float width) { + boundingBox.setSizeX(width); + boundingBox.setSizeZ(width); + } + + public void setHeight(float height) { + boundingBox.translate(0, (height - boundingBox.getSizeY()) / 2, 0); + boundingBox.setSizeY(height); + } + + public void moveAbsolute(double x, double y, double z) { + boundingBox.setMiddleX(x); + boundingBox.setMiddleY(y + boundingBox.getSizeY() / 2); + boundingBox.setMiddleZ(z); + } + + public void moveAbsolute(Vector3d vec) { + moveAbsolute(vec.getX(), vec.getY(), vec.getZ()); + } + + public void moveRelative(double x, double y, double z) { + boundingBox.translate(x, y, z); + } + + public void moveRelative(Vector3d vec) { + boundingBox.translate(vec); + } + + public void setEffect(Effect effect, int effectAmplifier) { + switch (effect) { + case LEVITATION -> effectLevitation = effectAmplifier + 1; + case SLOW_FALLING -> effectSlowFalling = true; + case WEAVING -> effectWeaving = true; + } + } + + public void removeEffect(Effect effect) { + switch (effect) { + case LEVITATION -> effectLevitation = 0; + case SLOW_FALLING -> effectSlowFalling = false; + case WEAVING -> effectWeaving = false; + } + } + + public void setStepHeight(float stepHeight) { + this.stepHeight = MathUtils.clamp(stepHeight, 1.0f, 10.0f); + } + + public void setGravity(double gravity) { + this.gravity = MathUtils.constrain(gravity, -1.0, 1.0); + } + + public Vector3d correctMovement(Vector3d movement) { + return vehicle.getSession().getCollisionManager().correctMovement( + movement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava() + ); + } + + public void onMount() { + vehicle.getSession().getPlayerEntity().setVehicleInput(Vector2f.ZERO); + vehicle.getSession().getPlayerEntity().setVehicleJumpStrength(0); + } + + public void onDismount() { + // + } + + /** + * Called every session tick while the player is mounted on the vehicle. + */ + public void tickVehicle() { + if (!vehicle.isClientControlled()) { + return; + } + + VehicleContext ctx = new VehicleContext(); + ctx.loadSurroundingBlocks(); + + ObjectDoublePair fluidHeight = updateFluidMovement(ctx); + switch (fluidHeight.left()) { + case WATER -> waterMovement(ctx); + case LAVA -> { + if (vehicle.canWalkOnLava() && ctx.centerBlock().is(Blocks.LAVA)) { + landMovement(ctx); + } else { + lavaMovement(ctx, fluidHeight.rightDouble()); + } + } + case EMPTY -> landMovement(ctx); + } + } + + /** + * Update the rotation of the vehicle. Should be called once per tick, and before getInputVector. + */ + protected void updateRotation() { + Vector2f rot = getRiddenRotation(); + vehicle.setYaw(rot.getX()); + vehicle.setHeadYaw(rot.getX()); + vehicle.setPitch(rot.getY()); + } + + /** + * Adds velocity of all colliding fluids to the vehicle, and returns the height of the fluid to use for movement. + * + * @param ctx context + * @return type and height of fluid to use for movement + */ + protected ObjectDoublePair updateFluidMovement(VehicleContext ctx) { + BoundingBox box = boundingBox.clone(); + box.expand(-0.001); + + Vector3d min = box.getMin(); + Vector3d max = box.getMax(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + + // Mojmap Entity#updateInWaterStateAndDoFluidPushing + double waterHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.WATER, 0.014, min.getY()); + double lavaHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.LAVA, vehicle.getSession().getDimensionType().ultrawarm() ? 0.007 : 0.007 / 3, min.getY()); + + // Apply upward motion if the vehicle is a Strider, and it is submerged in lava + if (lavaHeight > 0 && vehicle.getDefinition().entityType() == EntityType.STRIDER) { + Vector3i blockPos = ctx.centerPos().toInt(); + if (!CollisionManager.FLUID_COLLISION.isBelow(blockPos.getY(), boundingBox) || ctx.getBlock(blockPos.up()).is(Blocks.LAVA)) { + vehicle.setMotion(vehicle.getMotion().mul(0.5f).add(0, 0.05f, 0)); + } else { + vehicle.setOnGround(true); + } + } + + // Water movement has priority over lava movement + if (waterHeight > 0) { + return ObjectDoublePair.of(Fluid.WATER, waterHeight); + } + + if (lavaHeight > 0) { + return ObjectDoublePair.of(Fluid.LAVA, lavaHeight); + } + + return EMPTY_FLUID_PAIR; + } + + /** + * Calculates how deep the vehicle is in a fluid, and applies its velocity. + * + * @param ctx context + * @param iter iterator of colliding blocks + * @param fluid type of fluid + * @param speed multiplier for fluid motion + * @param minY minY of the bounding box used to check for fluid collision; not exactly the same as the vehicle's bounding box + * @return height of fluid compared to minY + */ + protected double getFluidHeightAndApplyMovement(VehicleContext ctx, BlockPositionIterator iter, Fluid fluid, double speed, double minY) { + Vector3d totalVelocity = Vector3d.ZERO; + double maxFluidHeight = 0; + int fluidBlocks = 0; + + for (iter.reset(); iter.hasNext(); iter.next()) { + int blockId = ctx.getBlockId(iter); + if (BlockStateValues.getFluid(blockId) != fluid) { + continue; + } + + Vector3i blockPos = Vector3i.from(iter.getX(), iter.getY(), iter.getZ()); + float worldFluidHeight = getWorldFluidHeight(fluid, blockId); + + double vehicleFluidHeight = blockPos.getY() + worldFluidHeight - minY; + if (vehicleFluidHeight < 0) { + // Vehicle is not submerged in this fluid block + continue; + } + + // flowBlocked is only used when determining if a falling fluid should drag the vehicle downwards. + // If this block is not a falling fluid, set to true to avoid unnecessary checks. + boolean flowBlocked = worldFluidHeight != 1; + + Vector3d velocity = Vector3d.ZERO; + for (Direction direction : Direction.HORIZONTAL) { + Vector3i adjacentBlockPos = blockPos.add(direction.getUnitVector()); + int adjacentBlockId = ctx.getBlockId(adjacentBlockPos); + Fluid adjacentFluid = BlockStateValues.getFluid(adjacentBlockId); + + float fluidHeightDiff = 0; + if (adjacentFluid == fluid) { + fluidHeightDiff = getLogicalFluidHeight(fluid, blockId) - getLogicalFluidHeight(fluid, adjacentBlockId); + } else if (adjacentFluid == Fluid.EMPTY) { + // If the adjacent block is not a fluid and does not have collision, + // check if there is a fluid under it + BlockCollision adjacentBlockCollision = BlockUtils.getCollision(adjacentBlockId); + if (adjacentBlockCollision == null) { + float adjacentFluidHeight = getLogicalFluidHeight(fluid, ctx.getBlockId(adjacentBlockPos.add(Direction.DOWN.getUnitVector()))); + if (adjacentFluidHeight != -1) { // Only care about same type of fluid + fluidHeightDiff = getLogicalFluidHeight(fluid, blockId) - (adjacentFluidHeight - MAX_LOGICAL_FLUID_HEIGHT); + } + } else if (!flowBlocked) { + // No need to check if flow is already blocked from another direction, or if this isn't a falling fluid. + flowBlocked = isFlowBlocked(fluid, adjacentBlockId); + } + } + + if (fluidHeightDiff != 0) { + velocity = velocity.add(direction.getUnitVector().toDouble().mul(fluidHeightDiff)); + } + } + + if (worldFluidHeight == 1) { // If falling fluid + // If flow is not blocked, check if it is blocked for the fluid above + if (!flowBlocked) { + Vector3i blockPosUp = blockPos.up(); + for (Direction direction : Direction.HORIZONTAL) { + flowBlocked = isFlowBlocked(fluid, ctx.getBlockId(blockPosUp.add(direction.getUnitVector()))); + if (flowBlocked) { + break; + } + } + } + + if (flowBlocked) { + velocity = javaNormalize(velocity).add(0.0, -6.0, 0.0); + } + } + + velocity = javaNormalize(velocity); + + maxFluidHeight = Math.max(vehicleFluidHeight, maxFluidHeight); + if (maxFluidHeight < 0.4) { + velocity = velocity.mul(maxFluidHeight); + } + + totalVelocity = totalVelocity.add(velocity); + fluidBlocks++; + } + + if (!totalVelocity.equals(Vector3d.ZERO)) { + Vector3f motion = vehicle.getMotion(); + + totalVelocity = javaNormalize(totalVelocity.mul(1.0 / fluidBlocks)); + totalVelocity = totalVelocity.mul(speed); + + if (totalVelocity.length() < 0.0045 && Math.abs(motion.getX()) < MIN_VELOCITY && Math.abs(motion.getZ()) < MIN_VELOCITY) { + totalVelocity = javaNormalize(totalVelocity).mul(0.0045); + } + + vehicle.setMotion(motion.add(totalVelocity.toFloat())); + } + + return maxFluidHeight; + } + + /** + * Java edition returns the zero vector if the length of the input vector is less than 0.00001f + */ + protected Vector3d javaNormalize(Vector3d vec) { + double len = vec.length(); + // Used to be 1.0E-4 + return len < 1.0E-5F ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len); + } + + protected float getWorldFluidHeight(Fluid fluidType, int blockId) { + return (float) switch (fluidType) { + case WATER -> BlockStateValues.getWaterHeight(blockId); + case LAVA -> BlockStateValues.getLavaHeight(blockId); + case EMPTY -> -1; + }; + } + + protected float getLogicalFluidHeight(Fluid fluidType, int blockId) { + return Math.min(getWorldFluidHeight(fluidType, blockId), MAX_LOGICAL_FLUID_HEIGHT); + } + + protected boolean isFlowBlocked(Fluid fluid, int adjacentBlockId) { + if (BlockState.of(adjacentBlockId).is(Blocks.ICE)) { + return false; + } + + if (BlockStateValues.getFluid(adjacentBlockId) == fluid) { + return false; + } + + // TODO: supposed to check if the opposite face of the block touching the fluid is solid, instead of SolidCollision + return BlockUtils.getCollision(adjacentBlockId) instanceof SolidCollision; + } + + // Mojmap: LivingEntity#travelInFluid + protected void waterMovement(VehicleContext ctx) { + double gravity = getGravity(); + float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : 0.8f; // 0.8f: getBaseMovementSpeedMultiplier + double originalY = ctx.centerPos().getY(); + boolean falling = vehicle.getMotion().getY() <= 0; + + // NOT IMPLEMENTED: depth strider and dolphins grace +// float g = 0.02f; +// float waterMovementEfficiencyMultiplier = (float) waterMovementEfficiency; +// if (!vehicle.isOnGround()) { +// // TODO test +// waterMovementEfficiencyMultiplier *= 0.5f; +// } +// +// if (waterMovementEfficiencyMultiplier > 0.0F) { +// drag += (0.54600006F - drag) * waterMovementEfficiencyMultiplier; +// g += (this.getSpeed() - g) * waterMovementEfficiencyMultiplier; +// } + +// if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) { +// drag = 0.96F; +// } + + boolean horizontalCollision = travel(ctx, 0.02f); + + if (horizontalCollision && isClimbing(ctx)) { + vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.2f, vehicle.getMotion().getZ())); + } + + vehicle.setMotion(vehicle.getMotion().mul(drag, 0.8f, drag)); + vehicle.setMotion(getFluidGravity(gravity, falling)); + + if (horizontalCollision && shouldApplyFluidJumpBoost(ctx, originalY)) { + vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.3f, vehicle.getMotion().getZ())); + } + } + + protected void lavaMovement(VehicleContext ctx, double lavaHeight) { + double gravity = getGravity(); + double originalY = ctx.centerPos().getY(); + boolean falling = vehicle.getMotion().getY() <= 0; + + boolean horizontalCollision = travel(ctx, 0.02f); + + if (lavaHeight <= (boundingBox.getSizeY() * 0.85 < 0.4 ? 0.0 : 0.4)) { // Swim height + vehicle.setMotion(vehicle.getMotion().mul(0.5f, 0.8f, 0.5f)); + vehicle.setMotion(getFluidGravity(gravity, falling)); + } else { + vehicle.setMotion(vehicle.getMotion().mul(0.5f)); + } + + vehicle.setMotion(vehicle.getMotion().down((float) (gravity / 4.0))); + + if (horizontalCollision && shouldApplyFluidJumpBoost(ctx, originalY)) { + vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.3f, vehicle.getMotion().getZ())); + } + } + + protected void landMovement(VehicleContext ctx) { + double gravity = getGravity(); + float slipperiness = BlockStateValues.getSlipperiness(getVelocityBlock(ctx)); + float drag = vehicle.isOnGround() ? 0.91f * slipperiness : 0.91f; + float speed = vehicle.getVehicleSpeed() * (vehicle.isOnGround() ? BASE_SLIPPERINESS_CUBED / (slipperiness * slipperiness * slipperiness) : 0.1f); + + boolean horizontalCollision = travel(ctx, speed); + + if (isClimbing(ctx)) { + Vector3f motion = vehicle.getMotion(); + vehicle.setMotion( + Vector3f.from( + MathUtils.clamp(motion.getX(), -0.15f, 0.15f), + horizontalCollision ? 0.2f : Math.max(motion.getY(), -0.15f), + MathUtils.clamp(motion.getZ(), -0.15f, 0.15f) + ) + ); + // NOT IMPLEMENTED: climbing in powdered snow + } + + if (effectLevitation > 0) { + vehicle.setMotion(vehicle.getMotion().up((0.05f * effectLevitation - vehicle.getMotion().getY()) * 0.2f)); + } else { + vehicle.setMotion(vehicle.getMotion().down((float) gravity)); + // NOT IMPLEMENTED: slow fall when in unloaded chunk + } + + vehicle.setMotion(vehicle.getMotion().mul(drag, 0.98f, drag)); + } + + protected boolean shouldApplyFluidJumpBoost(VehicleContext ctx, double originalY) { + BoundingBox box = boundingBox.clone(); + box.translate(vehicle.getMotion().toDouble().up(0.6f - ctx.centerPos().getY() + originalY)); + box.expand(-1.0E-7); + + BlockPositionIterator iter = vehicle.getSession().getCollisionManager().collidableBlocksIterator(box); + for (iter.reset(); iter.hasNext(); iter.next()) { + int blockId = ctx.getBlockId(iter); + + // Also check for fluids + BlockCollision blockCollision = BlockUtils.getCollision(blockId); + if (blockCollision == null && BlockStateValues.getFluid(blockId) != Fluid.EMPTY) { + blockCollision = CollisionManager.SOLID_COLLISION; + } + + if (blockCollision != null && blockCollision.checkIntersection(iter.getX(), iter.getY(), iter.getZ(), box)) { + return false; + } + } + + return true; + } + + protected Vector3f getFluidGravity(double gravity, boolean falling) { + Vector3f motion = vehicle.getMotion(); + if (gravity != 0 && !vehicle.getFlag(EntityFlag.SPRINTING)) { + float newY = (float) (motion.getY() - gravity / 16); + if (falling && Math.abs(motion.getY() - 0.005f) >= MIN_VELOCITY && Math.abs(newY) < MIN_VELOCITY) { + newY = -MIN_VELOCITY; + } + return Vector3f.from(motion.getX(), newY, motion.getZ()); + } + return motion; + } + + /** + * Check if any blocks the vehicle is colliding with should multiply movement. (Cobweb, powder snow, berry bush) + *

+ * This is different from the speed factor of a block the vehicle is standing on, such as soul sand. + * + * @param ctx context + * @return the multiplier + */ + protected @Nullable Vector3f getBlockMovementMultiplier(VehicleContext ctx) { + BoundingBox box = boundingBox.clone(); + box.expand(-1.0E-7); + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + + // Iterate xyz backwards + // Minecraft iterates forwards but only the last multiplier affects movement + for (int x = max.getX(); x >= min.getX(); x--) { + for (int y = max.getY(); y >= min.getY(); y--) { + for (int z = max.getZ(); z >= min.getZ(); z--) { + Block block = ctx.getBlock(x, y, z).block(); + Vector3f multiplier = null; + + if (block == Blocks.COBWEB) { + if (effectWeaving) { + multiplier = Vector3f.from(0.5, 0.25, 0.5); + } else { + multiplier = Vector3f.from(0.25, 0.05f, 0.25); + } + } else if (block == Blocks.POWDER_SNOW) { + multiplier = Vector3f.from(0.9f, 1.5, 0.9f); + } else if (block == Blocks.SWEET_BERRY_BUSH) { + multiplier = Vector3f.from(0.8f, 0.75, 0.8f); + } + + if (multiplier != null) { + return multiplier; + } + } + } + } + + return null; + } + + protected void applyBlockCollisionEffects(VehicleContext ctx) { + BoundingBox box = boundingBox.clone(); + box.expand(-1.0E-7); + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); + for (iter.reset(); iter.hasNext(); iter.next()) { + BlockState blockState = ctx.getBlock(iter); + + if (blockState.is(Blocks.HONEY_BLOCK)) { + onHoneyBlockCollision(); + } else if (blockState.is(Blocks.BUBBLE_COLUMN)) { + onBubbleColumnCollision(blockState.getValue(Properties.DRAG)); + } + } + } + + protected void onHoneyBlockCollision() { + if (vehicle.isOnGround() || vehicle.getMotion().getY() >= -0.08f) { + return; + } + + // NOT IMPLEMENTED: don't slide if inside the honey block + Vector3f motion = vehicle.getMotion(); + float mul = motion.getY() < -0.13f ? -0.05f / motion.getY() : 1; + vehicle.setMotion(Vector3f.from(motion.getX() * mul, -0.05f, motion.getZ() * mul)); + } + + protected void onBubbleColumnCollision(boolean drag) { + Vector3f motion = vehicle.getMotion(); + vehicle.setMotion(Vector3f.from( + motion.getX(), + drag ? Math.max(-0.3f, motion.getY() - 0.03f) : Math.min(0.7f, motion.getY() + 0.06f), + motion.getZ() + )); + } + + /** + * Calculates the next position of the vehicle while checking for collision and adjusting velocity. + * + * @return true if there was a horizontal collision + */ + // Mojmap: LivingEntity#moveRelative / LivingEntity#move + protected boolean travel(VehicleContext ctx, float speed) { + Vector3f motion = vehicle.getMotion(); + + motion = Vector3f.from( + Math.abs(motion.getX()) < MIN_VELOCITY ? 0 : motion.getX(), + Math.abs(motion.getY()) < MIN_VELOCITY ? 0 : motion.getY(), + Math.abs(motion.getZ()) < MIN_VELOCITY ? 0 : motion.getZ() + ); + + Vector3f lastRotation = vehicle.getBedrockRotation(); + updateRotation(); + + Vector2f playerInput = vehicle.getSession().getPlayerEntity().getVehicleInput(); + Vector3f riddenInput = vehicle.getRiddenInput(playerInput.mul(0.98f)); + + // !isImmobile + if (vehicle.isAlive()) { + motion = motion.add(getInputVector(ctx, speed, riddenInput)); + } + + Vector3f movementMultiplier = getBlockMovementMultiplier(ctx); + if (movementMultiplier != null) { + motion = motion.mul(movementMultiplier); + } + + // Check world border before blocks + Vector3d correctedMovement = vehicle.getSession().getWorldBorder().correctMovement(boundingBox, motion.toDouble()); + correctedMovement = vehicle.getSession().getCollisionManager().correctMovement( + correctedMovement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava() + ); + + boundingBox.translate(correctedMovement); + ctx.loadSurroundingBlocks(); // Context must be reloaded after vehicle is moved + + // Non-zero values indicate a collision on that axis + Vector3d moveDiff = motion.toDouble().sub(correctedMovement); + + vehicle.setOnGround(moveDiff.getY() != 0 && motion.getY() < 0); + boolean horizontalCollision = moveDiff.getX() != 0 || moveDiff.getZ() != 0; + + boolean bounced = false; + if (vehicle.isOnGround()) { + Block landingBlock = getLandingBlock(ctx).block(); + + if (landingBlock == Blocks.SLIME_BLOCK) { + motion = Vector3f.from(motion.getX(), -motion.getY(), motion.getZ()); + bounced = true; + + // Slow horizontal movement + float absY = Math.abs(motion.getY()); + if (absY < 0.1f) { + float mul = 0.4f + absY * 0.2f; + motion = motion.mul(mul, 1.0f, mul); + } + } else if (landingBlock instanceof BedBlock) { + motion = Vector3f.from(motion.getX(), -motion.getY() * 0.66f, motion.getZ()); + bounced = true; + } + } + + // Set motion to 0 if a movement multiplier was used, else set to 0 on each axis with a collision + if (movementMultiplier != null) { + motion = Vector3f.ZERO; + } else { + motion = motion.mul( + moveDiff.getX() == 0 ? 1 : 0, + moveDiff.getY() == 0 || bounced ? 1 : 0, + moveDiff.getZ() == 0 ? 1 : 0 + ); + } + + // Send the new position to the bedrock client and java server + moveVehicle(ctx.centerPos(), lastRotation); + vehicle.setMotion(motion); + + applyBlockCollisionEffects(ctx); + + float velocityMultiplier = getVelocityMultiplier(ctx); + vehicle.setMotion(vehicle.getMotion().mul(velocityMultiplier, 1.0f, velocityMultiplier)); + + return horizontalCollision; + } + + protected boolean isClimbing(VehicleContext ctx) { + if (!vehicle.canClimb()) { + return false; + } + + BlockState blockState = ctx.centerBlock(); + if (blockState.block().is(vehicle.getSession(), BlockTag.CLIMBABLE)) { + return true; + } + + // Check if the vehicle is in an open trapdoor with a ladder of the same direction under it + if (blockState.block() instanceof TrapDoorBlock && blockState.getValue(Properties.OPEN)) { + BlockState ladderState = ctx.getBlock(ctx.centerPos().toInt().down()); + return ladderState.is(Blocks.LADDER) && + ladderState.getValue(Properties.HORIZONTAL_FACING) == blockState.getValue(Properties.HORIZONTAL_FACING); + } + + return false; + } + + protected Vector3f getInputVector(VehicleContext ctx, float speed, Vector3f input) { + double lenSquared = input.lengthSquared(); + if (lenSquared < 1.0E-7) { + return Vector3f.ZERO; + } + + if (lenSquared > 1.0f) { + input = input.normalize(); + } + input = input.mul(speed); + + // Match vehicle rotation + float yaw = vehicle.getYaw(); + float sin = TrigMath.sin(yaw * TrigMath.DEG_TO_RAD); + float cos = TrigMath.cos(yaw * TrigMath.DEG_TO_RAD); + return Vector3f.from(input.getX() * cos - input.getZ() * sin, input.getY(), input.getZ() * cos + input.getX() * sin); + } + + /** + * Gets the rotation to use for the vehicle. This is based on the player's head rotation. + * + * @return (yaw, pitch) + */ + protected Vector2f getRiddenRotation() { + LivingEntity player = vehicle.getSession().getPlayerEntity(); + return Vector2f.from(player.getYaw(), player.getPitch() * 0.5f); + } + + /** + * Sets the new position for the vehicle and sends packets to both the java server and bedrock client. + *

+ * This also updates the session's last vehicle move timestamp. + * @param javaPos the new java position of the vehicle + * @param lastRotation the previous rotation of the vehicle (pitch, yaw, headYaw) + */ + protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) { + Vector3f bedrockPos = javaPos.toFloat(); + + MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); + moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId()); + + if (vehicle.isOnGround()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); + } + + if (vehicle.getPosition().getX() != bedrockPos.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X); + moveEntityDeltaPacket.setX(bedrockPos.getX()); + } + if (vehicle.getPosition().getY() != bedrockPos.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); + moveEntityDeltaPacket.setY(bedrockPos.getY()); + } + if (vehicle.getPosition().getZ() != bedrockPos.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); + moveEntityDeltaPacket.setZ(bedrockPos.getZ()); + } + vehicle.setPosition(bedrockPos); + + if (vehicle.getPitch() != lastRotation.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); + moveEntityDeltaPacket.setPitch(vehicle.getPitch()); + } + if (vehicle.getYaw() != lastRotation.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + moveEntityDeltaPacket.setYaw(vehicle.getYaw()); + } + if (vehicle.getHeadYaw() != lastRotation.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); + moveEntityDeltaPacket.setHeadYaw(vehicle.getHeadYaw()); + } + + if (!moveEntityDeltaPacket.getFlags().isEmpty()) { + vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket); + } + + ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, vehicle.getYaw(), vehicle.getPitch(), vehicle.isOnGround()); + vehicle.getSession().sendDownstreamPacket(moveVehiclePacket); + } + + protected double getGravity() { + if (!vehicle.getFlag(EntityFlag.HAS_GRAVITY)) { + return 0; + } + + if (vehicle.getMotion().getY() <= 0 && effectSlowFalling) { + return Math.min(0.01, this.gravity); + } + + return this.gravity; + } + + /** + * Finds the position of the main block supporting the vehicle. + * Used when determining slipperiness, speed, etc. + *

+ * Should use {@link VehicleContext#supportingBlockPos()}, instead of calling this directly. + * + * @param ctx context + * @return position of the main block supporting this entity + */ + private @Nullable Vector3i getSupportingBlockPos(VehicleContext ctx) { + Vector3i result = null; + + if (vehicle.isOnGround()) { + BoundingBox box = boundingBox.clone(); + box.extend(0, -1.0E-6, 0); // Extend slightly down + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + + // Use minY as maxY + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), min.getY(), max.getZ()); + + double minDistance = Double.MAX_VALUE; + for (iter.reset(); iter.hasNext(); iter.next()) { + Vector3i blockPos = Vector3i.from(iter.getX(), iter.getY(), iter.getZ()); + int blockId = ctx.getBlockId(iter); + + BlockCollision blockCollision; + if (vehicle.canWalkOnLava()) { + blockCollision = vehicle.getSession().getCollisionManager().getCollisionLavaWalking(blockId, blockPos.getY(), boundingBox); + } else { + blockCollision = BlockUtils.getCollision(blockId); + } + + if (blockCollision != null && blockCollision.checkIntersection(blockPos, box)) { + double distance = ctx.centerPos().distanceSquared(blockPos.toDouble().add(0.5f, 0.5f, 0.5f)); + if (distance <= minDistance) { + minDistance = distance; + result = blockPos; + } + } + } + } + + return result; + } + + /** + * Returns the block that is x amount of blocks under the main supporting block. + */ + protected BlockState getBlockUnderSupport(VehicleContext ctx, float dist) { + Vector3i supportingBlockPos = ctx.supportingBlockPos(); + + Vector3i blockPos; + if (supportingBlockPos != null) { + blockPos = Vector3i.from(supportingBlockPos.getX(), Math.floor(ctx.centerPos().getY() - dist), supportingBlockPos.getZ()); + } else { + blockPos = ctx.centerPos().sub(0, dist, 0).toInt(); + } + + return ctx.getBlock(blockPos); + } + + /** + * The block to use when determining if the vehicle should bounce after landing. Currently just slime and bed blocks. + */ + protected BlockState getLandingBlock(VehicleContext ctx) { + return getBlockUnderSupport(ctx, 0.2f); + } + + /** + * The block to use when calculating slipperiness and speed. If on a slab, this will be the block under the slab. + */ + protected BlockState getVelocityBlock(VehicleContext ctx) { + return getBlockUnderSupport(ctx, 0.500001f); + } + + protected float getVelocityMultiplier(VehicleContext ctx) { + Block block = ctx.centerBlock().block(); + if (block == Blocks.WATER || block == Blocks.BUBBLE_COLUMN) { + return 1.0f; + } + + if (block == Blocks.SOUL_SAND || block == Blocks.HONEY_BLOCK) { + return 0.4f; + } + + block = getVelocityBlock(ctx).block(); + if (block == Blocks.SOUL_SAND || block == Blocks.HONEY_BLOCK) { + return 0.4f; + } + + return 1.0f; + } + + protected float getJumpVelocityMultiplier(VehicleContext ctx) { + Block block = ctx.centerBlock().block(); + if (block == Blocks.HONEY_BLOCK) { + return 0.5f; + } + + block = getVelocityBlock(ctx).block(); + if (block == Blocks.HONEY_BLOCK) { + return 0.5f; + } + + return 1.0f; + } + + protected class VehicleContext { + private Vector3d centerPos; + private Vector3d cachePos; + private BlockState centerBlock; + private Vector3i supportingBlockPos; + private BlockPositionIterator blockIter; + private int[] blocks; + + /** + * Cache frequently used data and blocks used in movement calculations. + *

+ * Can be called multiple times, and must be called at least once before using the VehicleContext. + */ + protected void loadSurroundingBlocks() { + this.centerPos = boundingBox.getBottomCenter(); + + // Reuse block cache if vehicle moved less than 1 block + if (this.cachePos == null || this.cachePos.distanceSquared(this.centerPos) > 1) { + BoundingBox box = boundingBox.clone(); + box.expand(2); + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + this.blockIter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); + this.blocks = vehicle.getSession().getGeyser().getWorldManager().getBlocksAt(vehicle.getSession(), this.blockIter); + + this.cachePos = this.centerPos; + } + + this.centerBlock = getBlock(this.centerPos.toInt()); + this.supportingBlockPos = null; + } + + protected Vector3d centerPos() { + return this.centerPos; + } + + protected BlockState centerBlock() { + return this.centerBlock; + } + + protected Vector3i supportingBlockPos() { + if (this.supportingBlockPos == null) { + this.supportingBlockPos = getSupportingBlockPos(this); + } + + return this.supportingBlockPos; + } + + protected int getBlockId(int x, int y, int z) { + int index = this.blockIter.getIndex(x, y, z); + if (index == -1) { + vehicle.getSession().getGeyser().getLogger().debug("[client-vehicle] Block cache miss"); + return vehicle.getSession().getGeyser().getWorldManager().getBlockAt(vehicle.getSession(), x, y, z); + } + + return blocks[index]; + } + + protected int getBlockId(Vector3i pos) { + return getBlockId(pos.getX(), pos.getY(), pos.getZ()); + } + + protected int getBlockId(BlockPositionIterator iter) { + return getBlockId(iter.getX(), iter.getY(), iter.getZ()); + } + + protected BlockState getBlock(int x, int y, int z) { + return BlockState.of(getBlockId(x, y, z)); + } + + protected BlockState getBlock(Vector3i pos) { + return BlockState.of(getBlockId(pos.getX(), pos.getY(), pos.getZ())); + } + + protected BlockState getBlock(BlockPositionIterator iter) { + return BlockState.of(getBlockId(iter.getX(), iter.getY(), iter.getZ())); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java b/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java new file mode 100644 index 000000000..ae283895b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.erosion; + +import java.io.Serial; +import java.util.concurrent.CancellationException; + +public class ErosionCancellationException extends CancellationException { + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java index 0b4f03643..88a7e0cd3 100644 --- a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java @@ -42,7 +42,6 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound public void handleHandshake(GeyserboundHandshakePacket packet) { boolean useTcp = packet.getTransportType().getSocketAddress() == null; GeyserboundPacketHandlerImpl handler = new GeyserboundPacketHandlerImpl(session, useTcp ? new GeyserErosionPacketSender(session) : new NettyPacketSender<>()); - session.setErosionHandler(handler); if (!useTcp) { if (session.getGeyser().getErosionUnixListener() == null) { session.disconnect("Erosion configurations using Unix socket handling are not supported on this hardware!"); @@ -52,6 +51,7 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound } else { handler.onConnect(); } + session.setErosionHandler(handler); session.ensureInEventLoop(() -> session.getChunkCache().clear()); } diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java index 3ae458f63..7202db449 100644 --- a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java @@ -25,14 +25,13 @@ package org.geysermc.geyser.erosion; -import com.github.steveice10.mc.protocol.data.game.level.block.value.PistonValueType; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import io.netty.channel.Channel; +import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import lombok.Getter; import lombok.Setter; import org.cloudburstmc.math.vector.Vector3i; @@ -45,12 +44,16 @@ import org.geysermc.erosion.packet.backendbound.BackendboundInitializePacket; import org.geysermc.erosion.packet.backendbound.BackendboundPacket; import org.geysermc.erosion.packet.geyserbound.*; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PistonCache; import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity; import org.geysermc.geyser.util.BlockEntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValueType; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; @@ -64,7 +67,7 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke @Setter private CompletableFuture pendingBatchLookup = null; @Setter - private CompletableFuture pickBlockLookup = null; + private CompletableFuture> pickBlockLookup = null; private final AtomicInteger nextTransactionId = new AtomicInteger(1); @@ -120,7 +123,7 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke } CompletableFuture future = this.asyncPendingLookups.remove(transactionId); if (future != null) { - future.complete(BlockStateValues.JAVA_AIR_ID); + future.complete(Block.JAVA_AIR_ID); } } @@ -134,28 +137,29 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke placeBlockSoundPacket.setIdentifier(":"); session.sendUpstreamPacket(placeBlockSoundPacket); session.setLastBlockPlacePosition(null); - session.setLastBlockPlacedId(null); + session.setLastBlockPlaced(null); } @Override public void handlePickBlock(GeyserboundPickBlockPacket packet) { if (this.pickBlockLookup != null) { - this.pickBlockLookup.complete(packet.getTag()); + this.pickBlockLookup.complete(packet.getComponents()); } } @Override public void handlePistonEvent(GeyserboundPistonEventPacket packet) { - Direction orientation = BlockStateValues.getPistonOrientation(packet.getBlockId()); + Direction orientation = BlockState.of(packet.getBlockId()).getValue(Properties.FACING); Vector3i position = packet.getPos(); boolean isExtend = packet.isExtend(); var stream = packet.getAttachedBlocks() .object2IntEntrySet() .stream() - .filter(entry -> BlockStateValues.canPistonMoveBlock(entry.getIntValue(), isExtend)); - Object2IntMap attachedBlocks = new Object2IntArrayMap<>(); - stream.forEach(entry -> attachedBlocks.put(entry.getKey(), entry.getIntValue())); + .map(entry -> Pair.of(entry.getKey(), BlockState.of(entry.getIntValue()))) + .filter(pair -> BlockStateValues.canPistonMoveBlock(pair.value(), isExtend)); + Object2ObjectMap attachedBlocks = new Object2ObjectArrayMap<>(); + stream.forEach(pair -> attachedBlocks.put(pair.key(), pair.value())); session.executeInEventLoop(() -> { PistonCache pistonCache = session.getPistonCache(); @@ -167,10 +171,10 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke @Override public void handleHandshake(GeyserboundHandshakePacket packet) { - this.close(); var handler = new GeyserboundHandshakePacketHandler(this.session); session.setErosionHandler(handler); handler.handleHandshake(packet); + this.close(); } @Override @@ -194,6 +198,17 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke public void close() { this.packetSender.close(); + + if (pendingLookup != null) { + pendingLookup.completeExceptionally(new ErosionCancellationException()); + } + if (pendingBatchLookup != null) { + pendingBatchLookup.completeExceptionally(new ErosionCancellationException()); + } + if (pickBlockLookup != null) { + pickBlockLookup.completeExceptionally(new ErosionCancellationException()); + } + asyncPendingLookups.forEach(($, future) -> future.completeExceptionally(new ErosionCancellationException())); } public int getNextTransactionId() { diff --git a/core/src/main/java/org/geysermc/geyser/event/GeyserEventBus.java b/core/src/main/java/org/geysermc/geyser/event/GeyserEventBus.java index d8657b612..ddebf02e4 100644 --- a/core/src/main/java/org/geysermc/geyser/event/GeyserEventBus.java +++ b/core/src/main/java/org/geysermc/geyser/event/GeyserEventBus.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.event; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.event.Event; +import org.geysermc.event.FireResult; import org.geysermc.event.PostOrder; import org.geysermc.event.bus.impl.OwnedEventBusImpl; import org.geysermc.event.subscribe.OwnedSubscriber; @@ -34,6 +35,7 @@ import org.geysermc.event.subscribe.Subscribe; import org.geysermc.geyser.api.event.EventBus; import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.event.EventSubscriber; +import org.geysermc.geyser.session.GeyserSession; import java.util.Set; import java.util.function.BiConsumer; @@ -68,4 +70,11 @@ public final class GeyserEventBus extends OwnedEventBusImpl Set> subscribers(@NonNull Class eventClass) { return castGenericSet(super.subscribers(eventClass)); } + + public void fireEventElseKick(@NonNull Event event, GeyserSession session) { + FireResult result = this.fire(event); + if (!result.success()) { + session.disconnect("Internal server error occurred! Please contact a server administrator."); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java index e07a62d8a..4a6efbbd4 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java @@ -35,12 +35,12 @@ import java.util.Map; public abstract class GeyserDefineCommandsEventImpl implements GeyserDefineCommandsEvent { private final Map commands; - public GeyserDefineCommandsEventImpl(Map commands) { - this.commands = commands; + public GeyserDefineCommandsEventImpl(Map commands) { + this.commands = Collections.unmodifiableMap(commands); } @Override public @NonNull Map commands() { - return Collections.unmodifiableMap(this.commands); + return this.commands; } } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java new file mode 100644 index 000000000..2e62a4482 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.event.type; + +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +@Getter +public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent { + private final Map packs; + + public GeyserDefineResourcePacksEventImpl(Map packMap) { + this.packs = packMap; + } + + @Override + public @NonNull List resourcePacks() { + return packs.values().stream().map(ResourcePackHolder::resourcePack).toList(); + } + + @Override + public void register(@NonNull ResourcePack resourcePack, @Nullable ResourcePackOption... options) { + Objects.requireNonNull(resourcePack, "resource pack must not be null!"); + if (!(resourcePack instanceof GeyserResourcePack pack)) { + throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION); + } + + UUID uuid = resourcePack.uuid(); + if (packs.containsKey(uuid)) { + throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE); + } + + ResourcePackHolder holder = ResourcePackHolder.of(pack); + attemptRegisterOptions(holder, options); + packs.put(uuid, holder); + } + + @Override + public void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(options); + + ResourcePackHolder holder = packs.get(uuid); + if (holder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + attemptRegisterOptions(holder, options); + } + + @Override + public Collection> options(@NonNull UUID uuid) { + Objects.requireNonNull(uuid); + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + return packHolder.optionHolder().immutableValues(); + } + + @Override + public @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(type); + + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + return packHolder.optionHolder().get(type); + } + + @Override + public void unregister(@NonNull UUID uuid) { + packs.remove(uuid); + } + + private void attemptRegisterOptions(@NonNull ResourcePackHolder holder, @Nullable ResourcePackOption... options) { + if (options == null) { + return; + } + + holder.optionHolder().validateAndAdd(holder.pack(), options); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionDisconnectEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionDisconnectEventImpl.java new file mode 100644 index 000000000..c845b2399 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionDisconnectEventImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.event.type; + +import lombok.Getter; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.text.MessageTranslator; + +/** + * A wrapper around the {@link SessionDisconnectEvent} that allows + * Geyser to access the underlying component when replacing disconnect messages. + */ +@Getter +public class SessionDisconnectEventImpl extends SessionDisconnectEvent { + + private final Component reasonComponent; + + public SessionDisconnectEventImpl(@NonNull GeyserSession session, Component reason) { + super(session, MessageTranslator.convertMessageRaw(reason, session.locale())); + this.reasonComponent = reason; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index 5ed0f8d22..241f5d170 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -25,45 +25,216 @@ package org.geysermc.geyser.event.type; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; +import org.geysermc.geyser.pack.option.OptionHolder; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { - private final Map packs; + /** + * The packs for this Session. A {@link ResourcePackHolder} may contain resource pack options registered + * during the {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent}. + */ + @Getter + private final Map packs; - public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap) { + /** + * The additional, per-session options for the resource packs of this session. + * These options are prioritized over the "default" options registered + * in the {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent} + */ + private final Map sessionPackOptionOverrides; + + private final GeyserSession session; + + public SessionLoadResourcePacksEventImpl(GeyserSession session) { super(session); - this.packs = packMap; - } - - public @NonNull Map getPacks() { - return packs; + this.session = session; + this.packs = new Object2ObjectLinkedOpenHashMap<>(Registries.RESOURCE_PACKS.get()); + this.sessionPackOptionOverrides = new Object2ObjectOpenHashMap<>(); } @Override public @NonNull List resourcePacks() { - return List.copyOf(packs.values()); + return packs.values().stream().map(ResourcePackHolder::resourcePack).toList(); } @Override public boolean register(@NonNull ResourcePack resourcePack) { - String packID = resourcePack.manifest().header().uuid().toString(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { + try { + register(resourcePack, PriorityOption.NORMAL); + } catch (ResourcePackException e) { + GeyserImpl.getInstance().getLogger().error("An exception occurred while registering resource pack: " + e.getMessage(), e); return false; } - packs.put(resourcePack.manifest().header().uuid().toString(), resourcePack); return true; } + @Override + public void register(@NonNull ResourcePack resourcePack, @Nullable ResourcePackOption... options) { + Objects.requireNonNull(resourcePack); + if (!(resourcePack instanceof GeyserResourcePack pack)) { + throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION); + } + + UUID uuid = resourcePack.uuid(); + if (packs.containsKey(uuid)) { + throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE); + } + + attemptRegisterOptions(pack, options); + packs.put(uuid, ResourcePackHolder.of(pack)); + } + + @Override + public void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options) { + Objects.requireNonNull(uuid, "uuid cannot be null"); + Objects.requireNonNull(options, "options cannot be null"); + ResourcePackHolder holder = packs.get(uuid); + if (holder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + attemptRegisterOptions(holder.pack(), options); + } + + @Override + public Collection> options(@NonNull UUID uuid) { + Objects.requireNonNull(uuid); + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + OptionHolder optionHolder = sessionPackOptionOverrides.get(uuid); + if (optionHolder == null) { + // No need to create a new session option holder + return packHolder.optionHolder().immutableValues(); + } + + return optionHolder.immutableValues(packHolder.optionHolder()); + } + + @Override + public @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(type); + + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + @Nullable OptionHolder additionalOptions = sessionPackOptionOverrides.get(uuid); + OptionHolder defaultHolder = packHolder.optionHolder(); + Objects.requireNonNull(defaultHolder); // should never be null + + return OptionHolder.optionByType(type, additionalOptions, defaultHolder); + } + @Override public boolean unregister(@NonNull UUID uuid) { - return packs.remove(uuid.toString()) != null; + sessionPackOptionOverrides.remove(uuid); + return packs.remove(uuid) != null; + } + + @Override + public void allowVibrantVisuals(boolean enabled) { + session.setAllowVibrantVisuals(enabled); + } + + private void attemptRegisterOptions(@NonNull GeyserResourcePack pack, @Nullable ResourcePackOption... options) { + if (options == null) { + return; + } + + OptionHolder holder = this.sessionPackOptionOverrides.computeIfAbsent(pack.uuid(), $ -> new OptionHolder()); + holder.validateAndAdd(pack, options); + } + + // Methods used internally for e.g. ordered packs, or resource pack entries + + public List orderedPacks() { + return packs.values().stream() + // Map each ResourcePack to a pair of (GeyserResourcePack, Priority) + .map(holder -> new AbstractMap.SimpleEntry<>(holder.pack(), priority(holder.pack()))) + // Sort by priority in descending order + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + // Map the sorted entries to ResourcePackStackPacket.Entry + .map(entry -> { + ResourcePackManifest.Header header = entry.getKey().manifest().header(); + return new ResourcePackStackPacket.Entry( + header.uuid().toString(), + header.version().toString(), + subpackName(entry.getKey()) + ); + }) + .toList(); + } + + public List infoPacketEntries() { + List entries = new ArrayList<>(); + + for (ResourcePackHolder holder : packs.values()) { + GeyserResourcePack pack = holder.pack(); + ResourcePackManifest.Header header = pack.manifest().header(); + entries.add(new ResourcePacksInfoPacket.Entry( + header.uuid(), header.version().toString(), pack.codec().size(), pack.contentKey(), + subpackName(pack), header.uuid().toString(), false, false, false, cdnUrl(pack)) + ); + } + + return entries; + } + + // Helper methods to get the options for a ResourcePack + + public T value(UUID uuid, ResourcePackOption.Type type, T defaultValue) { + OptionHolder holder = sessionPackOptionOverrides.get(uuid); + OptionHolder defaultHolder = packs.get(uuid).optionHolder(); + Objects.requireNonNull(defaultHolder); // should never be null + + return OptionHolder.valueOrFallback(type, holder, defaultHolder, defaultValue); + } + + private double priority(GeyserResourcePack pack) { + return value(pack.uuid(), ResourcePackOption.Type.PRIORITY, PriorityOption.NORMAL.value()); + } + + private String subpackName(GeyserResourcePack pack) { + return value(pack.uuid(), ResourcePackOption.Type.SUBPACK, ""); + } + + private String cdnUrl(GeyserResourcePack pack) { + if (pack.codec() instanceof UrlPackCodec urlPackCodec) { + return urlPackCodec.url(); + } + return ""; } } diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java index dca11dfcd..a8f5c5584 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java @@ -40,11 +40,11 @@ import java.nio.file.Path; public class GeyserExtensionClassLoader extends URLClassLoader { private final GeyserExtensionLoader loader; - private final ExtensionDescription description; + private final GeyserExtensionDescription description; private final Object2ObjectMap> classes = new Object2ObjectOpenHashMap<>(); private boolean warnedForExternalClassAccess; - public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, ExtensionDescription description) throws MalformedURLException { + public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, GeyserExtensionDescription description) throws MalformedURLException { super(new URL[] { path.toUri().toURL() }, parent); this.loader = loader; this.description = description; @@ -89,7 +89,7 @@ public class GeyserExtensionClassLoader extends URLClassLoader { // If class is not found in current extension, check in the global class loader // This is used for classes that are not in the extension, but are in other extensions if (checkGlobal) { - if (!warnedForExternalClassAccess) { + if (!warnedForExternalClassAccess && this.description.dependencies().isEmpty()) { // Don't warn when the extension has dependencies, it is probably using it's dependencies! GeyserImpl.getInstance().getLogger().warning("Extension " + this.description.name() + " loads class " + name + " from an external source. " + "This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!"); warnedForExternalClassAccess = true; diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java index 239ffc450..702ab0f68 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java @@ -43,11 +43,12 @@ import java.util.regex.Pattern; public record GeyserExtensionDescription(@NonNull String id, @NonNull String name, @NonNull String main, + int humanApiVersion, int majorApiVersion, int minorApiVersion, - int patchApiVersion, @NonNull String version, - @NonNull List authors) implements ExtensionDescription { + @NonNull List authors, + @NonNull Map dependencies) implements ExtensionDescription { private static final Yaml YAML = new Yaml(new CustomClassLoaderConstructor(Source.class.getClassLoader(), new LoaderOptions())); @@ -82,9 +83,9 @@ public record GeyserExtensionDescription(@NonNull String id, throw new InvalidDescriptionException(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion)); } String[] api = apiVersion.split("\\."); - int majorApi = Integer.parseUnsignedInt(api[0]); - int minorApi = Integer.parseUnsignedInt(api[1]); - int patchApi = Integer.parseUnsignedInt(api[2]); + int humanApi = Integer.parseUnsignedInt(api[0]); + int majorApi = Integer.parseUnsignedInt(api[1]); + int minorApi = Integer.parseUnsignedInt(api[2]); List authors = new ArrayList<>(); if (source.author != null) { @@ -94,7 +95,12 @@ public record GeyserExtensionDescription(@NonNull String id, authors.addAll(source.authors); } - return new GeyserExtensionDescription(id, name, main, majorApi, minorApi, patchApi, version, authors); + Map dependencies = new LinkedHashMap<>(); + if (source.dependencies != null) { + dependencies.putAll(source.dependencies); + } + + return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors, dependencies); } @NonNull @@ -116,5 +122,17 @@ public record GeyserExtensionDescription(@NonNull String id, String version; String author; List authors; + Map dependencies; + } + + @Getter + @Setter + public static class Dependency { + boolean required = true; // Defaults to true + LoadOrder load = LoadOrder.BEFORE; // Defaults to ensure the dependency loads before this extension + } + + public enum LoadOrder { + BEFORE, AFTER } } diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java index 2f0ff1580..2be6d2f8f 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java @@ -29,27 +29,46 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.RequiredArgsConstructor; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.api.Geyser; +import org.geysermc.api.util.ApiVersion; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.event.ExtensionEventBus; -import org.geysermc.geyser.api.extension.*; +import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.extension.ExtensionDescription; +import org.geysermc.geyser.api.extension.ExtensionLoader; +import org.geysermc.geyser.api.extension.ExtensionLogger; +import org.geysermc.geyser.api.extension.ExtensionManager; import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; import org.geysermc.geyser.extension.event.GeyserExtensionEventBus; import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.ThrowingBiConsumer; import java.io.IOException; import java.io.Reader; -import java.nio.file.*; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.regex.Pattern; @RequiredArgsConstructor public class GeyserExtensionLoader extends ExtensionLoader { - private static final Pattern[] EXTENSION_FILTERS = new Pattern[] { Pattern.compile("^.+\\.jar$") }; + private static final Pattern EXTENSION_FILTER = Pattern.compile("^.+\\.jar$"); private final Object2ObjectMap> classes = new Object2ObjectOpenHashMap<>(); private final Map classLoaders = new HashMap<>(); @@ -119,8 +138,8 @@ public class GeyserExtensionLoader extends ExtensionLoader { } } - public Pattern[] extensionFilters() { - return EXTENSION_FILTERS; + public Pattern extensionFilter() { + return EXTENSION_FILTER; } public Class classByName(final String name) throws ClassNotFoundException{ @@ -145,6 +164,7 @@ public class GeyserExtensionLoader extends ExtensionLoader { @Override protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); try { if (Files.notExists(extensionsDirectory)) { Files.createDirectory(extensionsDirectory); @@ -152,59 +172,230 @@ public class GeyserExtensionLoader extends ExtensionLoader { Map extensions = new LinkedHashMap<>(); Map loadedExtensions = new LinkedHashMap<>(); + Map descriptions = new LinkedHashMap<>(); + Map extensionPaths = new LinkedHashMap<>(); - Pattern[] extensionFilters = this.extensionFilters(); - List extensionPaths = Files.walk(extensionsDirectory).toList(); - extensionPaths.forEach(path -> { - if (Files.isDirectory(path)) { + Path updateDirectory = extensionsDirectory.resolve("update"); + if (Files.isDirectory(updateDirectory)) { + // Step 1: Collect the extension files that currently exist so they can be replaced + Map> extensionFiles = new HashMap<>(); + this.processExtensionsFolder(extensionsDirectory, (path, description) -> { + extensionFiles.computeIfAbsent(description.id(), k -> new ArrayList<>()).add(path); + }, (path, e) -> { + // this file will throw again when we actually try to load extensions, and it will be handled there + }); + + // Step 2: Move the updated/new extensions + this.processExtensionsFolder(updateDirectory, (path, description) -> { + // Remove the old extension files with the same ID if it exists + List oldExtensionFiles = extensionFiles.get(description.id()); + if (oldExtensionFiles != null) { + for (Path oldExtensionFile : oldExtensionFiles) { + Files.delete(oldExtensionFile); + } + } + + // Overwrite the extension with the new jar + Files.move(path, extensionsDirectory.resolve(path.getFileName()), StandardCopyOption.REPLACE_EXISTING); + }, (path, e) -> { + logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.update.failed", path.getFileName()), e); + }); + } + + // Step 3: Order the extensions to allow dependencies to load in the correct order + this.processExtensionsFolder(extensionsDirectory, (path, description) -> { + String id = description.id(); + descriptions.put(id, description); + extensionPaths.put(id, path); + + }, (path, e) -> { + logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e); + }); + + // The graph to back out loading order (Funny I just learnt these too) + Map> loadOrderGraph = new HashMap<>(); + + // Looks like the graph needs to be prepopulated otherwise issues happen + for (String id : descriptions.keySet()) { + loadOrderGraph.putIfAbsent(id, new ArrayList<>()); + } + + for (GeyserExtensionDescription description : descriptions.values()) { + for (Map.Entry dependency : description.dependencies().entrySet()) { + String from = null; + String to = null; // Java complains if this isn't initialised, but not from, so, both null. + + // Check if the extension is even loaded + if (!descriptions.containsKey(dependency.getKey())) { + if (dependency.getValue().isRequired()) { // Only disable the extension if this dependency is required + // The extension we are checking is missing 1 or more dependencies + logger.error( + GeyserLocale.getLocaleStringLog( + "geyser.extensions.load.failed_dependency_missing", + description.id(), + dependency.getKey() + ) + ); + + descriptions.remove(description.id()); // Prevents it from being loaded later + } + + continue; + } + + if ( + !(description.humanApiVersion() >= 2 && + description.majorApiVersion() >= 9 && + description.minorApiVersion() >= 0) + ) { + logger.error( + GeyserLocale.getLocaleStringLog( + "geyser.extensions.load.failed_cannot_use_dependencies", + description.id(), + description.apiVersion() + ) + ); + + descriptions.remove(description.id()); // Prevents it from being loaded later + + continue; + } + + // Determine which way they should go in the graph + switch (dependency.getValue().getLoad()) { + case BEFORE -> { + from = dependency.getKey(); + to = description.id(); + } + case AFTER -> { + from = description.id(); + to = dependency.getKey(); + } + } + + loadOrderGraph.get(from).add(to); + } + } + + Set visited = new HashSet<>(); + List visiting = new ArrayList<>(); + List loadOrder = new ArrayList<>(); + + AtomicReference> sortMethod = new AtomicReference<>(); // yay, lambdas. This doesn't feel to suited to be a method + sortMethod.set((node) -> { + if (visiting.contains(node)) { + logger.error( + GeyserLocale.getLocaleStringLog( + "geyser.extensions.load.failed_cyclical_dependencies", + node, + visiting.get(visiting.indexOf(node) - 1) + ) + ); + + visiting.remove(node); return; } - for (Pattern filter : extensionFilters) { - if (!filter.matcher(path.getFileName().toString()).matches()) { + if (visited.contains(node)) return; + + visiting.add(node); + for (String neighbor : loadOrderGraph.get(node)) { + sortMethod.get().accept(neighbor); + } + visiting.remove(node); + visited.add(node); + loadOrder.add(node); + }); + + for (String ext : descriptions.keySet()) { + if (!visited.contains(ext)) { + // Time to sort the graph to get a load order, this reveals any cycles we may have + sortMethod.get().accept(ext); + } + } + Collections.reverse(loadOrder); // This is inverted due to how the graph is created + + // Step 4: Load the extensions + for (String id : loadOrder) { + // Grab path and description found from before, since we want a custom load order now + Path path = extensionPaths.get(id); + GeyserExtensionDescription description = descriptions.get(id); + + String name = description.name(); + if (extensions.containsKey(id) || extensionManager.extension(id) != null) { + logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString())); + return; + } + + // Check whether an extensions' requested api version is compatible + ApiVersion.Compatibility compatibility = GeyserApi.api().geyserApiVersion().supportsRequestedVersion( + description.humanApiVersion(), + description.majorApiVersion(), + description.minorApiVersion() + ); + + if (compatibility != ApiVersion.Compatibility.COMPATIBLE) { + // Workaround for the switch to the Geyser API version instead of the Base API version in extensions + if (compatibility == ApiVersion.Compatibility.HUMAN_DIFFER && description.humanApiVersion() == 1) { + logger.warning("The extension %s requested the Base API version %s, which is deprecated in favor of specifying the Geyser API version. Please update the extension, or contact its developer." + .formatted(name, description.apiVersion())); + } else { + logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); return; } } try { - GeyserExtensionDescription description = this.extensionDescription(path); - - String name = description.name(); - String id = description.id(); - if (extensions.containsKey(id) || extensionManager.extension(id) != null) { - GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString())); - return; - } - - // Completely different API version - if (description.majorApiVersion() != Geyser.api().majorApiVersion()) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); - return; - } - - // If the extension requires new API features, being backwards compatible - if (description.minorApiVersion() > Geyser.api().minorApiVersion()) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); - return; - } - GeyserExtensionContainer container = this.loadExtension(path, description); extensions.put(id, path); loadedExtensions.put(id, container); } catch (Throwable e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e); + logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e); } - }); + } + // Step 5: Register the extensions for (GeyserExtensionContainer container : loadedExtensions.values()) { this.extensionContainers.put(container.extension(), container); this.register(container.extension(), extensionManager); } } catch (IOException ex) { - ex.printStackTrace(); + logger.error("Unable to read extensions.", ex); } } + /** + * Process extension jars in a folder and call the accept or reject consumer based on the result + * + * @param directory the directory to process + * @param accept the consumer to call when an extension is accepted + * @param reject the consumer to call when an extension is rejected + * @throws IOException if an I/O error occurs + */ + private void processExtensionsFolder(Path directory, ThrowingBiConsumer accept, BiConsumer reject) throws IOException { + List extensionPaths = Files.list(directory).toList(); + Pattern extensionFilter = this.extensionFilter(); + extensionPaths.forEach(path -> { + if (Files.isDirectory(path)) { + return; + } + + // Only look at files that meet the extension filter + if (!extensionFilter.matcher(path.getFileName().toString()).matches()) { + return; + } + + try { + // Try load the description, so we know it's a valid extension + GeyserExtensionDescription description = this.extensionDescription(path); + + accept.acceptThrows(path, description); + } catch (Throwable e) { + reject.accept(path, e); + } + }); + } + @Override protected boolean isEnabled(@NonNull Extension extension) { return this.extensionContainers.get(extension).enabled; diff --git a/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java b/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java index 4a7830c90..0dc49f5c1 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java +++ b/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java @@ -25,19 +25,215 @@ package org.geysermc.geyser.extension.command; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.command.CommandExecutor; +import org.geysermc.geyser.api.command.CommandSource; +import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.CommandDescription; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; public abstract class GeyserExtensionCommand extends GeyserCommand { + private final Extension extension; + private final String rootCommand; - public GeyserExtensionCommand(Extension extension, String name, String description, String permission) { - super(name, description, permission); + public GeyserExtensionCommand(@NonNull Extension extension, @NonNull String name, @NonNull String description, + @NonNull String permission, @Nullable TriState permissionDefault, + boolean playerOnly, boolean bedrockOnly) { + super(name, description, permission, permissionDefault, playerOnly, bedrockOnly); this.extension = extension; + this.rootCommand = Objects.requireNonNull(extension.rootCommand()); + + if (this.rootCommand.isBlank()) { + throw new IllegalStateException("rootCommand of extension " + extension.name() + " may not be blank"); + } } - public Extension extension() { + public final Extension extension() { return this.extension; } + + @Override + public final String rootCommand() { + return this.rootCommand; + } + + public static class Builder implements Command.Builder { + @NonNull private final Extension extension; + @Nullable private Class sourceType; + @Nullable private String name; + @NonNull private String description = ""; + @NonNull private String permission = ""; + @Nullable private TriState permissionDefault; + @Nullable private List aliases; + private boolean suggestedOpOnly = false; // deprecated for removal + private boolean playerOnly = false; + private boolean bedrockOnly = false; + @Nullable private CommandExecutor executor; + + public Builder(@NonNull Extension extension) { + this.extension = Objects.requireNonNull(extension); + } + + @Override + public Command.Builder source(@NonNull Class sourceType) { + this.sourceType = Objects.requireNonNull(sourceType, "command source type"); + return this; + } + + @Override + public Builder name(@NonNull String name) { + this.name = Objects.requireNonNull(name, "command name"); + return this; + } + + @Override + public Builder description(@NonNull String description) { + this.description = Objects.requireNonNull(description, "command description"); + return this; + } + + @Override + public Builder permission(@NonNull String permission) { + this.permission = Objects.requireNonNull(permission, "command permission"); + return this; + } + + @Override + public Builder permission(@NonNull String permission, @NonNull TriState defaultValue) { + this.permission = Objects.requireNonNull(permission, "command permission"); + this.permissionDefault = Objects.requireNonNull(defaultValue, "command permission defaultValue"); + return this; + } + + @Override + public Builder aliases(@NonNull List aliases) { + this.aliases = Objects.requireNonNull(aliases, "command aliases"); + return this; + } + + @SuppressWarnings("removal") // this is our doing + @Override + public Builder suggestedOpOnly(boolean suggestedOpOnly) { + this.suggestedOpOnly = suggestedOpOnly; + if (suggestedOpOnly) { + // the most amount of legacy/deprecated behaviour I'm willing to support + this.permissionDefault = TriState.NOT_SET; + } + return this; + } + + @SuppressWarnings("removal") // this is our doing + @Override + public Builder executableOnConsole(boolean executableOnConsole) { + this.playerOnly = !executableOnConsole; + return this; + } + + @Override + public Command.Builder playerOnly(boolean playerOnly) { + this.playerOnly = playerOnly; + return this; + } + + @Override + public Builder bedrockOnly(boolean bedrockOnly) { + this.bedrockOnly = bedrockOnly; + return this; + } + + @Override + public Builder executor(@NonNull CommandExecutor executor) { + this.executor = Objects.requireNonNull(executor, "command executor"); + return this; + } + + @NonNull + @Override + public GeyserExtensionCommand build() { + // These are captured in the anonymous lambda below and shouldn't change even if the builder does + final Class sourceType = this.sourceType; + final boolean suggestedOpOnly = this.suggestedOpOnly; + final CommandExecutor executor = this.executor; + + if (name == null) { + throw new IllegalArgumentException("name was not provided for a command in extension " + extension.name()); + } + if (sourceType == null) { + throw new IllegalArgumentException("Source type was not defined for command " + name + " in extension " + extension.name()); + } + if (executor == null) { + throw new IllegalArgumentException("Command executor was not defined for command " + name + " in extension " + extension.name()); + } + + // if the source type is a GeyserConnection then it is inherently bedrockOnly + final boolean bedrockOnly = this.bedrockOnly || GeyserConnection.class.isAssignableFrom(sourceType); + // a similar check would exist for executableOnConsole, but there is not a logger type exposed in the api + + GeyserExtensionCommand command = new GeyserExtensionCommand(extension, name, description, permission, permissionDefault, playerOnly, bedrockOnly) { + + @Override + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .optional("args", greedyStringParser()) + .handler(this::execute)); + } + + @Override + protected org.incendo.cloud.Command.Builder.Applicable meta() { + // We don't want to localize the extension command description + return builder -> builder.commandDescription(CommandDescription.commandDescription(description)); + } + + @SuppressWarnings("unchecked") + @Override + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String[] args = context.getOrDefault("args", " ").split(" "); + + if (sourceType.isInstance(source)) { + executor.execute((T) source, this, args); + return; + } + + @Nullable GeyserSession session = source.connection(); + if (sourceType.isInstance(session)) { + executor.execute((T) session, this, args); + return; + } + + // currently, the only subclass of CommandSource exposed in the api is GeyserConnection. + // when this command was registered, we enabled bedrockOnly if the sourceType was a GeyserConnection. + // as a result, the permission checker should handle that case and this method shouldn't even be reached. + source.sendMessage("You must be a " + sourceType.getSimpleName() + " to run this command."); + } + + @SuppressWarnings("removal") // this is our doing + @Override + public boolean isSuggestedOpOnly() { + return suggestedOpOnly; + } + }; + + if (aliases != null) { + command.aliases = new ArrayList<>(aliases); + } + return command; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java new file mode 100644 index 000000000..ca0324665 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.impl; + +import net.kyori.adventure.key.Key; +import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.util.MinecraftKey; + +import java.util.Objects; + +public record IdentifierImpl(Key identifier) implements Identifier { + + public static IdentifierImpl of(String namespace, String value) throws IllegalArgumentException { + Objects.requireNonNull(namespace, "namespace cannot be null!"); + Objects.requireNonNull(value, "value cannot be null!"); + try { + return new IdentifierImpl(MinecraftKey.key(namespace, value)); + } catch (Throwable e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + // FIXME using the identifier interface from the API breaks tests + public static IdentifierImpl of(String value) { + return of(Identifier.DEFAULT_NAMESPACE, value); + } + + @Override + public String namespace() { + return identifier.namespace(); + } + + @Override + public String path() { + return identifier.value(); + } + + @Override + public String toString() { + return identifier.toString(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java index 80564bdf3..346b971cc 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java @@ -43,13 +43,14 @@ public class CameraDefinitions { static { CAMERA_PRESETS = List.of( - new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(false)), - new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.of(true)), - new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(true))); + CameraPreset.builder().identifier(CameraPerspective.FIRST_PERSON.id()).build(), + CameraPreset.builder().identifier(CameraPerspective.FREE.id()).build(), + CameraPreset.builder().identifier(CameraPerspective.THIRD_PERSON.id()).build(), + CameraPreset.builder().identifier(CameraPerspective.THIRD_PERSON_FRONT.id()).build(), + CameraPreset.builder().identifier("geyser:free_audio").parentPreset(CameraPerspective.FREE.id()).listener(CameraAudioListener.PLAYER).playEffect(OptionalBoolean.of(false)).build(), + CameraPreset.builder().identifier("geyser:free_effects").parentPreset(CameraPerspective.FREE.id()).listener(CameraAudioListener.CAMERA).playEffect(OptionalBoolean.of(true)).build(), + CameraPreset.builder().identifier("geyser:free_audio_effects").parentPreset(CameraPerspective.FREE.id()).listener(CameraAudioListener.PLAYER).playEffect(OptionalBoolean.of(true)).build() + ); SimpleDefinitionRegistry.Builder builder = SimpleDefinitionRegistry.builder(); for (int i = 0; i < CAMERA_PRESETS.size(); i++) { diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java index 28c881eba..759828744 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java +++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java @@ -32,19 +32,24 @@ import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.CameraShakeAction; import org.cloudburstmc.protocol.bedrock.data.CameraShakeType; +import org.cloudburstmc.protocol.bedrock.data.HudElement; +import org.cloudburstmc.protocol.bedrock.data.HudVisibility; import org.cloudburstmc.protocol.bedrock.data.camera.CameraEase; import org.cloudburstmc.protocol.bedrock.data.camera.CameraFadeInstruction; import org.cloudburstmc.protocol.bedrock.data.camera.CameraSetInstruction; import org.cloudburstmc.protocol.bedrock.packet.CameraInstructionPacket; import org.cloudburstmc.protocol.bedrock.packet.CameraShakePacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerFogPacket; -import org.geysermc.geyser.api.bedrock.camera.CameraEaseType; +import org.cloudburstmc.protocol.bedrock.packet.SetHudPacket; import org.geysermc.geyser.api.bedrock.camera.CameraData; +import org.geysermc.geyser.api.bedrock.camera.CameraEaseType; import org.geysermc.geyser.api.bedrock.camera.CameraFade; import org.geysermc.geyser.api.bedrock.camera.CameraPerspective; import org.geysermc.geyser.api.bedrock.camera.CameraPosition; import org.geysermc.geyser.api.bedrock.camera.CameraShake; +import org.geysermc.geyser.api.bedrock.camera.GuiElement; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import java.util.Collections; import java.util.HashSet; @@ -53,12 +58,26 @@ import java.util.Set; import java.util.UUID; public class GeyserCameraData implements CameraData { + private static final HudElement[] HUD_ELEMENT_VALUES = HudElement.values(); + private static final Set ALL_HUD_ELEMENTS = Set.of(HUD_ELEMENT_VALUES); + + /** + * An array of elements to hide when the player is in spectator mode. + * Helps with tidying up the GUI; Java-style. + */ + private static final GuiElement[] SPECTATOR_HIDDEN_ELEMENTS = { + GuiElement.AIR_BUBBLES_BAR, + GuiElement.ARMOR, + GuiElement.HEALTH, + GuiElement.FOOD_BAR, + GuiElement.PROGRESS_BAR, + GuiElement.TOOL_TIPS, + GuiElement.PAPER_DOLL, + GuiElement.VEHICLE_HEALTH + }; private final GeyserSession session; - @Getter - private CameraPerspective cameraPerspective; - /** * All fog effects that are currently applied to the client. */ @@ -66,6 +85,14 @@ public class GeyserCameraData implements CameraData { private final Set cameraLockOwners = new HashSet<>(); + /** + * All currently hidden HUD elements + */ + private final Set hiddenHudElements = new HashSet<>(); + + @Getter + private CameraPerspective cameraPerspective; + public GeyserCameraData(GeyserSession session) { this.session = session; } @@ -232,4 +259,72 @@ public class GeyserCameraData implements CameraData { public boolean isCameraLocked() { return !this.cameraLockOwners.isEmpty(); } -} \ No newline at end of file + + @Override + public void hideElement(GuiElement... elements) { + Objects.requireNonNull(elements); + SetHudPacket packet = new SetHudPacket(); + packet.setVisibility(HudVisibility.HIDE); + Set elementSet = packet.getElements(); + + for (GuiElement element : elements) { + this.hiddenHudElements.add(element); + elementSet.add(HUD_ELEMENT_VALUES[element.id()]); + } + + if (session.isSentSpawnPacket()) { + session.sendUpstreamPacket(packet); + } else { + // Ensures hidden GUI elements properly hide when we spawn in the spectator gamemode + session.getUpstream().queuePostStartGamePacket(packet); + } + } + + @Override + public void resetElement(GuiElement... elements) { + SetHudPacket packet = new SetHudPacket(); + packet.setVisibility(HudVisibility.RESET); + Set elementSet = packet.getElements(); + + if (elements != null && elements.length != 0) { + for (GuiElement element : elements) { + this.hiddenHudElements.remove(element); + elementSet.add(HUD_ELEMENT_VALUES[element.id()]); + } + } else { + this.hiddenHudElements.clear(); + elementSet.addAll(ALL_HUD_ELEMENTS); + } + + session.sendUpstreamPacket(packet); + } + + @Override + public boolean isHudElementHidden(@NonNull GuiElement element) { + Objects.requireNonNull(element); + return this.hiddenHudElements.contains(element); + } + + @Override + public @NonNull Set hiddenElements() { + return Collections.unmodifiableSet(hiddenHudElements); + } + + /** + * Deals with hiding hud elements while in spectator. + * + * @param currentlySpectator whether the player is currently in spectator mode + * @param newGameMode the new GameMode to switch to + */ + public void handleGameModeChange(boolean currentlySpectator, GameMode newGameMode) { + if (newGameMode == GameMode.SPECTATOR) { + if (!currentlySpectator) { + hideElement(SPECTATOR_HIDDEN_ELEMENTS); + } + } else { + if (currentlySpectator) { + resetElement(SPECTATOR_HIDDEN_ELEMENTS); + } + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java index 648e70c81..f69504545 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java +++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java @@ -29,7 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.common.value.qual.IntRange; import org.geysermc.geyser.api.bedrock.camera.CameraFade; -import java.awt.Color; +import java.awt.*; import java.util.Objects; public record GeyserCameraFade( diff --git a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java index 9e0b83768..cc852d3bf 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java @@ -25,14 +25,15 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket; import lombok.Getter; import lombok.Setter; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket; /** * Used to determine if rename packets should be sent and stores @@ -61,8 +62,8 @@ public class AnvilContainer extends Container { private int lastTargetSlot = -1; - public AnvilContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public AnvilContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } /** @@ -72,9 +73,9 @@ public class AnvilContainer extends Container { String correctRename; newName = rename; - String originalName = ItemUtils.getCustomName(getInput().getNbt()); + Component originalName = getInput().getComponent(DataComponentTypes.CUSTOM_NAME); - String plainOriginalName = MessageTranslator.convertToPlainTextLenient(originalName, session.locale()); + String plainOriginalName = MessageTranslator.convertToPlainText(originalName, session.locale()); String plainNewName = MessageTranslator.convertToPlainText(rename); if (!plainOriginalName.equals(plainNewName)) { // Strip out formatting since Java Edition does not allow it @@ -84,7 +85,7 @@ public class AnvilContainer extends Container { session.sendDownstreamGamePacket(renameItemPacket); } else { // Restore formatting for item since we're not renaming - correctRename = MessageTranslator.convertMessageLenient(originalName); + correctRename = originalName != null ? MessageTranslator.convertMessage(originalName, session.locale()) : ""; // Java Edition sends the original custom name when not renaming, // if there isn't a custom name an empty string is sent ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(plainOriginalName); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java index 7644ada73..09c31d6b6 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java @@ -25,9 +25,10 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import lombok.Getter; import lombok.Setter; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; @Getter @Setter @@ -35,7 +36,7 @@ public class BeaconContainer extends Container { private int primaryId; private int secondaryId; - public BeaconContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public BeaconContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java index 72f1088c3..f4a47daa0 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java @@ -25,10 +25,11 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; public class CartographyContainer extends Container { - public CartographyContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public CartographyContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Container.java index 79fa67da1..16c5d5344 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Container.java @@ -25,11 +25,12 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.InventoryTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.jetbrains.annotations.Range; /** @@ -37,7 +38,7 @@ import org.jetbrains.annotations.Range; */ @Getter public class Container extends Inventory { - private final PlayerInventory playerInventory; + protected final PlayerInventory playerInventory; private final int containerSize; /** @@ -45,9 +46,9 @@ public class Container extends Inventory { */ private boolean isUsingRealBlock = false; - public Container(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType); - this.playerInventory = playerInventory; + public Container(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); + this.playerInventory = session.getPlayerInventory(); this.containerSize = this.size + InventoryTranslator.PLAYER_INVENTORY_SIZE; } @@ -83,9 +84,9 @@ public class Container extends Inventory { * Will be overwritten for droppers. * * @param usingRealBlock whether this container is using a real container or not - * @param javaBlockId the Java block string of the block, if real + * @param block the Java block, if real */ - public void setUsingRealBlock(boolean usingRealBlock, String javaBlockId) { + public void setUsingRealBlock(boolean usingRealBlock, Block block) { isUsingRealBlock = usingRealBlock; } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java index bcacd3587..664788b11 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java @@ -25,13 +25,19 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.CrafterInventoryTranslator; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.GeyserImpl; +import org.jetbrains.annotations.Range; @Getter public class CrafterContainer extends Container { + private GeyserItemStack resultItem = GeyserItemStack.EMPTY; @Setter private boolean triggered = false; @@ -42,12 +48,40 @@ public class CrafterContainer extends Container { */ private short disabledSlotsMask = 0; - public CrafterContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public CrafterContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); + } + + @Override + public GeyserItemStack getItem(int slot) { + if (slot == CrafterInventoryTranslator.JAVA_RESULT_SLOT) { + return this.resultItem; + } else if (isCraftingGrid(slot)) { + return super.getItem(slot); + } else { + return playerInventory.getItem(slot - CrafterInventoryTranslator.GRID_SIZE + InventoryTranslator.PLAYER_INVENTORY_OFFSET); + } + } + + @Override + public int getOffsetForHotbar(@Range(from = 0, to = 8) int slot) { + return playerInventory.getOffsetForHotbar(slot) - InventoryTranslator.PLAYER_INVENTORY_OFFSET + CrafterInventoryTranslator.GRID_SIZE; + } + + @Override + public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { + if (slot == CrafterInventoryTranslator.JAVA_RESULT_SLOT) { + // Result item probably won't be an item that needs to worry about net ID or lodestone compasses + this.resultItem = newItem; + } else if (isCraftingGrid(slot)) { + super.setItem(slot, newItem, session); + } else { + playerInventory.setItem(slot - CrafterInventoryTranslator.GRID_SIZE + InventoryTranslator.PLAYER_INVENTORY_OFFSET, newItem, session); + } } public void setSlot(int slot, boolean enabled) { - if (slot < 0 || slot > 8) { + if (!isCraftingGrid(slot)) { GeyserImpl.getInstance().getLogger().warning("Crafter slot out of bounds: " + slot); return; } @@ -58,4 +92,8 @@ public class CrafterContainer extends Container { disabledSlotsMask = (short) (disabledSlotsMask | (1 << slot)); } } + + private static boolean isCraftingGrid(int slot) { + return slot >= 0 && slot <= 8; + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java index ac55aae60..9c88ab231 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java @@ -25,24 +25,24 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.data.inventory.EnchantOptionData; import lombok.Getter; +@Getter public class EnchantingContainer extends Container { /** * A cache of what Bedrock sees */ - @Getter private final EnchantOptionData[] enchantOptions; /** * A mutable cache of what the server sends us */ - @Getter private final GeyserEnchantOption[] geyserEnchantOptions; - public EnchantingContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public EnchantingContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); enchantOptions = new EnchantOptionData[3]; geyserEnchantOptions = new GeyserEnchantOption[3]; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java index 6518dce7c..f84c8bb91 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java @@ -25,29 +25,31 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import lombok.Getter; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.Generic3X3InventoryTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Getter public class Generic3X3Container extends Container { /** * Whether we need to set the container type as {@link org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType#DROPPER}. *

- * Used at {@link Generic3X3InventoryTranslator#openInventory(GeyserSession, Inventory)} + * Used at {@link Generic3X3InventoryTranslator#openInventory(GeyserSession, Generic3X3Container)} */ - @Getter private boolean isDropper = false; - public Generic3X3Container(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public Generic3X3Container(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } @Override - public void setUsingRealBlock(boolean usingRealBlock, String javaBlockId) { - super.setUsingRealBlock(usingRealBlock, javaBlockId); + public void setUsingRealBlock(boolean usingRealBlock, Block block) { + super.setUsingRealBlock(usingRealBlock, block); if (usingRealBlock) { - isDropper = javaBlockId.startsWith("minecraft:dropper"); + isDropper = block == Blocks.DROPPER; } } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Generic9X3Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Generic9X3Container.java new file mode 100644 index 000000000..aeea15c70 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/Generic9X3Container.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory; + +import lombok.Getter; +import lombok.Setter; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; + +/** + * A "hack" to be able to use existing barrels. + * The only difference to chests appears to be the different ContainerSlotType - this accounts for it. + */ +@Getter @Setter +public class Generic9X3Container extends Container { + + private boolean isBarrel; + + public Generic9X3Container(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); + } + + @Override + public void setUsingRealBlock(boolean usingRealBlock, Block block) { + super.setUsingRealBlock(usingRealBlock, block); + if (usingRealBlock) { + isBarrel = block == Blocks.BARREL; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserEnchantOption.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserEnchantOption.java index 23365e392..de0bd7300 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserEnchantOption.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserEnchantOption.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.inventory; +import lombok.Getter; import org.cloudburstmc.protocol.bedrock.data.inventory.EnchantData; import org.cloudburstmc.protocol.bedrock.data.inventory.EnchantOptionData; -import lombok.Getter; import org.geysermc.geyser.session.GeyserSession; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -44,13 +43,13 @@ public class GeyserEnchantOption { * is controlled by the server. * So, of course, we have to throw in some easter eggs. ;) */ - private static final List ENCHANT_NAMES = Arrays.asList("tougher armor", "lukeeey", "fall better", - "explode less", "camo toy", "breathe better", "rtm five one six", "armor stab", "water walk", "you are elsa", - "tim two zero three", "fast walk nether", "davchoo", "oof ouch owie", "enemy on fire", "spider sad", "aj ferguson", "redned", - "more items thx", "long sword reach", "fast tool", "give me block", "less breaky break", "cube craft", - "strong arrow", "fist arrow", "spicy arrow", "many many arrows", "geyser", "come here fish", "i like this", - "stabby stab", "supreme mortal", "avatar i guess", "more arrows", "fly finder seventeen", "in and out", - "xp heals tools", "dragon proxy waz here"); + private static final List ENCHANT_NAMES = List.of("tougher armor", "lukeeey", "fall better", + "explode less", "camo toy", "armor stab", "breathe better", "water walk", "rtm five one six", "oof ouch owie", + "enemy on fire", "spider sad", "aj ferguson", "redned", "more items thx", "fast tool", "give me block", + "less breaky break", "cube craft", "strong arrow", "fist arrow", "spicy arrow", "many many arrows", "geyser", + "come here fish", "you are elsa", "xp heals tools", "tim two zero three", "dragon proxy waz here", + "stabby stab", "supreme mortal", "i like this", "avatar i guess", "more arrows", "in and out", + "fly finder seventeen", "fast walk nether", "davchoo", "onechris", "death bringer thirteen", "kastle"); @Getter private final int javaIndex; @@ -62,7 +61,6 @@ public class GeyserEnchantOption { private boolean hasChanged; private int xpCost = 0; - private int javaEnchantIndex = -1; private int bedrockEnchantIndex = -1; private int enchantLevel = -1; @@ -74,7 +72,7 @@ public class GeyserEnchantOption { this.hasChanged = false; return new EnchantOptionData(xpCost, javaIndex + 16, EMPTY, enchantLevel == -1 ? EMPTY : Collections.singletonList(new EnchantData(bedrockEnchantIndex, enchantLevel)), EMPTY, - javaEnchantIndex == -1 ? "unknown" : ENCHANT_NAMES.get(javaEnchantIndex), enchantLevel == -1 ? 0 : session.getNextItemNetId()); + bedrockEnchantIndex == -1 ? "unknown" : ENCHANT_NAMES.get(bedrockEnchantIndex), enchantLevel == -1 ? 0 : session.getNextItemNetId()); } public boolean hasChanged() { @@ -88,10 +86,9 @@ public class GeyserEnchantOption { } } - public void setEnchantIndex(int javaEnchantIndex, int bedrockEnchantIndex) { - if (this.javaEnchantIndex != javaEnchantIndex) { + public void setEnchantIndex(int bedrockEnchantIndex) { + if (this.bedrockEnchantIndex != bedrockEnchantIndex) { hasChanged = true; - this.javaEnchantIndex = javaEnchantIndex; this.bedrockEnchantIndex = bedrockEnchantIndex; } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java index 4ff8db9f0..0de4580aa 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java @@ -25,21 +25,36 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.item.ItemTranslator; +import org.geysermc.geyser.session.cache.BundleCache; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.translator.item.ItemTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; + +import java.util.HashMap; +import java.util.function.Supplier; @Data public class GeyserItemStack { @@ -47,26 +62,52 @@ public class GeyserItemStack { private final int javaId; private int amount; - private CompoundTag nbt; + private DataComponents components; private int netId; - @Getter(AccessLevel.NONE) + @EqualsAndHashCode.Exclude + private BundleCache.BundleData bundleData; + + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) @EqualsAndHashCode.Exclude private Item item; - private GeyserItemStack(int javaId, int amount, CompoundTag nbt) { - this(javaId, amount, nbt, 1); + private GeyserItemStack(int javaId, int amount, DataComponents components) { + this(javaId, amount, components, 1, null); } - private GeyserItemStack(int javaId, int amount, CompoundTag nbt, int netId) { + private GeyserItemStack(int javaId, int amount, DataComponents components, int netId, BundleCache.BundleData bundleData) { this.javaId = javaId; this.amount = amount; - this.nbt = nbt; + this.components = components; this.netId = netId; + this.bundleData = bundleData; + } + + public static @NonNull GeyserItemStack of(int javaId, int amount) { + return of(javaId, amount, null); + } + + public static @NonNull GeyserItemStack of(int javaId, int amount, @Nullable DataComponents components) { + return new GeyserItemStack(javaId, amount, components); } public static @NonNull GeyserItemStack from(@Nullable ItemStack itemStack) { - return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getNbt()); + return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getDataComponentsPatch()); + } + + public static @NonNull GeyserItemStack from(@NonNull SlotDisplay slotDisplay) { + if (slotDisplay instanceof EmptySlotDisplay) { + return GeyserItemStack.EMPTY; + } + if (slotDisplay instanceof ItemSlotDisplay itemSlotDisplay) { + return GeyserItemStack.of(itemSlotDisplay.item(), 1); + } + if (slotDisplay instanceof ItemStackSlotDisplay itemStackSlotDisplay) { + return GeyserItemStack.from(itemStackSlotDisplay.itemStack()); + } + GeyserImpl.getInstance().getLogger().warning("Unsure how to convert to ItemStack: " + slotDisplay); + return GeyserItemStack.EMPTY; } public int getJavaId() { @@ -77,14 +118,102 @@ public class GeyserItemStack { return isEmpty() ? 0 : amount; } - public @Nullable CompoundTag getNbt() { - return isEmpty() ? null : nbt; + public boolean is(Item item) { + return javaId == item.javaId(); + } + + public boolean is(GeyserSession session, Tag tag) { + return session.getTagCache().is(tag, javaId); + } + + public boolean is(GeyserSession session, HolderSet set) { + return session.getTagCache().is(set, JavaRegistries.ITEM, javaId); + } + + public boolean isSameItem(GeyserItemStack other) { + return javaId == other.javaId; + } + + /** + * Returns all components of this item - base and additional components sent over the network. + * These are NOT modifiable! To add components, use {@link #getOrCreateComponents()}. + * + * @return the item's base data components and the "additional" ones that may exist. + */ + public @Nullable DataComponents getAllComponents() { + return isEmpty() ? null : asItem().gatherComponents(components); + } + + /** + * @return the {@link DataComponents} patch that's sent over the network. + */ + public @Nullable DataComponents getComponents() { + return isEmpty() ? null : components; + } + + /** + * @return whether this GeyserItemStack has any component modifications additional to + * the base item components. + */ + public boolean hasNonBaseComponents() { + return components != null; + } + + @NonNull + public DataComponents getOrCreateComponents() { + if (components == null) { + return components = new DataComponents(new HashMap<>()); + } + return components; + } + + /** + * Returns the stored data component for a given {@link DataComponentType}, or null. + *

+ * This method will first check the additional components that may exist, + * and fallback to the item's default (or, "base") components if need be. + * @param type the {@link DataComponentType} to query + * @return the value for said type, or null. + * @param the value's type + */ + @Nullable + public T getComponent(@NonNull DataComponentType type) { + // A data component patch may contain null values to remove base components + // e.g. an elytra without the glider component + if (components != null && components.contains(type)) { + return components.get(type); + } + + return asItem().getComponent(type); + } + + public T getComponentElseGet(@NonNull DataComponentType type, Supplier supplier) { + T value = getComponent(type); + return value == null ? supplier.get() : value; } public int getNetId() { return isEmpty() ? 0 : netId; } + public int getBundleId() { + if (isEmpty()) { + return -1; + } + + return bundleData == null ? -1 : bundleData.bundleId(); + } + + public void mergeBundleData(GeyserSession session, BundleCache.BundleData oldBundleData) { + if (oldBundleData != null && this.bundleData != null) { + // Old bundle; re-use old IDs + this.bundleData.updateNetIds(session, oldBundleData); + } else if (this.bundleData != null) { + // New bundle; allocate new ID + session.getBundleCache().markNewBundle(this.bundleData); + } + } + public void add(int add) { amount += add; } @@ -98,24 +227,76 @@ public class GeyserItemStack { } public @Nullable ItemStack getItemStack(int newAmount) { - return isEmpty() ? null : new ItemStack(javaId, newAmount, nbt); + if (isEmpty()) { + return null; + } + // Sync our updated bundle data to server, if applicable + // Not fresh from server? Then we have changes to apply!~ + if (bundleData != null && !bundleData.freshFromServer()) { + if (!bundleData.contents().isEmpty()) { + getOrCreateComponents().put(DataComponentTypes.BUNDLE_CONTENTS, bundleData.toComponent()); + } else { + if (components != null) { + // Empty list = no component = should delete + components.getDataComponents().remove(DataComponentTypes.BUNDLE_CONTENTS); + } + } + } + return isEmpty() ? null : new ItemStack(javaId, newAmount, components); } public ItemData getItemData(GeyserSession session) { if (isEmpty()) { return ItemData.AIR; } - ItemData.Builder itemData = ItemTranslator.translateToBedrock(session, javaId, amount, nbt); + ItemData.Builder itemData = ItemTranslator.translateToBedrock(session, javaId, amount, components); itemData.netId(getNetId()); itemData.usingNetId(true); - return itemData.build(); + + return session.getBundleCache().checkForBundle(this, itemData); } public ItemMapping getMapping(GeyserSession session) { return session.getItemMappings().getMapping(this.javaId); } + public SlotDisplay asSlotDisplay() { + if (isEmpty()) { + return EmptySlotDisplay.INSTANCE; + } + return new ItemStackSlotDisplay(this.getItemStack()); + } + + public int getMaxStackSize() { + return getComponentElseGet(DataComponentTypes.MAX_STACK_SIZE, () -> 1); + } + + public int getMaxDamage() { + return getComponentElseGet(DataComponentTypes.MAX_DAMAGE, () -> 0); + } + + public int getDamage() { + // Damage can't be negative + int damage = Math.max(this.getComponentElseGet(DataComponentTypes.DAMAGE, () -> 0), 0); + return Math.min(damage, this.getMaxDamage()); + } + + public boolean nextDamageWillBreak() { + return this.isDamageable() && this.getDamage() >= this.getMaxDamage() - 1; + } + + public boolean isDamageable() { + return getComponent(DataComponentTypes.MAX_DAMAGE) != null && getComponent(DataComponentTypes.UNBREAKABLE) == null && getComponent(DataComponentTypes.DAMAGE) != null; + } + + public boolean isDamaged() { + return isDamageable() && getDamage() > 0; + } + public Item asItem() { + if (isEmpty()) { + return Items.AIR; + } if (item == null) { return (item = Registries.JAVA_ITEMS.get().get(javaId)); } @@ -131,6 +312,6 @@ public class GeyserItemStack { } public GeyserItemStack copy(int newAmount) { - return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, nbt == null ? null : nbt.clone(), netId); + return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, components == null ? null : components.clone(), netId, bundleData == null ? null : bundleData.copy()); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java index 3376d6c26..414ad2951 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java @@ -25,20 +25,19 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; -import com.github.steveice10.opennbt.tag.builtin.ByteTag; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.cloudburstmc.math.vector.Vector3i; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.inventory.click.ClickPlan; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.item.ItemTranslator; +import org.geysermc.geyser.translator.item.ItemTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.jetbrains.annotations.Range; import java.util.Arrays; @@ -48,6 +47,10 @@ public abstract class Inventory { @Getter protected final int javaId; + @Setter + @Getter + private int bedrockId; + /** * The Java inventory state ID from the server. As of Java Edition 1.18.1 this value has one instance per player. * If this is out of sync with the server when a packet containing it is handled, the server will resync items. @@ -57,7 +60,7 @@ public abstract class Inventory { @Setter private int stateId; /** - * See {@link org.geysermc.geyser.inventory.click.ClickPlan#execute(boolean)}; used as a hack + * See {@link ClickPlan#execute(boolean)}; used as a hack */ @Getter private int nextStateId = -1; @@ -77,43 +80,52 @@ public abstract class Inventory { protected final GeyserItemStack[] items; /** - * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location + * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location. */ @Getter @Setter protected Vector3i holderPosition = Vector3i.ZERO; + /** + * The entity id of the entity holding the inventory. + * Either this, or the holder position must be set in order for Bedrock to open inventories. + */ @Getter @Setter protected long holderId = -1; + /** + * Whether this inventory is currently shown to the Bedrock player. + */ @Getter @Setter - private boolean pending = false; + private boolean displayed; - @Getter - @Setter - private boolean displayed = false; - - protected Inventory(int id, int size, ContainerType containerType) { - this("Inventory", id, size, containerType); + protected Inventory(GeyserSession session, int id, int size, ContainerType containerType) { + this(session, "Inventory", id, size, containerType); } - protected Inventory(String title, int javaId, int size, ContainerType containerType) { + protected Inventory(GeyserSession session, String title, int javaId, int size, ContainerType containerType) { this.title = title; this.javaId = javaId; this.size = size; this.containerType = containerType; this.items = new GeyserItemStack[size]; Arrays.fill(items, GeyserItemStack.EMPTY); - } - // This is to prevent conflicts with special bedrock inventory IDs. - // The vanilla java server only sends an ID between 1 and 100 when opening an inventory, - // so this is rarely needed. (certain plugins) - // Example: https://github.com/GeyserMC/Geyser/issues/3254 - public int getBedrockId() { - return javaId <= 100 ? javaId : (javaId % 100) + 1; + // This is to prevent conflicts with special bedrock inventory IDs. + // The vanilla java server only sends an ID between 1 and 100 when opening an inventory, + // so this is rarely needed. (certain plugins) + // Example: https://github.com/GeyserMC/Geyser/issues/3254 + this.bedrockId = javaId <= 100 ? javaId : (javaId % 100) + 1; + + // We occasionally need to re-open inventories with a delay in cases where + // Java wouldn't - e.g. for virtual chest menus that switch pages. + // And, well, we want to avoid reusing Bedrock inventory id's that are currently being used in a closing inventory; + // so to be safe we just deviate in that case as well. + if ((session.getInventoryHolder() != null && session.getInventoryHolder().bedrockId() == bedrockId) || session.isClosingInventory()) { + this.bedrockId += 1; + } } public GeyserItemStack getItem(int slot) { @@ -127,8 +139,8 @@ public abstract class Inventory { public abstract int getOffsetForHotbar(@Range(from = 0, to = 8) int slot); public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { - if (slot > this.size) { - session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this); + if (slot < 0 || slot >= this.size) { + session.getGeyser().getLogger().debug("Tried to set an item out of bounds (slot was " + slot + ")! " + this); return; } GeyserItemStack oldItem = items[slot]; @@ -136,26 +148,29 @@ public abstract class Inventory { items[slot] = newItem; // Lodestone caching - if (newItem.asItem() == Items.COMPASS) { - CompoundTag nbt = newItem.getNbt(); - if (nbt != null) { - Tag lodestoneTag = nbt.get("LodestoneTracked"); - if (lodestoneTag instanceof ByteTag) { - session.getLodestoneCache().cacheInventoryItem(newItem); - } + if (newItem.is(Items.COMPASS)) { + var tracker = newItem.getComponent(DataComponentTypes.LODESTONE_TRACKER); + if (tracker != null) { + session.getLodestoneCache().cacheInventoryItem(newItem, tracker); } } } - protected void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) { + public static void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) { if (!newItem.isEmpty()) { ItemDefinition oldMapping = ItemTranslator.getBedrockItemDefinition(session, oldItem); ItemDefinition newMapping = ItemTranslator.getBedrockItemDefinition(session, newItem); if (oldMapping.equals(newMapping)) { newItem.setNetId(oldItem.getNetId()); + newItem.mergeBundleData(session, oldItem.getBundleData()); } else { newItem.setNetId(session.getNextItemNetId()); + session.getBundleCache().markNewBundle(newItem.getBundleData()); + session.getBundleCache().onOldItemDelete(oldItem); } + } else { + // Empty item means no more bundle if one existed. + session.getBundleCache().onOldItemDelete(oldItem); } } @@ -170,4 +185,12 @@ public abstract class Inventory { public void resetNextStateId() { nextStateId = -1; } + + /** + * Whether we should be sending a {@link org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClosePacket} + * when closing the inventory. + */ + public boolean shouldConfirmContainerClose() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java new file mode 100644 index 000000000..6c811676e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; + +import java.util.List; + +/** + * A helper class storing the current inventory, translator, and session. + */ +@Accessors(fluent = true) +@Getter +public final class InventoryHolder { + private final GeyserSession session; + private final T inventory; + private final InventoryTranslator translator; + + /** + * Whether this inventory is currently pending. + * It can be pending if this inventory was opened while another inventory was still open, + * or because opening this inventory takes more time (e.g. virtual inventories). + */ + @Setter + private boolean pending; + + /** + * Stores the number of attempts to open virtual inventories. + * Capped at 3, and isn't used in ideal circumstances. + * Used to resolve container closing issues. + */ + @Setter + private int containerOpenAttempts; + + @SuppressWarnings("unchecked") + public InventoryHolder(GeyserSession session, Inventory newInventory, InventoryTranslator newTranslator) { + this.session = session; + this.inventory = (T) newInventory; + this.translator = (InventoryTranslator) newTranslator; + } + + public void markCurrent() { + this.session.setInventoryHolder(this); + } + + public boolean shouldSetPending() { + return session.isClosingInventory() || !session.getUpstream().isInitialized() || session.getPendingOrCurrentBedrockInventoryId() != -1; + } + + public boolean shouldConfirmClose(boolean confirm) { + return confirm && inventory.isDisplayed() && !pending; + } + + public void inheritFromExisting(InventoryHolder existing) { + // Mirror Bedrock id + inventory.setBedrockId(existing.bedrockId()); + + // Also mirror other properties - in case we're e.g. dealing with a pending virtual inventory + Inventory existingInventory = existing.inventory; + this.pending = existing.pending(); + inventory.setDisplayed(existingInventory.isDisplayed()); + inventory.setHolderPosition(existingInventory.getHolderPosition()); + inventory.setHolderId(existingInventory.getHolderId()); + this.markCurrent(); + } + + /* + * Helper methods to avoid using the wrong translator to update specific inventories. + */ + + public void updateInventory() { + this.translator.updateInventory(session, inventory); + } + + public void updateProperty(int rawProperty, int value) { + this.translator.updateProperty(session, inventory, rawProperty, value); + } + + public void updateSlot(int slot) { + this.translator.updateSlot(session, inventory, slot); + } + + public void openInventory() { + this.translator.openInventory(session, inventory); + this.pending = false; + this.inventory.setDisplayed(true); + } + + public void closeInventory(boolean force) { + this.translator.closeInventory(session, inventory, force); + if (session.getContainerOutputFuture() != null) { + session.getContainerOutputFuture().cancel(true); + } + } + + public boolean requiresOpeningDelay() { + return this.translator.requiresOpeningDelay(session, inventory); + } + + public boolean prepareInventory() { + return this.translator.prepareInventory(session, inventory); + } + + public void translateRequests(List requests) { + this.translator.translateRequests(session, inventory, requests); + } + + public GeyserSession session() { + return session; + } + + public T inventory() { + return inventory; + } + + public InventoryTranslator translator() { + return translator; + } + + public void incrementContainerOpenAttempts() { + this.containerOpenAttempts++; + } + + public int javaId() { + return inventory.getJavaId(); + } + + public int bedrockId() { + return inventory.getBedrockId(); + } + + @Override + public String toString() { + return "InventoryHolder[" + + "session=" + session.bedrockUsername() + ", " + + "inventory=" + inventory + ", " + + "pending= " + pending + ", " + + "containerOpenAttempts=" + containerOpenAttempts + ", " + + "translator=" + translator.getClass().getSimpleName() + ']'; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java index 7ac2fed99..e828698f9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java @@ -25,51 +25,37 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import lombok.Getter; import lombok.Setter; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.java.inventory.JavaOpenBookTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Getter public class LecternContainer extends Container { - @Getter @Setter + @Setter private int currentBedrockPage = 0; - @Getter @Setter + @Setter private NbtMap blockEntityTag; - @Getter @Setter - private Vector3i position; - // Sigh. When the lectern container is created, we don't know (yet) if it's fake or not. - // So... time for a manual check :/ - @Getter - private boolean isFakeLectern = false; + private boolean isBookInPlayerInventory = false; - public LecternContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); - } - - /** - * When we are using a fake lectern, the Java server expects us to still be in a player inventory. - * We can't use {@link #isUsingRealBlock()} as that may not be determined yet. - */ - @Override - public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { - if (isFakeLectern) { - session.getPlayerInventory().setItem(slot, newItem, session); - } else { - super.setItem(slot, newItem, session); - } + public LecternContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } /** * This is used ONLY once to set the book of a fake lectern in {@link JavaOpenBookTranslator}. * See {@link LecternContainer#setItem(int, GeyserItemStack, GeyserSession)} as for why this is separate. */ - public void setFakeLecternBook(GeyserItemStack book, GeyserSession session) { - this.isFakeLectern = true; + public void setVirtualLecternBook(GeyserItemStack book, GeyserSession session) { + this.isBookInPlayerInventory = true; super.setItem(0, book, session); } + + @Override + public boolean shouldConfirmContainerClose() { + return !isBookInPlayerInventory; + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java index 105b5ca5b..be0e1cd60 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java @@ -25,33 +25,35 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; -import com.github.steveice10.mc.protocol.data.game.inventory.VillagerTrade; -import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundMerchantOffersPacket; import lombok.Getter; import lombok.Setter; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.VillagerTrade; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundMerchantOffersPacket; +import java.util.List; + +@Setter public class MerchantContainer extends Container { - @Getter @Setter + @Getter private Entity villager; - @Setter - private VillagerTrade[] villagerTrades; - @Getter @Setter + private List villagerTrades; + @Getter private ClientboundMerchantOffersPacket pendingOffersPacket; - @Getter @Setter + @Getter private int tradeExperience; - public MerchantContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public MerchantContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } public void onTradeSelected(GeyserSession session, int slot) { - if (villagerTrades != null && slot >= 0 && slot < villagerTrades.length) { - VillagerTrade trade = villagerTrades[slot]; - setItem(2, GeyserItemStack.from(trade.getOutput()), session); + if (villagerTrades != null && slot >= 0 && slot < villagerTrades.size()) { + VillagerTrade trade = villagerTrades.get(slot); + setItem(2, GeyserItemStack.from(trade.getResult()), session); tradeExperience += trade.getXp(); villager.getDirtyMetadata().put(EntityDataTypes.TRADE_EXPERIENCE, tradeExperience); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java index bda09a4ed..6c34aa7d5 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java @@ -25,29 +25,32 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.jetbrains.annotations.Range; +import java.util.Map; + +@Getter public class PlayerInventory extends Inventory { /** * Stores the held item slot, starting at index 0. * Add 36 in order to get the network item slot. */ - @Getter @Setter private int heldItemSlot; - @Getter @NonNull private GeyserItemStack cursor = GeyserItemStack.EMPTY; - public PlayerInventory() { - super(0, 46, null); + public PlayerInventory(GeyserSession session) { + super(session, 0, 46, null); heldItemSlot = 0; } @@ -61,6 +64,16 @@ public class PlayerInventory extends Inventory { cursor = newCursor; } + /** + * Checks if the player is holding the specified item in either hand + * + * @param item The item to look for + * @return If the player is holding the item in either hand + */ + public boolean isHolding(@NonNull Item item) { + return getItemInHand().is(item) || getOffhand().is(item); + } + public GeyserItemStack getItemInHand(@NonNull Hand hand) { return hand == Hand.OFF_HAND ? getOffhand() : getItemInHand(); } @@ -73,6 +86,18 @@ public class PlayerInventory extends Inventory { return items[36 + heldItemSlot]; } + // TODO other equipment slots + public Map getEquipment() { + return Map.of( + EquipmentSlot.MAIN_HAND, getItemInHand(), + EquipmentSlot.OFF_HAND, items[45], + EquipmentSlot.BOOTS, items[8], + EquipmentSlot.LEGGINGS, items[7], + EquipmentSlot.CHESTPLATE, items[6], + EquipmentSlot.HELMET, items[5] + ); + } + public void setItemInHand(@NonNull GeyserItemStack item) { if (36 + heldItemSlot > this.size) { GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!"); @@ -81,6 +106,11 @@ public class PlayerInventory extends Inventory { items[36 + heldItemSlot] = item; } + @Override + public boolean shouldConfirmContainerClose() { + return false; + } + public GeyserItemStack getOffhand() { return items[45]; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java index f99a0c71e..8a33db7d2 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java @@ -25,27 +25,27 @@ package org.geysermc.geyser.inventory; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Setter +@Getter public class StonecutterContainer extends Container { /** * The button that has currently been pressed Java-side */ - @Getter - @Setter private int stonecutterButton = -1; - public StonecutterContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public StonecutterContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } @Override public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { - if (slot == 0 && newItem.getJavaId() != items[slot].getJavaId()) { + if (slot == 0 && !newItem.isSameItem(items[slot])) { // The pressed stonecutter button output resets whenever the input item changes this.stonecutterButton = -1; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java b/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java index d7068920e..cf16d0b6f 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java @@ -25,13 +25,16 @@ package org.geysermc.geyser.inventory.click; -import com.github.steveice10.mc.protocol.data.game.inventory.*; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.*; import lombok.AllArgsConstructor; @AllArgsConstructor public enum Click { LEFT(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK), + LEFT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK), + LEFT_BUNDLE_FROM_CURSOR(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK), RIGHT(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK), + RIGHT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK), LEFT_SHIFT(ContainerActionType.SHIFT_CLICK_ITEM, ShiftClickItemAction.LEFT_CLICK), DROP_ONE(ContainerActionType.DROP_ITEM, DropItemAction.DROP_FROM_SELECTED), DROP_ALL(ContainerActionType.DROP_ITEM, DropItemAction.DROP_SELECTED_STACK), diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java index f31f6d82f..68d1445d8 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java @@ -25,19 +25,28 @@ package org.geysermc.geyser.inventory.click; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerActionType; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; -import com.github.steveice10.mc.protocol.data.game.inventory.MoveToHotbarAction; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; -import it.unimi.dsi.fastutil.ints.*; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.SlotType; +import org.geysermc.geyser.item.hashing.DataComponentHashers; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.BundleInventoryTranslator; import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.geyser.util.thirdparty.Fraction; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction; +import org.geysermc.mcprotocollib.protocol.data.game.item.HashedStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSelectBundleItemPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket; import org.jetbrains.annotations.Contract; import java.util.ArrayList; @@ -51,23 +60,26 @@ public final class ClickPlan { * Used for 1.17.1+ proper packet translation - any non-cursor item that is changed in a single transaction gets sent here. */ private Int2ObjectMap changedItems; + private Int2ObjectMap changedHashedItems; private GeyserItemStack simulatedCursor; - private boolean finished; + private int desiredBundleSlot; + private boolean executionBegan; private final GeyserSession session; - private final InventoryTranslator translator; + private final InventoryTranslator translator; private final Inventory inventory; private final int gridSize; - public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory) { + public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory) { this.session = session; this.translator = translator; this.inventory = inventory; this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize()); this.changedItems = null; + this.changedHashedItems = null; this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); - this.finished = false; + this.executionBegan = false; gridSize = translator.getGridSize(); } @@ -82,7 +94,7 @@ public final class ClickPlan { } public void add(Click click, int slot, boolean force) { - if (finished) + if (executionBegan) throw new UnsupportedOperationException("ClickPlan already executed"); if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) { @@ -97,6 +109,7 @@ public final class ClickPlan { } public void execute(boolean refresh) { + executionBegan = true; //update geyser inventory after simulation to avoid net id desync resetSimulation(); ListIterator planIter = plan.listIterator(); @@ -109,6 +122,7 @@ public final class ClickPlan { } changedItems = new Int2ObjectOpenHashMap<>(); + changedHashedItems = new Int2ObjectOpenHashMap<>(); boolean emulatePost1_16Logic = session.isEmulatePost1_16Logic(); @@ -148,8 +162,8 @@ public final class ClickPlan { action.slot, action.click.actionType, action.click.action, - clickedItemStack, - changedItems + DataComponentHashers.hashStack(session, clickedItemStack), + changedHashedItems ); session.sendDownstreamGamePacket(clickPacket); @@ -159,13 +173,59 @@ public final class ClickPlan { for (Int2ObjectMap.Entry simulatedSlot : simulatedItems.int2ObjectEntrySet()) { inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session); } - finished = true; + } + + public void executeForCreativeMode() { + executionBegan = true; + //update geyser inventory after simulation to avoid net id desync + resetSimulation(); + changedItems = new Int2ObjectOpenHashMap<>(); + changedHashedItems = new Int2ObjectOpenHashMap<>(); + for (ClickAction action : plan) { + simulateAction(action); + } + session.getPlayerInventory().setCursor(simulatedCursor, session); + for (Int2ObjectMap.Entry simulatedSlot : simulatedItems.int2ObjectEntrySet()) { + inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session); + } + for (Int2ObjectMap.Entry changedSlot : changedItems.int2ObjectEntrySet()) { + ItemStack value = changedSlot.getValue(); + ItemStack toSend = InventoryUtils.isEmpty(value) ? new ItemStack(-1, 0, null) : value; + session.sendDownstreamGamePacket( + new ServerboundSetCreativeModeSlotPacket((short) changedSlot.getIntKey(), toSend) + ); + } + } + + public Inventory getInventory() { + return inventory; + } + + /** + * Test if the item stacks with another item in the specified slot. + * This will check the simulated inventory without copying. + */ + public boolean canStack(int slot, GeyserItemStack item) { + GeyserItemStack slotItem = simulatedItems.getOrDefault(slot, inventory.getItem(slot)); + return InventoryUtils.canStack(slotItem, item); + } + + /** + * Test if the specified slot is empty. + * This will check the simulated inventory without copying. + */ + public boolean isEmpty(int slot) { + return simulatedItems.getOrDefault(slot, inventory.getItem(slot)).isEmpty(); } public GeyserItemStack getItem(int slot) { return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy()); } + public void setDesiredBundleSlot(int desiredBundleSlot) { + this.desiredBundleSlot = desiredBundleSlot; + } + public GeyserItemStack getCursor() { return simulatedCursor; } @@ -200,6 +260,7 @@ public final class ClickPlan { private void onSlotItemChange(int slot, GeyserItemStack itemStack) { if (changedItems != null) { changedItems.put(slot, itemStack.getItemStack()); + changedHashedItems.put(slot, DataComponentHashers.hashStack(session, itemStack.getItemStack())); } } @@ -254,8 +315,60 @@ public final class ClickPlan { } else if (InventoryUtils.canStack(cursor, clicked)) { cursor.sub(1); add(action.slot, clicked, 1); + } else { + // Can't stack, but both the cursor and the slot have an item + // (Called for bundles) + setCursor(clicked); + setItem(action.slot, cursor); } break; + case LEFT_BUNDLE: + Fraction bundleWeight = BundleInventoryTranslator.calculateBundleWeight(clicked.getBundleData().contents()); + int amountToAddInBundle = Math.min(BundleInventoryTranslator.capacityForItemStack(bundleWeight, cursor), cursor.getAmount()); + GeyserItemStack toInsertInBundle = cursor.copy(amountToAddInBundle); + if (executionBegan) { + clicked.getBundleData().contents().add(0, toInsertInBundle); + session.getBundleCache().onItemAdded(clicked); // Must be run before onSlotItemChange as the latter exports an ItemStack from the bundle + } + onSlotItemChange(action.slot, clicked); + cursor.sub(amountToAddInBundle); + break; + case LEFT_BUNDLE_FROM_CURSOR: + List contents = cursor.getBundleData().contents(); + bundleWeight = BundleInventoryTranslator.calculateBundleWeight(contents); + amountToAddInBundle = Math.min(BundleInventoryTranslator.capacityForItemStack(bundleWeight, clicked), clicked.getAmount()); + toInsertInBundle = clicked.copy(amountToAddInBundle); + if (executionBegan) { + cursor.getBundleData().contents().add(0, toInsertInBundle); + session.getBundleCache().onItemAdded(cursor); + } + sub(action.slot, clicked, amountToAddInBundle); + break; + case RIGHT_BUNDLE: + if (!cursor.isEmpty()) { + // Bundle should be in player's hand. + GeyserItemStack itemStack = cursor.getBundleData() + .contents() + .remove(0); + if (executionBegan) { + session.getBundleCache().onItemRemoved(cursor, 0); + } + setItem(action.slot, itemStack); + break; + } + + if (executionBegan) { + sendSelectedBundleSlot(action.slot); + } + GeyserItemStack itemStack = clicked.getBundleData() + .contents() + .remove(desiredBundleSlot); + if (executionBegan) { + session.getBundleCache().onItemRemoved(clicked, desiredBundleSlot); + } + onSlotItemChange(action.slot, clicked); + setCursor(itemStack); + break; case SWAP_TO_HOTBAR_1: swap(action.slot, inventory.getOffsetForHotbar(0), clicked); break; @@ -298,6 +411,11 @@ public final class ClickPlan { } } + private void sendSelectedBundleSlot(int slot) { + // Looks like this is also technically sent in creative mode. + session.sendDownstreamGamePacket(new ServerboundSelectBundleItemPacket(slot, desiredBundleSlot)); + } + /** * Swap between two inventory slots without a cursor. This should only be used with {@link ContainerActionType#MOVE_TO_HOTBAR_SLOT} */ diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java index c11505ffb..7b57540f9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.inventory.holder; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; @@ -36,15 +37,15 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.LecternContainer; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; -import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; -import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.InventoryUtils; import java.util.Collections; import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -55,43 +56,62 @@ public class BlockInventoryHolder extends InventoryHolder { /** * The default Java block ID to translate as a fake block */ - private final int defaultJavaBlockState; + private final BlockState defaultJavaBlockState; private final ContainerType containerType; - private final Set validBlocks; + private final Set validBlocks; + private final @Nullable Class validBlockClass; - public BlockInventoryHolder(String javaBlockIdentifier, ContainerType containerType, String... validBlocks) { - this.defaultJavaBlockState = BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getInt(javaBlockIdentifier); + public BlockInventoryHolder(Block defaultJavaBlock, ContainerType containerType, Block... validBlocks) { + this(defaultJavaBlock.defaultBlockState(), null, containerType, validBlocks); + } + + public BlockInventoryHolder(BlockState defaultJavaBlockState, ContainerType containerType, Block... validBlocks) { + this(defaultJavaBlockState, null, containerType, validBlocks); + } + + public BlockInventoryHolder(BlockState defaultJavaBlockState, @Nullable Class validBlockClass, ContainerType containerType, Block... validBlocks) { + this.defaultJavaBlockState = defaultJavaBlockState; + this.validBlockClass = validBlockClass; this.containerType = containerType; if (validBlocks != null) { - Set validBlocksTemp = new HashSet<>(validBlocks.length + 1); + Set validBlocksTemp = new HashSet<>(validBlocks.length + 1); Collections.addAll(validBlocksTemp, validBlocks); - validBlocksTemp.add(BlockUtils.getCleanIdentifier(javaBlockIdentifier)); + validBlocksTemp.add(defaultJavaBlockState.block()); this.validBlocks = Set.copyOf(validBlocksTemp); } else { - this.validBlocks = Collections.singleton(BlockUtils.getCleanIdentifier(javaBlockIdentifier)); + this.validBlocks = Collections.singleton(defaultJavaBlockState.block()); } } @Override - public boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { - // Check to see if there is an existing block we can use that the player just selected. - // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. - // (This could be a virtual inventory that the player is opening) - if (checkInteractionPosition(session)) { - // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid - // and the bedrock block is vanilla - int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); - if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) { - String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.DEFAULT).getJavaIdentifier().split("\\["); - if (isValidBlock(javaBlockString)) { - // We can safely use this block - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); - setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId); + public boolean canReuseContainer(GeyserSession session, Container container, Container previous) { + // We already ensured that the inventories are using the same type, size, and title - return true; - } - } + // While we could reuse real blocks for virtual inventories, + // it can result in unpleasant visual artifacts with specific plugins. + // Specifically - a few plugins send multiple ClientboundOpenScreen packets + // with different titles; where Geyser needs to re-open the menu fully in order to get + // the correct title to appear. The additional delay added by using virtual blocks masks + // the quick closing of the first packet. + if (previous.isUsingRealBlock()) { + return false; + } + + // Check if we'd be using the same virtual inventory position. + Vector3i position = InventoryUtils.findAvailableWorldSpace(session); + if (Objects.equals(position, previous.getHolderPosition())) { + return true; + } else { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory due to virtual block holder changing (%s -> %s)!", + previous.getHolderPosition(), position); + return false; + } + } + + @Override + public boolean prepareInventory(GeyserSession session, Container container) { + if (canUseRealBlock(session, container)) { + return true; } Vector3i position = InventoryUtils.findAvailableWorldSpace(session); @@ -105,13 +125,36 @@ public class BlockInventoryHolder extends InventoryHolder { blockPacket.setDefinition(session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState)); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); - inventory.setHolderPosition(position); + container.setHolderPosition(position); - setCustomName(session, position, inventory, defaultJavaBlockState); + setCustomName(session, position, container, defaultJavaBlockState); return true; } + protected boolean canUseRealBlock(GeyserSession session, Container container) { + // Check to see if there is an existing block we can use that the player just selected. + // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. + // (This could be a virtual inventory that the player is opening) + if (checkInteractionPosition(session)) { + // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid + // and the bedrock block is vanilla + BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { + if (isValidBlock(session, session.getLastInteractionBlockPosition(), state)) { + // We can safely use this block + container.setHolderPosition(session.getLastInteractionBlockPosition()); + container.setUsingRealBlock(true, state.block()); + setCustomName(session, session.getLastInteractionBlockPosition(), container, state); + + return true; + } + } + } + + return false; + } + /** * Will be overwritten in the beacon inventory translator to remove the check, since virtual inventories can't exist. * @@ -125,11 +168,14 @@ public class BlockInventoryHolder extends InventoryHolder { /** * @return true if this Java block ID can be used for player inventory. */ - protected boolean isValidBlock(String[] javaBlockString) { - return this.validBlocks.contains(javaBlockString[0]); + protected boolean isValidBlock(GeyserSession session, Vector3i position, BlockState blockState) { + if (this.validBlockClass != null && this.validBlockClass.isInstance(blockState.block())) { + return true; + } + return this.validBlocks.contains(blockState.block()); } - protected void setCustomName(GeyserSession session, Vector3i position, Inventory inventory, int javaBlockState) { + protected void setCustomName(GeyserSession session, Vector3i position, Inventory inventory, BlockState javaBlockState) { NbtMap tag = NbtMap.builder() .putInt("x", position.getX()) .putInt("y", position.getY()) @@ -142,42 +188,49 @@ public class BlockInventoryHolder extends InventoryHolder { } @Override - public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void openInventory(GeyserSession session, Container container) { ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket(); - containerOpenPacket.setId((byte) inventory.getBedrockId()); + containerOpenPacket.setId((byte) container.getBedrockId()); containerOpenPacket.setType(containerType); - containerOpenPacket.setBlockPosition(inventory.getHolderPosition()); - containerOpenPacket.setUniqueEntityId(inventory.getHolderId()); + containerOpenPacket.setBlockPosition(container.getHolderPosition()); + containerOpenPacket.setUniqueEntityId(container.getHolderId()); session.sendUpstreamPacket(containerOpenPacket); + + GeyserImpl.getInstance().getLogger().debug(session, containerOpenPacket.toString()); } @Override - public void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { - if (inventory instanceof Container container) { - if (container.isUsingRealBlock() && !(inventory instanceof LecternContainer)) { - // No need to reset a block since we didn't change any blocks - // But send a container close packet because we aren't destroying the original. - ContainerClosePacket packet = new ContainerClosePacket(); - packet.setId((byte) inventory.getBedrockId()); - packet.setServerInitiated(true); - session.sendUpstreamPacket(packet); - return; + public void closeInventory(GeyserSession session, Container container, ContainerType type) { + if (container.isDisplayed() && !(container instanceof LecternContainer)) { + // No need to reset a block since we didn't change any blocks + // But send a container close packet because we aren't destroying the original. + ContainerClosePacket packet = new ContainerClosePacket(); + packet.setId((byte) container.getBedrockId()); + packet.setServerInitiated(true); + packet.setType(type != null ? type : containerType); + session.sendUpstreamPacket(packet); + + if (container.isUsingRealBlock()) { + // Type being null indicates that the ContainerClosePacket is not effective. + // So we yeet away the block! + if (type == null) { + Vector3i holderPos = container.getHolderPosition(); + UpdateBlockPacket blockPacket = new UpdateBlockPacket(); + blockPacket.setDataLayer(0); + blockPacket.setBlockPosition(holderPos); + blockPacket.setDefinition(session.getBlockMappings().getBedrockAir()); + blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(blockPacket); + } else { + // We're using a real block and are able to close the block without destroying it, + // so we can don't need to reset it below. + return; + } } - } else { - GeyserImpl.getInstance().getLogger().warning("Tried to close a non-container inventory in a block inventory holder! "); - if (GeyserImpl.getInstance().getLogger().isDebug()) { - GeyserImpl.getInstance().getLogger().debug("Current inventory: " + inventory); - GeyserImpl.getInstance().getLogger().debug("Open inventory: " + session.getOpenInventory()); - } - // Try to save ourselves? maybe? - // https://github.com/GeyserMC/Geyser/issues/4141 - // TODO: improve once this issue is pinned down properly - session.setOpenInventory(null); - session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); - return; } - Vector3i holderPos = inventory.getHolderPosition(); + // Reset to correct block + Vector3i holderPos = container.getHolderPosition(); int realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos.getX(), holderPos.getY(), holderPos.getZ()); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java index 986df53db..a4b2eddae 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java @@ -25,12 +25,13 @@ package org.geysermc.geyser.inventory.holder; -import org.geysermc.geyser.inventory.Inventory; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; public abstract class InventoryHolder { - public abstract boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); - public abstract void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); - public abstract void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); + public abstract boolean canReuseContainer(GeyserSession session, Container container, Container oldInventory); + public abstract boolean prepareInventory(GeyserSession session, Container container); + public abstract void openInventory(GeyserSession session, Container container); + public abstract void closeInventory(GeyserSession session, Container container, ContainerType containerType); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java b/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java new file mode 100644 index 000000000..33935e190 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory.item; + +import lombok.Getter; +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.util.MinecraftKey; + +import java.util.Locale; + +@Getter +public enum BannerPattern { + BASE("b"), + SQUARE_BOTTOM_LEFT("bl"), + SQUARE_BOTTOM_RIGHT("br"), + SQUARE_TOP_LEFT("tl"), + SQUARE_TOP_RIGHT("tr"), + STRIPE_BOTTOM("bs"), + STRIPE_TOP("ts"), + STRIPE_LEFT("ls"), + STRIPE_RIGHT("rs"), + STRIPE_CENTER("cs"), + STRIPE_MIDDLE("ms"), + STRIPE_DOWNRIGHT("drs"), + STRIPE_DOWNLEFT("dls"), + SMALL_STRIPES("ss"), + CROSS("cr"), + STRAIGHT_CROSS("sc"), + TRIANGLE_BOTTOM("bt"), + TRIANGLE_TOP("tt"), + TRIANGLES_BOTTOM("bts"), + TRIANGLES_TOP("tts"), + DIAGONAL_LEFT("ld"), + DIAGONAL_UP_RIGHT("rd"), + DIAGONAL_UP_LEFT("lud"), + DIAGONAL_RIGHT("rud"), + CIRCLE("mc"), + RHOMBUS("mr"), + HALF_VERTICAL("vh"), + HALF_HORIZONTAL("hh"), + HALF_VERTICAL_RIGHT("vhr"), + HALF_HORIZONTAL_BOTTOM("hhb"), + BORDER("bo"), + CURLY_BORDER("cbo"), + GRADIENT("gra"), + GRADIENT_UP("gru"), + BRICKS("bri"), + GLOBE("glb"), + CREEPER("cre"), + SKULL("sku"), + FLOWER("flo"), + MOJANG("moj"), + PIGLIN("pig"), + FLOW("flw"), + GUSTER("gus"); + + private static final BannerPattern[] VALUES = values(); + + private final Key javaIdentifier; + private final String bedrockIdentifier; + + BannerPattern(String bedrockIdentifier) { + this.javaIdentifier = MinecraftKey.key(this.name().toLowerCase(Locale.ROOT)); + this.bedrockIdentifier = bedrockIdentifier; + } + + public static BannerPattern getByJavaIdentifier(Key key) { + for (BannerPattern bannerPattern : VALUES) { + if (bannerPattern.javaIdentifier.equals(key)) { + return bannerPattern; + } + } + return BASE; // Default fallback + } + + public static @Nullable BannerPattern getByBedrockIdentifier(String bedrockIdentifier) { + for (BannerPattern bannerPattern : VALUES) { + if (bannerPattern.bedrockIdentifier.equals(bedrockIdentifier)) { + return bannerPattern; + } + } + return null; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/Enchantment.java b/core/src/main/java/org/geysermc/geyser/inventory/item/BedrockEnchantment.java similarity index 50% rename from core/src/main/java/org/geysermc/geyser/inventory/item/Enchantment.java rename to core/src/main/java/org/geysermc/geyser/inventory/item/BedrockEnchantment.java index 5fa2a5784..6d3fdbc27 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/Enchantment.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/BedrockEnchantment.java @@ -25,13 +25,11 @@ package org.geysermc.geyser.inventory.item; -import lombok.Getter; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Locale; -@Getter -public enum Enchantment { +public enum BedrockEnchantment { PROTECTION, FIRE_PROTECTION, FEATHER_FALLING, @@ -69,18 +67,21 @@ public enum Enchantment { PIERCING, QUICK_CHARGE, SOUL_SPEED, - SWIFT_SNEAK; + SWIFT_SNEAK, + WIND_BURST, + DENSITY, + BREACH; - private static final Enchantment[] VALUES = values(); + private static final BedrockEnchantment[] VALUES = values(); private final String javaIdentifier; - Enchantment() { + BedrockEnchantment() { this.javaIdentifier = "minecraft:" + this.name().toLowerCase(Locale.ENGLISH); } - public static @Nullable Enchantment getByJavaIdentifier(String javaIdentifier) { - for (Enchantment enchantment : VALUES) { + public static @Nullable BedrockEnchantment getByJavaIdentifier(String javaIdentifier) { + for (BedrockEnchantment enchantment : VALUES) { if (enchantment.javaIdentifier.equals(javaIdentifier) || enchantment.name().toLowerCase(Locale.ENGLISH).equalsIgnoreCase(javaIdentifier)) { return enchantment; } @@ -88,85 +89,10 @@ public enum Enchantment { return null; } - public static @Nullable Enchantment getByBedrockId(int bedrockId) { + public static @Nullable BedrockEnchantment getByBedrockId(int bedrockId) { if (bedrockId >= 0 && bedrockId < VALUES.length) { return VALUES[bedrockId]; } return null; } - - /** - * Enchantments classified by their Java index - */ - public enum JavaEnchantment { - PROTECTION, - FIRE_PROTECTION, - FEATHER_FALLING, - BLAST_PROTECTION, - PROJECTILE_PROTECTION, - RESPIRATION, - AQUA_AFFINITY, - THORNS, - DEPTH_STRIDER, - FROST_WALKER, - BINDING_CURSE, - SOUL_SPEED, - SWIFT_SNEAK, - SHARPNESS, - SMITE, - BANE_OF_ARTHROPODS, - KNOCKBACK, - FIRE_ASPECT, - LOOTING, - SWEEPING, - EFFICIENCY, - SILK_TOUCH, - UNBREAKING, - FORTUNE, - POWER, - PUNCH, - FLAME, - INFINITY, - LUCK_OF_THE_SEA, - LURE, - LOYALTY, - IMPALING, - RIPTIDE, - CHANNELING, - MULTISHOT, - QUICK_CHARGE, - PIERCING, - MENDING, - VANISHING_CURSE; - - private static final JavaEnchantment[] VALUES = JavaEnchantment.values(); - - public static JavaEnchantment of(int index) { - return VALUES[index]; - } - - /** - * A list of all enchantment Java identifiers for use with command suggestions. - */ - public static final String[] ALL_JAVA_IDENTIFIERS; - - public static @Nullable JavaEnchantment getByJavaIdentifier(String javaIdentifier) { - if (!javaIdentifier.startsWith("minecraft:")) { - javaIdentifier = "minecraft:" + javaIdentifier; - } - for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) { - if (ALL_JAVA_IDENTIFIERS[i].equalsIgnoreCase(javaIdentifier)) { - return VALUES[i]; - } - } - return null; - } - - static { - ALL_JAVA_IDENTIFIERS = new String[VALUES.length]; - for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) { - ALL_JAVA_IDENTIFIERS[i] = "minecraft:" + VALUES[i].name().toLowerCase(Locale.ENGLISH); - } - } - } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/DyeColor.java b/core/src/main/java/org/geysermc/geyser/inventory/item/DyeColor.java new file mode 100644 index 000000000..e2649a343 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/DyeColor.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory.item; + +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Locale; + +@Getter +public enum DyeColor { + WHITE, + ORANGE, + MAGENTA, + LIGHT_BLUE, + YELLOW, + LIME, + PINK, + GRAY, + LIGHT_GRAY, + CYAN, + PURPLE, + BLUE, + BROWN, + GREEN, + RED, + BLACK; + + private static final DyeColor[] VALUES = values(); + + private final String javaIdentifier; + + DyeColor() { + this.javaIdentifier = this.name().toLowerCase(Locale.ROOT); + } + + public static @Nullable DyeColor getById(int id) { + if (id >= 0 && id < VALUES.length) { + return VALUES[id]; + } + return null; + } + + public static @Nullable DyeColor getByJavaIdentifier(String javaIdentifier) { + for (DyeColor dyeColor : VALUES) { + if (dyeColor.javaIdentifier.equals(javaIdentifier)) { + return dyeColor; + } + } + return null; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java new file mode 100644 index 000000000..dc4a2030d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory.item; + +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.geyser.util.SoundUtils; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; +import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; + +import java.util.Locale; + +public interface GeyserInstrument { + + static GeyserInstrument read(RegistryEntryContext context) { + NbtMap data = context.data(); + String soundEvent = SoundUtils.readSoundEvent(data, "instrument " + context.id()); + float range = data.getFloat("range"); + String description = MessageTranslator.deserializeDescriptionForTooltip(context.session(), data); + BedrockInstrument bedrockInstrument = BedrockInstrument.getByJavaIdentifier(context.id()); + return new GeyserInstrument.Impl(soundEvent, range, description, bedrockInstrument); + } + + String soundEvent(); + + float range(); + + /** + * In Bedrock format + */ + String description(); + + BedrockInstrument bedrockInstrument(); + + /** + * @return the ID of the Bedrock counterpart for this instrument. If there is none ({@link #bedrockInstrument()} is null), then -1 is returned. + */ + default int bedrockId() { + BedrockInstrument bedrockInstrument = bedrockInstrument(); + if (bedrockInstrument != null) { + return bedrockInstrument.ordinal(); + } + return -1; + } + + /** + * @return the ID of the Java counterpart for the given Bedrock ID. If an invalid Bedrock ID was given, or there is no counterpart, -1 is returned. + */ + static int bedrockIdToJava(GeyserSession session, int id) { + JavaRegistry instruments = session.getRegistryCache().registry(JavaRegistries.INSTRUMENT); + BedrockInstrument bedrockInstrument = BedrockInstrument.getByBedrockId(id); + if (bedrockInstrument != null) { + for (int i = 0; i < instruments.values().size(); i++) { + GeyserInstrument instrument = instruments.byId(i); + if (instrument.bedrockInstrument() == bedrockInstrument) { + return i; + } + } + } + return -1; + } + + // TODO test in 1.21.5 + static GeyserInstrument fromComponent(GeyserSession session, InstrumentComponent component) { + if (component.instrumentLocation() != null) { + return session.getRegistryCache().registry(JavaRegistries.INSTRUMENT).byKey(component.instrumentLocation()); + } else if (component.instrumentHolder() != null) { + if (component.instrumentHolder().isId()) { + return session.getRegistryCache().registry(JavaRegistries.INSTRUMENT).byId(component.instrumentHolder().id()); + } + InstrumentComponent.Instrument custom = component.instrumentHolder().custom(); + return new Wrapper(custom, session.locale()); + } + throw new IllegalStateException("InstrumentComponent must have either a location or a holder"); + } + + record Wrapper(InstrumentComponent.Instrument instrument, String locale) implements GeyserInstrument { + @Override + public String soundEvent() { + return instrument.soundEvent().getName(); + } + + @Override + public float range() { + return instrument.range(); + } + + @Override + public String description() { + return MessageTranslator.convertMessageForTooltip(instrument.description(), locale); + } + + @Override + public BedrockInstrument bedrockInstrument() { + if (instrument.soundEvent() instanceof BuiltinSound) { + return BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.soundEvent().getName())); + } + // Probably custom + return null; + } + } + + record Impl(String soundEvent, float range, String description, @Nullable BedrockInstrument bedrockInstrument) implements GeyserInstrument { + } + + /** + * Each vanilla instrument on Bedrock, ordered in their network IDs. + */ + enum BedrockInstrument { + PONDER, + SING, + SEEK, + FEEL, + ADMIRE, + CALL, + YEARN, + DREAM; + + private static final BedrockInstrument[] VALUES = values(); + private final Key javaIdentifier; + + BedrockInstrument() { + this.javaIdentifier = MinecraftKey.key(this.name().toLowerCase(Locale.ENGLISH) + "_goat_horn"); + } + + public static @Nullable BedrockInstrument getByJavaIdentifier(Key javaIdentifier) { + for (BedrockInstrument instrument : VALUES) { + if (instrument.javaIdentifier.equals(javaIdentifier)) { + return instrument; + } + } + return null; + } + + public static @Nullable BedrockInstrument getByBedrockId(int bedrockId) { + if (bedrockId >= 0 && bedrockId < VALUES.length) { + return VALUES[bedrockId]; + } + return null; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/Potion.java b/core/src/main/java/org/geysermc/geyser/inventory/item/Potion.java index 7ce0ee278..21de6394e 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/Potion.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/Potion.java @@ -27,65 +27,82 @@ package org.geysermc.geyser.inventory.item; import lombok.Getter; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; +import java.util.Collections; import java.util.Locale; @Getter public enum Potion { - WATER(0), - MUNDANE(1), - THICK(3), - AWKWARD(4), - NIGHT_VISION(5), - LONG_NIGHT_VISION(6), - INVISIBILITY(7), - LONG_INVISIBILITY(8), - LEAPING(9), - STRONG_LEAPING(11), - LONG_LEAPING(10), - FIRE_RESISTANCE(12), - LONG_FIRE_RESISTANCE(13), - SWIFTNESS(14), - STRONG_SWIFTNESS(16), - LONG_SWIFTNESS(15), - SLOWNESS(17), - STRONG_SLOWNESS(42), - LONG_SLOWNESS(18), - WATER_BREATHING(19), - LONG_WATER_BREATHING(20), - HEALING(21), - STRONG_HEALING(22), - HARMING(23), - STRONG_HARMING(24), - POISON(25), - STRONG_POISON(27), - LONG_POISON(26), - REGENERATION(28), - STRONG_REGENERATION(30), - LONG_REGENERATION(29), - STRENGTH(31), - STRONG_STRENGTH(33), - LONG_STRENGTH(32), - WEAKNESS(34), - LONG_WEAKNESS(35), - LUCK(2), //does not exist - TURTLE_MASTER(37), - STRONG_TURTLE_MASTER(39), - LONG_TURTLE_MASTER(38), - SLOW_FALLING(40), - LONG_SLOW_FALLING(41); + WATER(0, ArrowParticleColors.NONE), + MUNDANE(1, ArrowParticleColors.NONE), // 2 is extended? + THICK(3, ArrowParticleColors.NONE), + AWKWARD(4, ArrowParticleColors.NONE), + NIGHT_VISION(5, ArrowParticleColors.NIGHT_VISION), + LONG_NIGHT_VISION(6, ArrowParticleColors.NIGHT_VISION), + INVISIBILITY(7, ArrowParticleColors.INVISIBILITY), + LONG_INVISIBILITY(8, ArrowParticleColors.INVISIBILITY), + LEAPING(9, ArrowParticleColors.LEAPING), + LONG_LEAPING(10, ArrowParticleColors.LEAPING), + STRONG_LEAPING(11, ArrowParticleColors.LEAPING), + FIRE_RESISTANCE(12, ArrowParticleColors.FIRE_RESISTANCE), + LONG_FIRE_RESISTANCE(13, ArrowParticleColors.FIRE_RESISTANCE), + SWIFTNESS(14, ArrowParticleColors.SWIFTNESS), + LONG_SWIFTNESS(15, ArrowParticleColors.SWIFTNESS), + STRONG_SWIFTNESS(16, ArrowParticleColors.SWIFTNESS), + SLOWNESS(17, ArrowParticleColors.SLOWNESS), + LONG_SLOWNESS(18, ArrowParticleColors.SLOWNESS), + STRONG_SLOWNESS(42, ArrowParticleColors.SLOWNESS), + TURTLE_MASTER(37, ArrowParticleColors.TURTLE_MASTER), + LONG_TURTLE_MASTER(38, ArrowParticleColors.TURTLE_MASTER), + STRONG_TURTLE_MASTER(39, ArrowParticleColors.TURTLE_MASTER), + WATER_BREATHING(19, ArrowParticleColors.WATER_BREATHING), + LONG_WATER_BREATHING(20, ArrowParticleColors.WATER_BREATHING), + HEALING(21, ArrowParticleColors.HEALING), + STRONG_HEALING(22, ArrowParticleColors.HEALING), + HARMING(23, ArrowParticleColors.HARMING), + STRONG_HARMING(24, ArrowParticleColors.HARMING), + POISON(25, ArrowParticleColors.POISON), + LONG_POISON(26, ArrowParticleColors.POISON), + STRONG_POISON(27, ArrowParticleColors.POISON), + REGENERATION(28, ArrowParticleColors.REGENERATION), + LONG_REGENERATION(29, ArrowParticleColors.REGENERATION), + STRONG_REGENERATION(30, ArrowParticleColors.REGENERATION), + STRENGTH(31, ArrowParticleColors.STRENGTH), + LONG_STRENGTH(32, ArrowParticleColors.STRENGTH), + STRONG_STRENGTH(33, ArrowParticleColors.STRENGTH), + WEAKNESS(34, ArrowParticleColors.WEAKNESS), + LONG_WEAKNESS(35, ArrowParticleColors.WEAKNESS), + LUCK(2, ArrowParticleColors.NONE), // does not exist in Bedrock + SLOW_FALLING(40, ArrowParticleColors.SLOW_FALLING), + LONG_SLOW_FALLING(41, ArrowParticleColors.SLOW_FALLING), + WIND_CHARGING(43, ArrowParticleColors.WIND_CHARGING), + WEAVING(44, ArrowParticleColors.WEAVING), + OOZING(45, ArrowParticleColors.OOZING), + INFESTATION(46, ArrowParticleColors.INFESTATION); public static final Potion[] VALUES = values(); private final String javaIdentifier; private final short bedrockId; + private final int javaColor; - Potion(int bedrockId) { + Potion(int bedrockId, int javaColor) { this.javaIdentifier = "minecraft:" + this.name().toLowerCase(Locale.ENGLISH); this.bedrockId = (short) bedrockId; + this.javaColor = javaColor; } - public static @Nullable Potion getByJavaIdentifier(String javaIdentifier) { + public int tippedArrowId() { + // +1 likely to offset 0 as nothing? + return this.bedrockId + 1; + } + + public PotionContents toComponent() { + return new PotionContents(this.ordinal(), -1, Collections.emptyList(), null); + } + + public static Potion getByJavaIdentifier(String javaIdentifier) { for (Potion potion : VALUES) { if (potion.javaIdentifier.equals(javaIdentifier)) { return potion; @@ -94,6 +111,13 @@ public enum Potion { return null; } + public static @Nullable Potion getByJavaId(int javaId) { + if (javaId >= 0 && javaId < VALUES.length) { + return VALUES[javaId]; + } + return null; + } + public static @Nullable Potion getByBedrockId(int bedrockId) { for (Potion potion : VALUES) { if (potion.bedrockId == bedrockId) { @@ -102,4 +126,44 @@ public enum Potion { } return null; } + + public static @Nullable Potion getByTippedArrowDamage(int bedrockId) { + return getByBedrockId(bedrockId - 1); + } + + public static byte toTippedArrowId(int javaParticleColor) { + for (Potion potion : VALUES) { + if (potion.javaColor == javaParticleColor) { + return (byte) (potion.bedrockId + 1); + } + } + return (byte) 0; + } + + /** + * For tipped arrow usage + */ + private static final class ArrowParticleColors { + static final int NONE = 1; + static final int NIGHT_VISION = 2039713; + static final int INVISIBILITY = 8356754; + static final int LEAPING = 2293580; + static final int FIRE_RESISTANCE = 14981690; + static final int SWIFTNESS = 8171462; + static final int SLOWNESS = 5926017; + static final int TURTLE_MASTER = 7691106; + static final int WATER_BREATHING = 3035801; + static final int HEALING = 16262179; + static final int HARMING = 4393481; + static final int POISON = 5149489; + static final int REGENERATION = 13458603; + static final int STRENGTH = 9643043; + static final int WEAKNESS = 4738376; + static final int LUCK = 3381504; + static final int SLOW_FALLING = 16773073; + static final int WIND_CHARGING = 12438015; + static final int WEAVING = 7891290; + static final int OOZING = 10092451; + static final int INFESTATION = 9214860; + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index c15a5d3b4..a58c218f0 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -40,32 +40,24 @@ import java.util.Map; @Getter @Accessors(fluent = true) public class StoredItemMappings { - private final ItemMapping bamboo; - private final ItemMapping banner; private final ItemMapping barrier; private final ItemMapping compass; - private final ItemMapping crossbow; - private final ItemMapping egg; private final ItemMapping glassBottle; private final ItemMapping milkBucket; private final ItemMapping powderSnowBucket; - private final ItemMapping shield; + private final ItemMapping totem; private final ItemMapping upgradeTemplate; private final ItemMapping wheat; private final ItemMapping writableBook; private final ItemMapping writtenBook; public StoredItemMappings(Map itemMappings) { - this.bamboo = load(itemMappings, Items.BAMBOO); - this.banner = load(itemMappings, Items.WHITE_BANNER); // As of 1.17.10, all banners have the same Bedrock ID this.barrier = load(itemMappings, Items.BARRIER); this.compass = load(itemMappings, Items.COMPASS); - this.crossbow = load(itemMappings, Items.CROSSBOW); - this.egg = load(itemMappings, Items.EGG); this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE); this.milkBucket = load(itemMappings, Items.MILK_BUCKET); this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET); - this.shield = load(itemMappings, Items.SHIELD); + this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING); this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); this.wheat = load(itemMappings, Items.WHEAT); this.writableBook = load(itemMappings, Items.WRITABLE_BOOK); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java b/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java deleted file mode 100644 index 3ba0ad56f..000000000 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.inventory.item; - -import lombok.Getter; -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.util.Locale; - -/** - * Potion identifiers and their respective Bedrock IDs used with arrows. - * See here - */ -@Getter -public enum TippedArrowPotion { - MUNDANE(2, ArrowParticleColors.NONE), // 3 is extended? - THICK(4, ArrowParticleColors.NONE), - AWKWARD(5, ArrowParticleColors.NONE), - NIGHT_VISION(6, ArrowParticleColors.NIGHT_VISION), - LONG_NIGHT_VISION(7, ArrowParticleColors.NIGHT_VISION), - INVISIBILITY(8, ArrowParticleColors.INVISIBILITY), - LONG_INVISIBILITY(9, ArrowParticleColors.INVISIBILITY), - LEAPING(10, ArrowParticleColors.LEAPING), - LONG_LEAPING(11, ArrowParticleColors.LEAPING), - STRONG_LEAPING(12, ArrowParticleColors.LEAPING), - FIRE_RESISTANCE(13, ArrowParticleColors.FIRE_RESISTANCE), - LONG_FIRE_RESISTANCE(14, ArrowParticleColors.FIRE_RESISTANCE), - SWIFTNESS(15, ArrowParticleColors.SWIFTNESS), - LONG_SWIFTNESS(16, ArrowParticleColors.SWIFTNESS), - STRONG_SWIFTNESS(17, ArrowParticleColors.SWIFTNESS), - SLOWNESS(18, ArrowParticleColors.SLOWNESS), - LONG_SLOWNESS(19, ArrowParticleColors.SLOWNESS), - STRONG_SLOWNESS(43, ArrowParticleColors.SLOWNESS), - WATER_BREATHING(20, ArrowParticleColors.WATER_BREATHING), - LONG_WATER_BREATHING(21, ArrowParticleColors.WATER_BREATHING), - HEALING(22, ArrowParticleColors.HEALING), - STRONG_HEALING(23, ArrowParticleColors.HEALING), - HARMING(24, ArrowParticleColors.HARMING), - STRONG_HARMING(25, ArrowParticleColors.HARMING), - POISON(26, ArrowParticleColors.POISON), - LONG_POISON(27, ArrowParticleColors.POISON), - STRONG_POISON(28, ArrowParticleColors.POISON), - REGENERATION(29, ArrowParticleColors.REGENERATION), - LONG_REGENERATION(30, ArrowParticleColors.REGENERATION), - STRONG_REGENERATION(31, ArrowParticleColors.REGENERATION), - STRENGTH(32, ArrowParticleColors.STRENGTH), - LONG_STRENGTH(33, ArrowParticleColors.STRENGTH), - STRONG_STRENGTH(34, ArrowParticleColors.STRENGTH), - WEAKNESS(35, ArrowParticleColors.WEAKNESS), - LONG_WEAKNESS(36, ArrowParticleColors.WEAKNESS), - LUCK(2, ArrowParticleColors.NONE), // does not exist in Bedrock - TURTLE_MASTER(38, ArrowParticleColors.TURTLE_MASTER), - LONG_TURTLE_MASTER(39, ArrowParticleColors.TURTLE_MASTER), - STRONG_TURTLE_MASTER(40, ArrowParticleColors.TURTLE_MASTER), - SLOW_FALLING(41, ArrowParticleColors.SLOW_FALLING), - LONG_SLOW_FALLING(42, ArrowParticleColors.SLOW_FALLING); - - private static final TippedArrowPotion[] VALUES = values(); - - private final String javaIdentifier; - private final short bedrockId; - /** - * The Java color associated with this ID. - * Used for looking up Java arrow color entity metadata as Bedrock potion IDs, which is what is used for entities in Bedrock - */ - private final int javaColor; - - TippedArrowPotion(int bedrockId, ArrowParticleColors arrowParticleColor) { - this.javaIdentifier = "minecraft:" + this.name().toLowerCase(Locale.ENGLISH); - this.bedrockId = (short) bedrockId; - this.javaColor = arrowParticleColor.getColor(); - } - - public static @Nullable TippedArrowPotion getByJavaIdentifier(String javaIdentifier) { - for (TippedArrowPotion potion : VALUES) { - if (potion.javaIdentifier.equals(javaIdentifier)) { - return potion; - } - } - return null; - } - - public static @Nullable TippedArrowPotion getByBedrockId(int bedrockId) { - for (TippedArrowPotion potion : VALUES) { - if (potion.bedrockId == bedrockId) { - return potion; - } - } - return null; - } - - /** - * @param color the potion color to look up - * @return the tipped arrow potion that most closely resembles that color. - */ - public static @Nullable TippedArrowPotion getByJavaColor(int color) { - for (TippedArrowPotion potion : VALUES) { - if (potion.javaColor == color) { - return potion; - } - } - return null; - } - - private enum ArrowParticleColors { - NONE(-1), - NIGHT_VISION(2039713), - INVISIBILITY(8356754), - LEAPING(2293580), - FIRE_RESISTANCE(14981690), - SWIFTNESS(8171462), - SLOWNESS(5926017), - TURTLE_MASTER(7691106), - WATER_BREATHING(3035801), - HEALING(16262179), - HARMING(4393481), - POISON(5149489), - REGENERATION(13458603), - STRENGTH(9643043), - WEAKNESS(4738376), - LUCK(3381504), - SLOW_FALLING(16773073); - - @Getter - private final int color; - - ArrowParticleColors(int color) { - this.color = color; - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java index 641d5ad94..7d25c5803 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java @@ -25,12 +25,16 @@ package org.geysermc.geyser.inventory.recipe; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; + /** - * A more compact version of {@link com.github.steveice10.mc.protocol.data.game.recipe.Recipe}. + * A more compact version of {@link org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDisplay}. */ public interface GeyserRecipe { /** * Whether the recipe is flexible or not in which items can be placed where. */ boolean isShaped(); + + SlotDisplay result(); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java index 05c17cf9f..7fc1d52aa 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.inventory.recipe; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient; -import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapedCraftingRecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; -public record GeyserShapedRecipe(int width, int height, Ingredient[] ingredients, @Nullable ItemStack result) implements GeyserRecipe { +import java.util.List; - public GeyserShapedRecipe(ShapedRecipeData data) { - this(data.getWidth(), data.getHeight(), data.getIngredients(), data.getResult()); +public record GeyserShapedRecipe(int width, int height, List ingredients, SlotDisplay result) implements GeyserRecipe { + + public GeyserShapedRecipe(ShapedCraftingRecipeDisplay data) { + this(data.width(), data.height(), data.ingredients(), data.result()); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java index e300e3ec8..ed513a804 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.inventory.recipe; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient; -import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapelessCraftingRecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; -public record GeyserShapelessRecipe(Ingredient[] ingredients, @Nullable ItemStack result) implements GeyserRecipe { +import java.util.List; - public GeyserShapelessRecipe(ShapelessRecipeData data) { - this(data.getIngredients(), data.getResult()); +public record GeyserShapelessRecipe(List ingredients, SlotDisplay result) implements GeyserRecipe { + + public GeyserShapelessRecipe(ShapelessCraftingRecipeDisplay data) { + this(data.ingredients(), data.result()); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java new file mode 100644 index 000000000..7e4131a4c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory.recipe; + +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.SmithingRecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; + +public record GeyserSmithingRecipe(SlotDisplay template, + SlotDisplay base, + SlotDisplay addition, + SlotDisplay result) implements GeyserRecipe { + public GeyserSmithingRecipe(SmithingRecipeDisplay display) { + this(display.template(), display.base(), display.addition(), display.result()); + } + + @Override + public boolean isShaped() { + return false; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserStonecutterData.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserStonecutterData.java index 22163eced..7bd21ecfa 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserStonecutterData.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserStonecutterData.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.inventory.recipe; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.checkerframework.checker.nullness.qual.Nullable; /** diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java index 584928e65..9b49f8577 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java @@ -25,24 +25,31 @@ package org.geysermc.geyser.inventory.recipe; +import net.kyori.adventure.text.Component; import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.text.ChatColor; +import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ArmorTrim; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ProvidesTrimMaterial; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.HashMap; +import java.util.Map; /** - * Hardcoded recipe information about armor trims until further improvements can be made. This information was scraped - * from BDS 1.19.81 with a world with the next_major_update and sniffer features enabled, using ProxyPass. + * Stores information on trim materials and patterns, including smithing armor hacks for pre-1.20. */ -public class TrimRecipe { - - // For TrimDataPacket, which BDS sends just before the CraftingDataPacket - public static final List PATTERNS; - public static final List MATERIALS; +public final class TrimRecipe { + private static final Map trimMaterialProviders = new HashMap<>(); // For CraftingDataPacket public static final String ID = "minecraft:smithing_armor_trim"; @@ -50,44 +57,65 @@ public class TrimRecipe { public static final ItemDescriptorWithCount ADDITION = tagDescriptor("minecraft:trim_materials"); public static final ItemDescriptorWithCount TEMPLATE = tagDescriptor("minecraft:trim_templates"); - static { - List patterns = new ArrayList<>(16); - patterns.add(new TrimPattern("minecraft:ward_armor_trim_smithing_template", "ward")); - patterns.add(new TrimPattern("minecraft:sentry_armor_trim_smithing_template", "sentry")); - patterns.add(new TrimPattern("minecraft:snout_armor_trim_smithing_template", "snout")); - patterns.add(new TrimPattern("minecraft:dune_armor_trim_smithing_template", "dune")); - patterns.add(new TrimPattern("minecraft:spire_armor_trim_smithing_template", "spire")); - patterns.add(new TrimPattern("minecraft:tide_armor_trim_smithing_template", "tide")); - patterns.add(new TrimPattern("minecraft:wild_armor_trim_smithing_template", "wild")); - patterns.add(new TrimPattern("minecraft:rib_armor_trim_smithing_template", "rib")); - patterns.add(new TrimPattern("minecraft:coast_armor_trim_smithing_template", "coast")); - patterns.add(new TrimPattern("minecraft:shaper_armor_trim_smithing_template", "shaper")); - patterns.add(new TrimPattern("minecraft:eye_armor_trim_smithing_template", "eye")); - patterns.add(new TrimPattern("minecraft:vex_armor_trim_smithing_template", "vex")); - patterns.add(new TrimPattern("minecraft:silence_armor_trim_smithing_template", "silence")); - patterns.add(new TrimPattern("minecraft:wayfinder_armor_trim_smithing_template", "wayfinder")); - patterns.add(new TrimPattern("minecraft:raiser_armor_trim_smithing_template", "raiser")); - patterns.add(new TrimPattern("minecraft:host_armor_trim_smithing_template", "host")); - PATTERNS = Collections.unmodifiableList(patterns); + public static TrimMaterial readTrimMaterial(RegistryEntryContext context) { + String key = context.id().asMinimalString(); - List materials = new ArrayList<>(10); - materials.add(new TrimMaterial("quartz", "§h", "minecraft:quartz")); - materials.add(new TrimMaterial("iron", "§i", "minecraft:iron_ingot")); - materials.add(new TrimMaterial("netherite", "§j", "minecraft:netherite_ingot")); - materials.add(new TrimMaterial("redstone", "§m", "minecraft:redstone")); - materials.add(new TrimMaterial("copper", "§n", "minecraft:copper_ingot")); - materials.add(new TrimMaterial("gold", "§p", "minecraft:gold_ingot")); - materials.add(new TrimMaterial("emerald", "§q", "minecraft:emerald")); - materials.add(new TrimMaterial("diamond", "§s", "minecraft:diamond")); - materials.add(new TrimMaterial("lapis", "§t", "minecraft:lapis_lazuli")); - materials.add(new TrimMaterial("amethyst", "§u", "minecraft:amethyst_shard")); - MATERIALS = Collections.unmodifiableList(materials); + // Color is used when hovering over the item + // Find the nearest legacy color from the style Java gives us to work with + Component description = MessageTranslator.componentFromNbtTag(context.data().get("description")); + String legacy = MessageTranslator.convertMessage(Component.space().style(description.style())); + String color = legacy.isBlank() ? ChatColor.WHITE : legacy.substring(2).trim(); + + int networkId = context.getNetworkId(context.id()); + ItemMapping trimItem = null; + for (ProvidesTrimMaterial provider : materialProviders().keySet()) { + Holder materialHolder = provider.materialHolder(); + if (context.id().equals(provider.materialLocation()) || (materialHolder != null && materialHolder.isId() && materialHolder.id() == networkId)) { + trimItem = context.session().getItemMappings().getMapping(materialProviders().get(provider)); + break; + } + } + + if (trimItem == null) { + // This happens for custom trim materials, not sure what to do here. + GeyserImpl.getInstance().getLogger().debug("Unable to found trim material item for material " + context.id()); + trimItem = ItemMapping.AIR; + } + + // Just pick out the resulting color code, without RESET in front. + return new TrimMaterial(key, color, trimItem.getBedrockIdentifier()); + } + + public static TrimPattern readTrimPattern(RegistryEntryContext context) { + String key = context.id().asMinimalString(); + + // Not ideal, Java edition also gives us a translatable description... Bedrock wants the template item + String identifier = context.id().asString() + "_armor_trim_smithing_template"; + ItemMapping itemMapping = context.session().getItemMappings().getMapping(identifier); + if (itemMapping == null) { + // This should never happen so not sure what to do here. + itemMapping = ItemMapping.AIR; + } + return new TrimPattern(itemMapping.getBedrockIdentifier(), key); } private TrimRecipe() { //no-op } + // Lazy initialise + private static Map materialProviders() { + if (trimMaterialProviders.isEmpty()) { + for (Item item : Registries.JAVA_ITEMS.get()) { + ProvidesTrimMaterial provider = item.getComponent(DataComponentTypes.PROVIDES_TRIM_MATERIAL); + if (provider != null) { + trimMaterialProviders.put(provider, item); + } + } + } + return trimMaterialProviders; + } + private static ItemDescriptorWithCount tagDescriptor(String tag) { return new ItemDescriptorWithCount(new ItemTagDescriptor(tag), 1); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java index 5adee0c20..89af5842b 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java @@ -25,15 +25,9 @@ package org.geysermc.geyser.inventory.updater; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.ListTag; -import com.github.steveice10.opennbt.tag.builtin.StringTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntMaps; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.kyori.adventure.text.Component; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; @@ -43,15 +37,21 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.AnvilContainer; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; -import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment; +import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.registry.Registries; -import org.geysermc.geyser.registry.type.EnchantmentData; +import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket; +import java.util.List; +import java.util.Map; import java.util.Objects; public class AnvilInventoryUpdater extends InventoryUpdater { @@ -60,11 +60,11 @@ public class AnvilInventoryUpdater extends InventoryUpdater { private static final int MAX_LEVEL_COST = 40; @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); AnvilContainer anvilContainer = (AnvilContainer) inventory; updateInventoryState(session, anvilContainer); - int targetSlot = getTargetSlot(anvilContainer); + int targetSlot = getTargetSlot(anvilContainer, session); for (int i = 0; i < translator.size; i++) { final int bedrockSlot = translator.javaSlotToBedrock(i); if (bedrockSlot == 50) @@ -82,14 +82,14 @@ public class AnvilInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; AnvilContainer anvilContainer = (AnvilContainer) inventory; updateInventoryState(session, anvilContainer); int lastTargetSlot = anvilContainer.getLastTargetSlot(); - int targetSlot = getTargetSlot(anvilContainer); + int targetSlot = getTargetSlot(anvilContainer, session); if (targetSlot != javaSlot) { // Update the requested slot InventorySlotPacket slotPacket = new InventorySlotPacket(); @@ -118,7 +118,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { // Changing the item in the input slot resets the name field on Bedrock, but // does not result in a FilterTextPacket - String originalName = MessageTranslator.convertToPlainTextLenient(ItemUtils.getCustomName(input.getNbt()), session.locale()); + String originalName = MessageTranslator.convertToPlainText(input.getComponent(DataComponentTypes.CUSTOM_NAME), session.locale()); ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(originalName); session.sendDownstreamGamePacket(renameItemPacket); @@ -136,12 +136,12 @@ public class AnvilInventoryUpdater extends InventoryUpdater { * @param anvilContainer the anvil inventory * @return the slot to change the repair cost */ - private int getTargetSlot(AnvilContainer anvilContainer) { + private int getTargetSlot(AnvilContainer anvilContainer, GeyserSession session) { GeyserItemStack input = anvilContainer.getInput(); GeyserItemStack material = anvilContainer.getMaterial(); if (!material.isEmpty()) { - if (!input.isEmpty() && isRepairing(input, material)) { + if (!input.isEmpty() && isRepairing(input, material, session)) { // Changing the repair cost on the material item makes it non-stackable return 0; } @@ -151,7 +151,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { return 0; } - private void updateTargetSlot(InventoryTranslator translator, GeyserSession session, AnvilContainer anvilContainer, int slot) { + private void updateTargetSlot(InventoryTranslator translator, GeyserSession session, AnvilContainer anvilContainer, int slot) { ItemData itemData = anvilContainer.getItem(slot).getItemData(session); itemData = hijackRepairCost(session, anvilContainer, itemData); @@ -223,7 +223,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { if (!material.isEmpty()) { totalRepairCost += getRepairCost(material); if (isCombining(input, material)) { - if (hasDurability(input) && input.getJavaId() == material.getJavaId()) { + if (hasDurability(input) && input.isSameItem(material)) { cost += calcMergeRepairCost(input, material); } @@ -234,7 +234,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { // Can't repair or merge enchantments return -1; } - } else if (hasDurability(input) && isRepairing(input, material)) { + } else if (hasDurability(input) && isRepairing(input, material, session)) { cost = calcRepairLevelCost(input, material); if (cost == -1) { // No damage to repair @@ -266,14 +266,14 @@ public class AnvilInventoryUpdater extends InventoryUpdater { */ private int calcRepairLevelCost(GeyserItemStack input, GeyserItemStack material) { int newDamage = getDamage(input); - int unitRepair = Math.min(newDamage, input.asItem().maxDamage() / 4); + int unitRepair = Math.min(newDamage, input.asItem().defaultMaxDamage() / 4); if (unitRepair <= 0) { // No damage to repair return -1; } for (int i = 0; i < material.getAmount(); i++) { newDamage -= unitRepair; - unitRepair = Math.min(newDamage, input.asItem().maxDamage() / 4); + unitRepair = Math.min(newDamage, input.asItem().defaultMaxDamage() / 4); if (unitRepair <= 0) { return i + 1; } @@ -290,7 +290,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { */ private int calcMergeRepairCost(GeyserItemStack input, GeyserItemStack material) { // If the material item is damaged 112% or more, then the input item will not be repaired - if (getDamage(input) > 0 && getDamage(material) < (material.asItem().maxDamage() * 112 / 100)) { + if (getDamage(input) > 0 && getDamage(material) < (material.asItem().defaultMaxDamage() * 112 / 100)) { return 2; } return 0; @@ -307,18 +307,15 @@ public class AnvilInventoryUpdater extends InventoryUpdater { */ private int calcMergeEnchantmentCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material, boolean bedrock) { boolean hasCompatible = false; - Object2IntMap combinedEnchantments = getEnchantments(input, bedrock); + Object2IntMap combinedEnchantments = getEnchantments(session, input); int cost = 0; - for (Object2IntMap.Entry entry : getEnchantments(material, bedrock).object2IntEntrySet()) { - JavaEnchantment enchantment = entry.getKey(); - EnchantmentData data = Registries.ENCHANTMENTS.get(enchantment); - if (data == null) { - GeyserImpl.getInstance().getLogger().debug("Java enchantment not in registry: " + enchantment); - continue; - } + for (Object2IntMap.Entry entry : getEnchantments(session, material).object2IntEntrySet()) { + Enchantment enchantment = entry.getKey(); - boolean canApply = isEnchantedBook(input) || data.validItems().contains(input.getJavaId()); - for (JavaEnchantment incompatible : data.incompatibleEnchantments()) { + boolean canApply = isEnchantedBook(input) || enchantment.supportedItems().contains(session, input.asItem()); + + List incompatibleEnchantments = enchantment.exclusiveSet().resolve(session); + for (Enchantment incompatible : incompatibleEnchantments) { if (combinedEnchantments.containsKey(incompatible)) { canApply = false; if (!bedrock) { @@ -334,12 +331,12 @@ public class AnvilInventoryUpdater extends InventoryUpdater { newLevel++; } newLevel = Math.max(currentLevel, newLevel); - if (newLevel > data.maxLevel()) { - newLevel = data.maxLevel(); + if (newLevel > enchantment.maxLevel()) { + newLevel = enchantment.maxLevel(); } combinedEnchantments.put(enchantment, newLevel); - int rarityMultiplier = data.rarityMultiplier(); + int rarityMultiplier = enchantment.anvilCost(); if (isEnchantedBook(material) && rarityMultiplier > 1) { rarityMultiplier /= 2; } @@ -347,11 +344,11 @@ public class AnvilInventoryUpdater extends InventoryUpdater { if (newLevel > currentLevel) { hasCompatible = true; } - if (enchantment == JavaEnchantment.IMPALING) { + if (enchantment.bedrockEnchantment() == BedrockEnchantment.IMPALING) { // Multiplier is halved on Bedrock for some reason rarityMultiplier /= 2; - } else if (enchantment == JavaEnchantment.SWEEPING) { - // Doesn't exist on Bedrock + } else if (enchantment.bedrockEnchantment() == null) { + // Whatever this is, doesn't exist on Bedrock rarityMultiplier = 0; } cost += rarityMultiplier * (newLevel - currentLevel); @@ -368,54 +365,43 @@ public class AnvilInventoryUpdater extends InventoryUpdater { return cost; } - private Object2IntMap getEnchantments(GeyserItemStack itemStack, boolean bedrock) { - if (itemStack.getNbt() == null) { - return Object2IntMaps.emptyMap(); - } - Object2IntMap enchantments = new Object2IntOpenHashMap<>(); - Tag enchantmentTag; + private Object2IntMap getEnchantments(GeyserSession session, GeyserItemStack itemStack) { + ItemEnchantments enchantmentComponent; if (isEnchantedBook(itemStack)) { - enchantmentTag = itemStack.getNbt().get("StoredEnchantments"); + enchantmentComponent = itemStack.getComponent(DataComponentTypes.STORED_ENCHANTMENTS); } else { - enchantmentTag = itemStack.getNbt().get("Enchantments"); + enchantmentComponent = itemStack.getComponent(DataComponentTypes.ENCHANTMENTS); } - if (enchantmentTag instanceof ListTag listTag) { - for (Tag tag : listTag.getValue()) { - if (tag instanceof CompoundTag enchantTag) { - if (enchantTag.get("id") instanceof StringTag javaEnchId) { - JavaEnchantment enchantment = JavaEnchantment.getByJavaIdentifier(javaEnchId.getValue()); - if (enchantment == null) { - GeyserImpl.getInstance().getLogger().debug("Unknown Java enchantment in anvil: " + javaEnchId.getValue()); - continue; - } - - Tag javaEnchLvl = enchantTag.get("lvl"); - if (javaEnchLvl == null || !(javaEnchLvl.getValue() instanceof Number number)) - continue; - - // Handle duplicate enchantments - if (bedrock) { - enchantments.putIfAbsent(enchantment, number.intValue()); - } else { - enchantments.mergeInt(enchantment, number.intValue(), Math::max); - } - } + if (enchantmentComponent != null) { + Object2IntMap enchantments = new Object2IntOpenHashMap<>(); + for (Map.Entry entry : enchantmentComponent.getEnchantments().entrySet()) { + Enchantment enchantment = session.getRegistryCache().registry(JavaRegistries.ENCHANTMENT).byId(entry.getKey()); + if (enchantment == null) { + GeyserImpl.getInstance().getLogger().debug("Unknown Java enchantment in anvil: " + entry.getKey()); + continue; } + enchantments.put(enchantment, entry.getValue().intValue()); } + return enchantments; } - return enchantments; + return new Object2IntOpenHashMap<>(); } private boolean isEnchantedBook(GeyserItemStack itemStack) { - return itemStack.asItem() == Items.ENCHANTED_BOOK; + return itemStack.is(Items.ENCHANTED_BOOK); } private boolean isCombining(GeyserItemStack input, GeyserItemStack material) { - return isEnchantedBook(material) || (input.getJavaId() == material.getJavaId() && hasDurability(input)); + return isEnchantedBook(material) || (input.isSameItem(material) && hasDurability(input)); } - private boolean isRepairing(GeyserItemStack input, GeyserItemStack material) { - return input.asItem().isValidRepairItem(material.asItem()); + private boolean isRepairing(GeyserItemStack input, GeyserItemStack material, GeyserSession session) { + HolderSet repairable = input.getComponent(DataComponentTypes.REPAIRABLE); + if (repairable == null) { + return false; + } + + return material.is(session, repairable); } private boolean isRenaming(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) { @@ -424,38 +410,27 @@ public class AnvilInventoryUpdater extends InventoryUpdater { } // This should really check the name field in all cases, but that requires the localized name // of the item which can change depending on NBT and Minecraft Edition - String originalName = ItemUtils.getCustomName(anvilContainer.getInput().getNbt()); + Component originalName = anvilContainer.getInput().getComponent(DataComponentTypes.CUSTOM_NAME); if (bedrock && originalName != null && anvilContainer.getNewName() != null) { // Check text and formatting - String legacyOriginalName = MessageTranslator.convertMessageLenient(originalName, session.locale()); + String legacyOriginalName = MessageTranslator.convertMessage(originalName, session.locale()); return !legacyOriginalName.equals(anvilContainer.getNewName()); } - return !Objects.equals(originalName, ItemUtils.getCustomName(anvilContainer.getResult().getNbt())); - } - - @SuppressWarnings("SameParameterValue") - private int getTagIntValueOr(GeyserItemStack itemStack, String tagName, int defaultValue) { - if (itemStack.getNbt() != null) { - Tag tag = itemStack.getNbt().get(tagName); - if (tag != null && tag.getValue() instanceof Number value) { - return value.intValue(); - } - } - return defaultValue; + return !Objects.equals(originalName, anvilContainer.getResult().getComponent(DataComponentTypes.CUSTOM_NAME)); } private int getRepairCost(GeyserItemStack itemStack) { - return getTagIntValueOr(itemStack, "RepairCost", 0); + return itemStack.getComponentElseGet(DataComponentTypes.REPAIR_COST, () -> 0); } private boolean hasDurability(GeyserItemStack itemStack) { - if (itemStack.asItem().maxDamage() > 0) { - return getTagIntValueOr(itemStack, "Unbreakable", 0) == 0; + if (itemStack.asItem().defaultMaxDamage() > 0) { + return itemStack.getComponent(DataComponentTypes.UNBREAKABLE) != null; } return false; } private int getDamage(GeyserItemStack itemStack) { - return getTagIntValueOr(itemStack, "Damage", 0); + return itemStack.getComponentElseGet(DataComponentTypes.DAMAGE, () -> 0); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java index 5d6214871..29e02523b 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java @@ -25,10 +25,10 @@ package org.geysermc.geyser.inventory.updater; +import lombok.AllArgsConstructor; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; -import lombok.AllArgsConstructor; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; @@ -46,7 +46,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { private final int paddedSize; @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); List bedrockItems = new ArrayList<>(paddedSize); @@ -65,7 +65,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java index c9f313f2a..5294eba04 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java @@ -38,7 +38,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { public static final ContainerInventoryUpdater INSTANCE = new ContainerInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); ItemData[] bedrockItems = new ItemData[translator.size]; @@ -53,7 +53,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java index 4474d420c..621e32b07 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java @@ -44,7 +44,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { public static final CrafterInventoryUpdater INSTANCE = new CrafterInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { ItemData[] bedrockItems; InventoryContentPacket contentPacket; @@ -74,7 +74,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { int containerId; if (javaSlot < CrafterInventoryTranslator.GRID_SIZE || javaSlot == CrafterInventoryTranslator.JAVA_RESULT_SLOT) { // Parts of the Crafter UI diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java index 7441e66d0..2f2c79ebf 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java @@ -38,7 +38,7 @@ public class HorseInventoryUpdater extends InventoryUpdater { public static final HorseInventoryUpdater INSTANCE = new HorseInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); ItemData[] bedrockItems = new ItemData[translator.size]; @@ -53,7 +53,7 @@ public class HorseInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java index 68ee334ba..4fd6a6aa3 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java @@ -36,7 +36,7 @@ import org.geysermc.geyser.translator.inventory.InventoryTranslator; import java.util.Arrays; public class InventoryUpdater { - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { ItemData[] bedrockItems = new ItemData[36]; for (int i = 0; i < 36; i++) { final int offset = i < 9 ? 27 : -9; @@ -48,7 +48,7 @@ public class InventoryUpdater { session.sendUpstreamPacket(contentPacket); } - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (javaSlot >= translator.size) { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(ContainerId.INVENTORY); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java index a23385b53..b025eecbb 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java @@ -35,7 +35,7 @@ public class UIInventoryUpdater extends InventoryUpdater { public static final UIInventoryUpdater INSTANCE = new UIInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); for (int i = 0; i < translator.size; i++) { @@ -51,7 +51,7 @@ public class UIInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/item/ArmorMaterial.java b/core/src/main/java/org/geysermc/geyser/item/ArmorMaterial.java index 315a8cd4d..348c0af8c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/ArmorMaterial.java +++ b/core/src/main/java/org/geysermc/geyser/item/ArmorMaterial.java @@ -31,12 +31,13 @@ import java.util.function.Supplier; public enum ArmorMaterial { LEATHER(() -> Items.LEATHER), - CHAIN(() -> Items.IRON_INGOT), + CHAINMAIL(() -> Items.IRON_INGOT), IRON(() -> Items.IRON_INGOT), GOLD(() -> Items.GOLD_INGOT), DIAMOND(() -> Items.DIAMOND), - TURTLE(() -> Items.SCUTE), - NETHERITE(() -> Items.NETHERITE_INGOT); + TURTLE(() -> Items.TURTLE_SCUTE), + NETHERITE(() -> Items.NETHERITE_INGOT), + ARMADILLO(() -> Items.ARMADILLO_SCUTE); private final Supplier repairIngredient; diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java index d71f2e548..22c457179 100644 --- a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.item; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; -import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData; -public record GeyserCustomMappingData(ComponentItemData componentItemData, ItemDefinition itemDefinition, String stringId, int integerId) { +public record GeyserCustomMappingData(ItemDefinition itemDefinition, String stringId, int integerId) { } diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java index 9d86bfa96..063a86a03 100644 --- a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -55,6 +55,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i private final boolean isEdible; private final boolean canAlwaysEat; private final boolean isChargeable; + private final String block; public GeyserNonVanillaCustomItemData(Builder builder) { super(builder.name, builder.customItemOptions, builder.displayName, builder.icon, builder.allowOffhand, @@ -78,6 +79,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i this.isEdible = builder.edible; this.canAlwaysEat = builder.canAlwaysEat; this.isChargeable = builder.chargeable; + this.block = builder.block; } @Override @@ -110,6 +112,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i return toolType; } + @SuppressWarnings("removal") @Override public String toolTier() { return toolTier; @@ -130,6 +133,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i return translationString; } + @SuppressWarnings("removal") @Override public Set repairMaterials() { return repairMaterials; @@ -160,6 +164,11 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i return isChargeable; } + @Override + public String block() { + return block; + } + public static class Builder extends GeyserCustomItemData.Builder implements NonVanillaCustomItemData.Builder { private String identifier = null; private int javaId = -1; @@ -186,6 +195,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i private boolean edible = false; private boolean canAlwaysEat = false; private boolean chargeable = false; + private String block = null; @Override public Builder name(@NonNull String name) { @@ -339,6 +349,12 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i return this; } + @Override + public Builder block(String block) { + this.block = block; + return this; + } + @Override public NonVanillaCustomItemData build() { if (identifier == null || javaId == -1) { diff --git a/core/src/main/java/org/geysermc/geyser/item/Items.java b/core/src/main/java/org/geysermc/geyser/item/Items.java index 2147ebb2c..d0bf6655d 100644 --- a/core/src/main/java/org/geysermc/geyser/item/Items.java +++ b/core/src/main/java/org/geysermc/geyser/item/Items.java @@ -25,12 +25,40 @@ package org.geysermc.geyser.item; -import org.geysermc.geyser.item.components.ToolTier; -import org.geysermc.geyser.item.type.*; +import org.geysermc.geyser.item.type.ArmorItem; +import org.geysermc.geyser.item.type.ArrowItem; +import org.geysermc.geyser.item.type.AxolotlBucketItem; +import org.geysermc.geyser.item.type.BannerItem; +import org.geysermc.geyser.item.type.BlockItem; +import org.geysermc.geyser.item.type.BoatItem; +import org.geysermc.geyser.item.type.CompassItem; +import org.geysermc.geyser.item.type.CrossbowItem; +import org.geysermc.geyser.item.type.DecoratedPotItem; +import org.geysermc.geyser.item.type.DyeItem; +import org.geysermc.geyser.item.type.DyeableArmorItem; +import org.geysermc.geyser.item.type.EnchantedBookItem; +import org.geysermc.geyser.item.type.FilledMapItem; +import org.geysermc.geyser.item.type.FireworkRocketItem; +import org.geysermc.geyser.item.type.FireworkStarItem; +import org.geysermc.geyser.item.type.FishingRodItem; +import org.geysermc.geyser.item.type.GoatHornItem; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.item.type.LightItem; +import org.geysermc.geyser.item.type.MapItem; +import org.geysermc.geyser.item.type.OminousBottleItem; +import org.geysermc.geyser.item.type.PlayerHeadItem; +import org.geysermc.geyser.item.type.PotionItem; +import org.geysermc.geyser.item.type.ShieldItem; +import org.geysermc.geyser.item.type.ShulkerBoxItem; +import org.geysermc.geyser.item.type.SpawnEggItem; +import org.geysermc.geyser.item.type.TippedArrowItem; +import org.geysermc.geyser.item.type.TropicalFishBucketItem; +import org.geysermc.geyser.item.type.WolfArmorItem; +import org.geysermc.geyser.item.type.WritableBookItem; +import org.geysermc.geyser.item.type.WrittenBookItem; +import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.registry.Registries; -import java.util.Collections; - import static org.geysermc.geyser.item.type.Item.builder; /** @@ -39,803 +67,900 @@ import static org.geysermc.geyser.item.type.Item.builder; @SuppressWarnings("unused") public final class Items { public static final Item AIR = register(new Item("air", builder())); - public static final Item STONE = register(new BlockItem("stone", builder())); - public static final Item GRANITE = register(new BlockItem("granite", builder())); - public static final Item POLISHED_GRANITE = register(new BlockItem("polished_granite", builder())); - public static final Item DIORITE = register(new BlockItem("diorite", builder())); - public static final Item POLISHED_DIORITE = register(new BlockItem("polished_diorite", builder())); - public static final Item ANDESITE = register(new BlockItem("andesite", builder())); - public static final Item POLISHED_ANDESITE = register(new BlockItem("polished_andesite", builder())); - public static final Item DEEPSLATE = register(new BlockItem("deepslate", builder())); - public static final Item COBBLED_DEEPSLATE = register(new BlockItem("cobbled_deepslate", builder())); - public static final Item POLISHED_DEEPSLATE = register(new BlockItem("polished_deepslate", builder())); - public static final Item CALCITE = register(new BlockItem("calcite", builder())); - public static final Item TUFF = register(new BlockItem("tuff", builder())); - public static final Item TUFF_SLAB = register(new BlockItem("tuff_slab", builder())); - public static final Item TUFF_STAIRS = register(new BlockItem("tuff_stairs", builder())); - public static final Item TUFF_WALL = register(new BlockItem("tuff_wall", builder())); - public static final Item CHISELED_TUFF = register(new BlockItem("chiseled_tuff", builder())); - public static final Item POLISHED_TUFF = register(new BlockItem("polished_tuff", builder())); - public static final Item POLISHED_TUFF_SLAB = register(new BlockItem("polished_tuff_slab", builder())); - public static final Item POLISHED_TUFF_STAIRS = register(new BlockItem("polished_tuff_stairs", builder())); - public static final Item POLISHED_TUFF_WALL = register(new BlockItem("polished_tuff_wall", builder())); - public static final Item TUFF_BRICKS = register(new BlockItem("tuff_bricks", builder())); - public static final Item TUFF_BRICK_SLAB = register(new BlockItem("tuff_brick_slab", builder())); - public static final Item TUFF_BRICK_STAIRS = register(new BlockItem("tuff_brick_stairs", builder())); - public static final Item TUFF_BRICK_WALL = register(new BlockItem("tuff_brick_wall", builder())); - public static final Item CHISELED_TUFF_BRICKS = register(new BlockItem("chiseled_tuff_bricks", builder())); - public static final Item DRIPSTONE_BLOCK = register(new BlockItem("dripstone_block", builder())); - public static final Item GRASS_BLOCK = register(new BlockItem("grass_block", builder())); - public static final Item DIRT = register(new BlockItem("dirt", builder())); - public static final Item COARSE_DIRT = register(new BlockItem("coarse_dirt", builder())); - public static final Item PODZOL = register(new BlockItem("podzol", builder())); - public static final Item ROOTED_DIRT = register(new BlockItem("rooted_dirt", builder())); - public static final Item MUD = register(new BlockItem("mud", builder())); - public static final Item CRIMSON_NYLIUM = register(new BlockItem("crimson_nylium", builder())); - public static final Item WARPED_NYLIUM = register(new BlockItem("warped_nylium", builder())); - public static final Item COBBLESTONE = register(new BlockItem("cobblestone", builder())); - public static final Item OAK_PLANKS = register(new BlockItem("oak_planks", builder())); - public static final Item SPRUCE_PLANKS = register(new BlockItem("spruce_planks", builder())); - public static final Item BIRCH_PLANKS = register(new BlockItem("birch_planks", builder())); - public static final Item JUNGLE_PLANKS = register(new BlockItem("jungle_planks", builder())); - public static final Item ACACIA_PLANKS = register(new BlockItem("acacia_planks", builder())); - public static final Item CHERRY_PLANKS = register(new BlockItem("cherry_planks", builder())); - public static final Item DARK_OAK_PLANKS = register(new BlockItem("dark_oak_planks", builder())); - public static final Item MANGROVE_PLANKS = register(new BlockItem("mangrove_planks", builder())); - public static final Item BAMBOO_PLANKS = register(new BlockItem("bamboo_planks", builder())); - public static final Item CRIMSON_PLANKS = register(new BlockItem("crimson_planks", builder())); - public static final Item WARPED_PLANKS = register(new BlockItem("warped_planks", builder())); - public static final Item BAMBOO_MOSAIC = register(new BlockItem("bamboo_mosaic", builder())); - public static final Item OAK_SAPLING = register(new BlockItem("oak_sapling", builder())); - public static final Item SPRUCE_SAPLING = register(new BlockItem("spruce_sapling", builder())); - public static final Item BIRCH_SAPLING = register(new BlockItem("birch_sapling", builder())); - public static final Item JUNGLE_SAPLING = register(new BlockItem("jungle_sapling", builder())); - public static final Item ACACIA_SAPLING = register(new BlockItem("acacia_sapling", builder())); - public static final Item CHERRY_SAPLING = register(new BlockItem("cherry_sapling", builder())); - public static final Item DARK_OAK_SAPLING = register(new BlockItem("dark_oak_sapling", builder())); - public static final Item MANGROVE_PROPAGULE = register(new BlockItem("mangrove_propagule", builder())); - public static final Item BEDROCK = register(new BlockItem("bedrock", builder())); - public static final Item SAND = register(new BlockItem("sand", builder())); - public static final Item SUSPICIOUS_SAND = register(new BlockItem("suspicious_sand", builder())); - public static final Item SUSPICIOUS_GRAVEL = register(new BlockItem("suspicious_gravel", builder())); - public static final Item RED_SAND = register(new BlockItem("red_sand", builder())); - public static final Item GRAVEL = register(new BlockItem("gravel", builder())); - public static final Item COAL_ORE = register(new BlockItem("coal_ore", builder())); - public static final Item DEEPSLATE_COAL_ORE = register(new BlockItem("deepslate_coal_ore", builder())); - public static final Item IRON_ORE = register(new BlockItem("iron_ore", builder())); - public static final Item DEEPSLATE_IRON_ORE = register(new BlockItem("deepslate_iron_ore", builder())); - public static final Item COPPER_ORE = register(new BlockItem("copper_ore", builder())); - public static final Item DEEPSLATE_COPPER_ORE = register(new BlockItem("deepslate_copper_ore", builder())); - public static final Item GOLD_ORE = register(new BlockItem("gold_ore", builder())); - public static final Item DEEPSLATE_GOLD_ORE = register(new BlockItem("deepslate_gold_ore", builder())); - public static final Item REDSTONE_ORE = register(new BlockItem("redstone_ore", builder())); - public static final Item DEEPSLATE_REDSTONE_ORE = register(new BlockItem("deepslate_redstone_ore", builder())); - public static final Item EMERALD_ORE = register(new BlockItem("emerald_ore", builder())); - public static final Item DEEPSLATE_EMERALD_ORE = register(new BlockItem("deepslate_emerald_ore", builder())); - public static final Item LAPIS_ORE = register(new BlockItem("lapis_ore", builder())); - public static final Item DEEPSLATE_LAPIS_ORE = register(new BlockItem("deepslate_lapis_ore", builder())); - public static final Item DIAMOND_ORE = register(new BlockItem("diamond_ore", builder())); - public static final Item DEEPSLATE_DIAMOND_ORE = register(new BlockItem("deepslate_diamond_ore", builder())); - public static final Item NETHER_GOLD_ORE = register(new BlockItem("nether_gold_ore", builder())); - public static final Item NETHER_QUARTZ_ORE = register(new BlockItem("nether_quartz_ore", builder())); - public static final Item ANCIENT_DEBRIS = register(new BlockItem("ancient_debris", builder())); - public static final Item COAL_BLOCK = register(new BlockItem("coal_block", builder())); - public static final Item RAW_IRON_BLOCK = register(new BlockItem("raw_iron_block", builder())); - public static final Item RAW_COPPER_BLOCK = register(new BlockItem("raw_copper_block", builder())); - public static final Item RAW_GOLD_BLOCK = register(new BlockItem("raw_gold_block", builder())); - public static final Item AMETHYST_BLOCK = register(new BlockItem("amethyst_block", builder())); - public static final Item BUDDING_AMETHYST = register(new BlockItem("budding_amethyst", builder())); - public static final Item IRON_BLOCK = register(new BlockItem("iron_block", builder())); - public static final Item COPPER_BLOCK = register(new BlockItem("copper_block", builder())); - public static final Item GOLD_BLOCK = register(new BlockItem("gold_block", builder())); - public static final Item DIAMOND_BLOCK = register(new BlockItem("diamond_block", builder())); - public static final Item NETHERITE_BLOCK = register(new BlockItem("netherite_block", builder())); - public static final Item EXPOSED_COPPER = register(new BlockItem("exposed_copper", builder())); - public static final Item WEATHERED_COPPER = register(new BlockItem("weathered_copper", builder())); - public static final Item OXIDIZED_COPPER = register(new BlockItem("oxidized_copper", builder())); - public static final Item CHISELED_COPPER = register(new BlockItem("chiseled_copper", builder())); - public static final Item EXPOSED_CHISELED_COPPER = register(new BlockItem("exposed_chiseled_copper", builder())); - public static final Item WEATHERED_CHISELED_COPPER = register(new BlockItem("weathered_chiseled_copper", builder())); - public static final Item OXIDIZED_CHISELED_COPPER = register(new BlockItem("oxidized_chiseled_copper", builder())); - public static final Item CUT_COPPER = register(new BlockItem("cut_copper", builder())); - public static final Item EXPOSED_CUT_COPPER = register(new BlockItem("exposed_cut_copper", builder())); - public static final Item WEATHERED_CUT_COPPER = register(new BlockItem("weathered_cut_copper", builder())); - public static final Item OXIDIZED_CUT_COPPER = register(new BlockItem("oxidized_cut_copper", builder())); - public static final Item CUT_COPPER_STAIRS = register(new BlockItem("cut_copper_stairs", builder())); - public static final Item EXPOSED_CUT_COPPER_STAIRS = register(new BlockItem("exposed_cut_copper_stairs", builder())); - public static final Item WEATHERED_CUT_COPPER_STAIRS = register(new BlockItem("weathered_cut_copper_stairs", builder())); - public static final Item OXIDIZED_CUT_COPPER_STAIRS = register(new BlockItem("oxidized_cut_copper_stairs", builder())); - public static final Item CUT_COPPER_SLAB = register(new BlockItem("cut_copper_slab", builder())); - public static final Item EXPOSED_CUT_COPPER_SLAB = register(new BlockItem("exposed_cut_copper_slab", builder())); - public static final Item WEATHERED_CUT_COPPER_SLAB = register(new BlockItem("weathered_cut_copper_slab", builder())); - public static final Item OXIDIZED_CUT_COPPER_SLAB = register(new BlockItem("oxidized_cut_copper_slab", builder())); - public static final Item WAXED_COPPER_BLOCK = register(new BlockItem("waxed_copper_block", builder())); - public static final Item WAXED_EXPOSED_COPPER = register(new BlockItem("waxed_exposed_copper", builder())); - public static final Item WAXED_WEATHERED_COPPER = register(new BlockItem("waxed_weathered_copper", builder())); - public static final Item WAXED_OXIDIZED_COPPER = register(new BlockItem("waxed_oxidized_copper", builder())); - public static final Item WAXED_CHISELED_COPPER = register(new BlockItem("waxed_chiseled_copper", builder())); - public static final Item WAXED_EXPOSED_CHISELED_COPPER = register(new BlockItem("waxed_exposed_chiseled_copper", builder())); - public static final Item WAXED_WEATHERED_CHISELED_COPPER = register(new BlockItem("waxed_weathered_chiseled_copper", builder())); - public static final Item WAXED_OXIDIZED_CHISELED_COPPER = register(new BlockItem("waxed_oxidized_chiseled_copper", builder())); - public static final Item WAXED_CUT_COPPER = register(new BlockItem("waxed_cut_copper", builder())); - public static final Item WAXED_EXPOSED_CUT_COPPER = register(new BlockItem("waxed_exposed_cut_copper", builder())); - public static final Item WAXED_WEATHERED_CUT_COPPER = register(new BlockItem("waxed_weathered_cut_copper", builder())); - public static final Item WAXED_OXIDIZED_CUT_COPPER = register(new BlockItem("waxed_oxidized_cut_copper", builder())); - public static final Item WAXED_CUT_COPPER_STAIRS = register(new BlockItem("waxed_cut_copper_stairs", builder())); - public static final Item WAXED_EXPOSED_CUT_COPPER_STAIRS = register(new BlockItem("waxed_exposed_cut_copper_stairs", builder())); - public static final Item WAXED_WEATHERED_CUT_COPPER_STAIRS = register(new BlockItem("waxed_weathered_cut_copper_stairs", builder())); - public static final Item WAXED_OXIDIZED_CUT_COPPER_STAIRS = register(new BlockItem("waxed_oxidized_cut_copper_stairs", builder())); - public static final Item WAXED_CUT_COPPER_SLAB = register(new BlockItem("waxed_cut_copper_slab", builder())); - public static final Item WAXED_EXPOSED_CUT_COPPER_SLAB = register(new BlockItem("waxed_exposed_cut_copper_slab", builder())); - public static final Item WAXED_WEATHERED_CUT_COPPER_SLAB = register(new BlockItem("waxed_weathered_cut_copper_slab", builder())); - public static final Item WAXED_OXIDIZED_CUT_COPPER_SLAB = register(new BlockItem("waxed_oxidized_cut_copper_slab", builder())); - public static final Item OAK_LOG = register(new BlockItem("oak_log", builder())); - public static final Item SPRUCE_LOG = register(new BlockItem("spruce_log", builder())); - public static final Item BIRCH_LOG = register(new BlockItem("birch_log", builder())); - public static final Item JUNGLE_LOG = register(new BlockItem("jungle_log", builder())); - public static final Item ACACIA_LOG = register(new BlockItem("acacia_log", builder())); - public static final Item CHERRY_LOG = register(new BlockItem("cherry_log", builder())); - public static final Item DARK_OAK_LOG = register(new BlockItem("dark_oak_log", builder())); - public static final Item MANGROVE_LOG = register(new BlockItem("mangrove_log", builder())); - public static final Item MANGROVE_ROOTS = register(new BlockItem("mangrove_roots", builder())); - public static final Item MUDDY_MANGROVE_ROOTS = register(new BlockItem("muddy_mangrove_roots", builder())); - public static final Item CRIMSON_STEM = register(new BlockItem("crimson_stem", builder())); - public static final Item WARPED_STEM = register(new BlockItem("warped_stem", builder())); - public static final Item BAMBOO_BLOCK = register(new BlockItem("bamboo_block", builder())); - public static final Item STRIPPED_OAK_LOG = register(new BlockItem("stripped_oak_log", builder())); - public static final Item STRIPPED_SPRUCE_LOG = register(new BlockItem("stripped_spruce_log", builder())); - public static final Item STRIPPED_BIRCH_LOG = register(new BlockItem("stripped_birch_log", builder())); - public static final Item STRIPPED_JUNGLE_LOG = register(new BlockItem("stripped_jungle_log", builder())); - public static final Item STRIPPED_ACACIA_LOG = register(new BlockItem("stripped_acacia_log", builder())); - public static final Item STRIPPED_CHERRY_LOG = register(new BlockItem("stripped_cherry_log", builder())); - public static final Item STRIPPED_DARK_OAK_LOG = register(new BlockItem("stripped_dark_oak_log", builder())); - public static final Item STRIPPED_MANGROVE_LOG = register(new BlockItem("stripped_mangrove_log", builder())); - public static final Item STRIPPED_CRIMSON_STEM = register(new BlockItem("stripped_crimson_stem", builder())); - public static final Item STRIPPED_WARPED_STEM = register(new BlockItem("stripped_warped_stem", builder())); - public static final Item STRIPPED_OAK_WOOD = register(new BlockItem("stripped_oak_wood", builder())); - public static final Item STRIPPED_SPRUCE_WOOD = register(new BlockItem("stripped_spruce_wood", builder())); - public static final Item STRIPPED_BIRCH_WOOD = register(new BlockItem("stripped_birch_wood", builder())); - public static final Item STRIPPED_JUNGLE_WOOD = register(new BlockItem("stripped_jungle_wood", builder())); - public static final Item STRIPPED_ACACIA_WOOD = register(new BlockItem("stripped_acacia_wood", builder())); - public static final Item STRIPPED_CHERRY_WOOD = register(new BlockItem("stripped_cherry_wood", builder())); - public static final Item STRIPPED_DARK_OAK_WOOD = register(new BlockItem("stripped_dark_oak_wood", builder())); - public static final Item STRIPPED_MANGROVE_WOOD = register(new BlockItem("stripped_mangrove_wood", builder())); - public static final Item STRIPPED_CRIMSON_HYPHAE = register(new BlockItem("stripped_crimson_hyphae", builder())); - public static final Item STRIPPED_WARPED_HYPHAE = register(new BlockItem("stripped_warped_hyphae", builder())); - public static final Item STRIPPED_BAMBOO_BLOCK = register(new BlockItem("stripped_bamboo_block", builder())); - public static final Item OAK_WOOD = register(new BlockItem("oak_wood", builder())); - public static final Item SPRUCE_WOOD = register(new BlockItem("spruce_wood", builder())); - public static final Item BIRCH_WOOD = register(new BlockItem("birch_wood", builder())); - public static final Item JUNGLE_WOOD = register(new BlockItem("jungle_wood", builder())); - public static final Item ACACIA_WOOD = register(new BlockItem("acacia_wood", builder())); - public static final Item CHERRY_WOOD = register(new BlockItem("cherry_wood", builder())); - public static final Item DARK_OAK_WOOD = register(new BlockItem("dark_oak_wood", builder())); - public static final Item MANGROVE_WOOD = register(new BlockItem("mangrove_wood", builder())); - public static final Item CRIMSON_HYPHAE = register(new BlockItem("crimson_hyphae", builder())); - public static final Item WARPED_HYPHAE = register(new BlockItem("warped_hyphae", builder())); - public static final Item OAK_LEAVES = register(new BlockItem("oak_leaves", builder())); - public static final Item SPRUCE_LEAVES = register(new BlockItem("spruce_leaves", builder())); - public static final Item BIRCH_LEAVES = register(new BlockItem("birch_leaves", builder())); - public static final Item JUNGLE_LEAVES = register(new BlockItem("jungle_leaves", builder())); - public static final Item ACACIA_LEAVES = register(new BlockItem("acacia_leaves", builder())); - public static final Item CHERRY_LEAVES = register(new BlockItem("cherry_leaves", builder())); - public static final Item DARK_OAK_LEAVES = register(new BlockItem("dark_oak_leaves", builder())); - public static final Item MANGROVE_LEAVES = register(new BlockItem("mangrove_leaves", builder())); - public static final Item AZALEA_LEAVES = register(new BlockItem("azalea_leaves", builder())); - public static final Item FLOWERING_AZALEA_LEAVES = register(new BlockItem("flowering_azalea_leaves", builder())); - public static final Item SPONGE = register(new BlockItem("sponge", builder())); - public static final Item WET_SPONGE = register(new BlockItem("wet_sponge", builder())); - public static final Item GLASS = register(new BlockItem("glass", builder())); - public static final Item TINTED_GLASS = register(new BlockItem("tinted_glass", builder())); - public static final Item LAPIS_BLOCK = register(new BlockItem("lapis_block", builder())); - public static final Item SANDSTONE = register(new BlockItem("sandstone", builder())); - public static final Item CHISELED_SANDSTONE = register(new BlockItem("chiseled_sandstone", builder())); - public static final Item CUT_SANDSTONE = register(new BlockItem("cut_sandstone", builder())); - public static final Item COBWEB = register(new BlockItem("cobweb", builder())); - public static final Item SHORT_GRASS = register(new BlockItem("short_grass", builder())); - public static final Item FERN = register(new BlockItem("fern", builder())); - public static final Item AZALEA = register(new BlockItem("azalea", builder())); - public static final Item FLOWERING_AZALEA = register(new BlockItem("flowering_azalea", builder())); - public static final Item DEAD_BUSH = register(new BlockItem("dead_bush", builder())); - public static final Item SEAGRASS = register(new BlockItem("seagrass", builder())); - public static final Item SEA_PICKLE = register(new BlockItem("sea_pickle", builder())); - public static final Item WHITE_WOOL = register(new BlockItem("white_wool", builder())); - public static final Item ORANGE_WOOL = register(new BlockItem("orange_wool", builder())); - public static final Item MAGENTA_WOOL = register(new BlockItem("magenta_wool", builder())); - public static final Item LIGHT_BLUE_WOOL = register(new BlockItem("light_blue_wool", builder())); - public static final Item YELLOW_WOOL = register(new BlockItem("yellow_wool", builder())); - public static final Item LIME_WOOL = register(new BlockItem("lime_wool", builder())); - public static final Item PINK_WOOL = register(new BlockItem("pink_wool", builder())); - public static final Item GRAY_WOOL = register(new BlockItem("gray_wool", builder())); - public static final Item LIGHT_GRAY_WOOL = register(new BlockItem("light_gray_wool", builder())); - public static final Item CYAN_WOOL = register(new BlockItem("cyan_wool", builder())); - public static final Item PURPLE_WOOL = register(new BlockItem("purple_wool", builder())); - public static final Item BLUE_WOOL = register(new BlockItem("blue_wool", builder())); - public static final Item BROWN_WOOL = register(new BlockItem("brown_wool", builder())); - public static final Item GREEN_WOOL = register(new BlockItem("green_wool", builder())); - public static final Item RED_WOOL = register(new BlockItem("red_wool", builder())); - public static final Item BLACK_WOOL = register(new BlockItem("black_wool", builder())); - public static final Item DANDELION = register(new FlowerItem("dandelion", builder())); - public static final Item POPPY = register(new FlowerItem("poppy", builder())); - public static final Item BLUE_ORCHID = register(new FlowerItem("blue_orchid", builder())); - public static final Item ALLIUM = register(new FlowerItem("allium", builder())); - public static final Item AZURE_BLUET = register(new FlowerItem("azure_bluet", builder())); - public static final Item RED_TULIP = register(new FlowerItem("red_tulip", builder())); - public static final Item ORANGE_TULIP = register(new FlowerItem("orange_tulip", builder())); - public static final Item WHITE_TULIP = register(new FlowerItem("white_tulip", builder())); - public static final Item PINK_TULIP = register(new FlowerItem("pink_tulip", builder())); - public static final Item OXEYE_DAISY = register(new FlowerItem("oxeye_daisy", builder())); - public static final Item CORNFLOWER = register(new FlowerItem("cornflower", builder())); - public static final Item LILY_OF_THE_VALLEY = register(new FlowerItem("lily_of_the_valley", builder())); - public static final Item WITHER_ROSE = register(new FlowerItem("wither_rose", builder())); - public static final Item TORCHFLOWER = register(new FlowerItem("torchflower", builder())); - public static final Item PITCHER_PLANT = register(new BlockItem("pitcher_plant", builder())); - public static final Item SPORE_BLOSSOM = register(new BlockItem("spore_blossom", builder())); - public static final Item BROWN_MUSHROOM = register(new BlockItem("brown_mushroom", builder())); - public static final Item RED_MUSHROOM = register(new BlockItem("red_mushroom", builder())); - public static final Item CRIMSON_FUNGUS = register(new BlockItem("crimson_fungus", builder())); - public static final Item WARPED_FUNGUS = register(new BlockItem("warped_fungus", builder())); - public static final Item CRIMSON_ROOTS = register(new BlockItem("crimson_roots", builder())); - public static final Item WARPED_ROOTS = register(new BlockItem("warped_roots", builder())); - public static final Item NETHER_SPROUTS = register(new BlockItem("nether_sprouts", builder())); - public static final Item WEEPING_VINES = register(new BlockItem("weeping_vines", builder())); - public static final Item TWISTING_VINES = register(new BlockItem("twisting_vines", builder())); - public static final Item SUGAR_CANE = register(new BlockItem("sugar_cane", builder())); - public static final Item KELP = register(new BlockItem("kelp", builder())); - public static final Item MOSS_CARPET = register(new BlockItem("moss_carpet", builder())); - public static final Item PINK_PETALS = register(new BlockItem("pink_petals", builder())); - public static final Item MOSS_BLOCK = register(new BlockItem("moss_block", builder())); - public static final Item HANGING_ROOTS = register(new BlockItem("hanging_roots", builder())); - public static final Item BIG_DRIPLEAF = register(new BlockItem("big_dripleaf", builder())); - public static final Item SMALL_DRIPLEAF = register(new BlockItem("small_dripleaf", builder())); - public static final Item BAMBOO = register(new BlockItem("bamboo", builder())); - public static final Item OAK_SLAB = register(new BlockItem("oak_slab", builder())); - public static final Item SPRUCE_SLAB = register(new BlockItem("spruce_slab", builder())); - public static final Item BIRCH_SLAB = register(new BlockItem("birch_slab", builder())); - public static final Item JUNGLE_SLAB = register(new BlockItem("jungle_slab", builder())); - public static final Item ACACIA_SLAB = register(new BlockItem("acacia_slab", builder())); - public static final Item CHERRY_SLAB = register(new BlockItem("cherry_slab", builder())); - public static final Item DARK_OAK_SLAB = register(new BlockItem("dark_oak_slab", builder())); - public static final Item MANGROVE_SLAB = register(new BlockItem("mangrove_slab", builder())); - public static final Item BAMBOO_SLAB = register(new BlockItem("bamboo_slab", builder())); - public static final Item BAMBOO_MOSAIC_SLAB = register(new BlockItem("bamboo_mosaic_slab", builder())); - public static final Item CRIMSON_SLAB = register(new BlockItem("crimson_slab", builder())); - public static final Item WARPED_SLAB = register(new BlockItem("warped_slab", builder())); - public static final Item STONE_SLAB = register(new BlockItem("stone_slab", builder())); - public static final Item SMOOTH_STONE_SLAB = register(new BlockItem("smooth_stone_slab", builder())); - public static final Item SANDSTONE_SLAB = register(new BlockItem("sandstone_slab", builder())); - public static final Item CUT_SANDSTONE_SLAB = register(new BlockItem("cut_sandstone_slab", builder())); - public static final Item PETRIFIED_OAK_SLAB = register(new BlockItem("petrified_oak_slab", builder())); - public static final Item COBBLESTONE_SLAB = register(new BlockItem("cobblestone_slab", builder())); - public static final Item BRICK_SLAB = register(new BlockItem("brick_slab", builder())); - public static final Item STONE_BRICK_SLAB = register(new BlockItem("stone_brick_slab", builder())); - public static final Item MUD_BRICK_SLAB = register(new BlockItem("mud_brick_slab", builder())); - public static final Item NETHER_BRICK_SLAB = register(new BlockItem("nether_brick_slab", builder())); - public static final Item QUARTZ_SLAB = register(new BlockItem("quartz_slab", builder())); - public static final Item RED_SANDSTONE_SLAB = register(new BlockItem("red_sandstone_slab", builder())); - public static final Item CUT_RED_SANDSTONE_SLAB = register(new BlockItem("cut_red_sandstone_slab", builder())); - public static final Item PURPUR_SLAB = register(new BlockItem("purpur_slab", builder())); - public static final Item PRISMARINE_SLAB = register(new BlockItem("prismarine_slab", builder())); - public static final Item PRISMARINE_BRICK_SLAB = register(new BlockItem("prismarine_brick_slab", builder())); - public static final Item DARK_PRISMARINE_SLAB = register(new BlockItem("dark_prismarine_slab", builder())); - public static final Item SMOOTH_QUARTZ = register(new BlockItem("smooth_quartz", builder())); - public static final Item SMOOTH_RED_SANDSTONE = register(new BlockItem("smooth_red_sandstone", builder())); - public static final Item SMOOTH_SANDSTONE = register(new BlockItem("smooth_sandstone", builder())); - public static final Item SMOOTH_STONE = register(new BlockItem("smooth_stone", builder())); - public static final Item BRICKS = register(new BlockItem("bricks", builder())); - public static final Item BOOKSHELF = register(new BlockItem("bookshelf", builder())); - public static final Item CHISELED_BOOKSHELF = register(new BlockItem("chiseled_bookshelf", builder())); - public static final Item DECORATED_POT = register(new DecoratedPotItem("decorated_pot", builder())); - public static final Item MOSSY_COBBLESTONE = register(new BlockItem("mossy_cobblestone", builder())); - public static final Item OBSIDIAN = register(new BlockItem("obsidian", builder())); - public static final Item TORCH = register(new BlockItem("torch", builder())); - public static final Item END_ROD = register(new BlockItem("end_rod", builder())); - public static final Item CHORUS_PLANT = register(new BlockItem("chorus_plant", builder())); - public static final Item CHORUS_FLOWER = register(new BlockItem("chorus_flower", builder())); - public static final Item PURPUR_BLOCK = register(new BlockItem("purpur_block", builder())); - public static final Item PURPUR_PILLAR = register(new BlockItem("purpur_pillar", builder())); - public static final Item PURPUR_STAIRS = register(new BlockItem("purpur_stairs", builder())); - public static final Item SPAWNER = register(new BlockItem("spawner", builder())); - public static final Item CHEST = register(new ChestItem("chest", builder())); - public static final Item CRAFTING_TABLE = register(new BlockItem("crafting_table", builder())); - public static final Item FARMLAND = register(new BlockItem("farmland", builder())); - public static final Item FURNACE = register(new BlockItem("furnace", builder())); - public static final Item LADDER = register(new BlockItem("ladder", builder())); - public static final Item COBBLESTONE_STAIRS = register(new BlockItem("cobblestone_stairs", builder())); - public static final Item SNOW = register(new BlockItem("snow", builder())); - public static final Item ICE = register(new BlockItem("ice", builder())); - public static final Item SNOW_BLOCK = register(new BlockItem("snow_block", builder())); - public static final Item CACTUS = register(new BlockItem("cactus", builder())); - public static final Item CLAY = register(new BlockItem("clay", builder())); - public static final Item JUKEBOX = register(new BlockItem("jukebox", builder())); - public static final Item OAK_FENCE = register(new BlockItem("oak_fence", builder())); - public static final Item SPRUCE_FENCE = register(new BlockItem("spruce_fence", builder())); - public static final Item BIRCH_FENCE = register(new BlockItem("birch_fence", builder())); - public static final Item JUNGLE_FENCE = register(new BlockItem("jungle_fence", builder())); - public static final Item ACACIA_FENCE = register(new BlockItem("acacia_fence", builder())); - public static final Item CHERRY_FENCE = register(new BlockItem("cherry_fence", builder())); - public static final Item DARK_OAK_FENCE = register(new BlockItem("dark_oak_fence", builder())); - public static final Item MANGROVE_FENCE = register(new BlockItem("mangrove_fence", builder())); - public static final Item BAMBOO_FENCE = register(new BlockItem("bamboo_fence", builder())); - public static final Item CRIMSON_FENCE = register(new BlockItem("crimson_fence", builder())); - public static final Item WARPED_FENCE = register(new BlockItem("warped_fence", builder())); - public static final Item PUMPKIN = register(new BlockItem("pumpkin", builder())); - public static final Item CARVED_PUMPKIN = register(new BlockItem("carved_pumpkin", builder())); - public static final Item JACK_O_LANTERN = register(new BlockItem("jack_o_lantern", builder())); - public static final Item NETHERRACK = register(new BlockItem("netherrack", builder())); - public static final Item SOUL_SAND = register(new BlockItem("soul_sand", builder())); - public static final Item SOUL_SOIL = register(new BlockItem("soul_soil", builder())); - public static final Item BASALT = register(new BlockItem("basalt", builder())); - public static final Item POLISHED_BASALT = register(new BlockItem("polished_basalt", builder())); - public static final Item SMOOTH_BASALT = register(new BlockItem("smooth_basalt", builder())); - public static final Item SOUL_TORCH = register(new BlockItem("soul_torch", builder())); - public static final Item GLOWSTONE = register(new BlockItem("glowstone", builder())); - public static final Item INFESTED_STONE = register(new BlockItem("infested_stone", builder())); - public static final Item INFESTED_COBBLESTONE = register(new BlockItem("infested_cobblestone", builder())); - public static final Item INFESTED_STONE_BRICKS = register(new BlockItem("infested_stone_bricks", builder())); - public static final Item INFESTED_MOSSY_STONE_BRICKS = register(new BlockItem("infested_mossy_stone_bricks", builder())); - public static final Item INFESTED_CRACKED_STONE_BRICKS = register(new BlockItem("infested_cracked_stone_bricks", builder())); - public static final Item INFESTED_CHISELED_STONE_BRICKS = register(new BlockItem("infested_chiseled_stone_bricks", builder())); - public static final Item INFESTED_DEEPSLATE = register(new BlockItem("infested_deepslate", builder())); - public static final Item STONE_BRICKS = register(new BlockItem("stone_bricks", builder())); - public static final Item MOSSY_STONE_BRICKS = register(new BlockItem("mossy_stone_bricks", builder())); - public static final Item CRACKED_STONE_BRICKS = register(new BlockItem("cracked_stone_bricks", builder())); - public static final Item CHISELED_STONE_BRICKS = register(new BlockItem("chiseled_stone_bricks", builder())); - public static final Item PACKED_MUD = register(new BlockItem("packed_mud", builder())); - public static final Item MUD_BRICKS = register(new BlockItem("mud_bricks", builder())); - public static final Item DEEPSLATE_BRICKS = register(new BlockItem("deepslate_bricks", builder())); - public static final Item CRACKED_DEEPSLATE_BRICKS = register(new BlockItem("cracked_deepslate_bricks", builder())); - public static final Item DEEPSLATE_TILES = register(new BlockItem("deepslate_tiles", builder())); - public static final Item CRACKED_DEEPSLATE_TILES = register(new BlockItem("cracked_deepslate_tiles", builder())); - public static final Item CHISELED_DEEPSLATE = register(new BlockItem("chiseled_deepslate", builder())); - public static final Item REINFORCED_DEEPSLATE = register(new BlockItem("reinforced_deepslate", builder())); - public static final Item BROWN_MUSHROOM_BLOCK = register(new BlockItem("brown_mushroom_block", builder())); - public static final Item RED_MUSHROOM_BLOCK = register(new BlockItem("red_mushroom_block", builder())); - public static final Item MUSHROOM_STEM = register(new BlockItem("mushroom_stem", builder())); - public static final Item IRON_BARS = register(new BlockItem("iron_bars", builder())); - public static final Item CHAIN = register(new BlockItem("chain", builder())); - public static final Item GLASS_PANE = register(new BlockItem("glass_pane", builder())); - public static final Item MELON = register(new BlockItem("melon", builder())); - public static final Item VINE = register(new BlockItem("vine", builder())); - public static final Item GLOW_LICHEN = register(new BlockItem("glow_lichen", builder())); - public static final Item BRICK_STAIRS = register(new BlockItem("brick_stairs", builder())); - public static final Item STONE_BRICK_STAIRS = register(new BlockItem("stone_brick_stairs", builder())); - public static final Item MUD_BRICK_STAIRS = register(new BlockItem("mud_brick_stairs", builder())); - public static final Item MYCELIUM = register(new BlockItem("mycelium", builder())); - public static final Item LILY_PAD = register(new BlockItem("lily_pad", builder())); - public static final Item NETHER_BRICKS = register(new BlockItem("nether_bricks", builder())); - public static final Item CRACKED_NETHER_BRICKS = register(new BlockItem("cracked_nether_bricks", builder())); - public static final Item CHISELED_NETHER_BRICKS = register(new BlockItem("chiseled_nether_bricks", builder())); - public static final Item NETHER_BRICK_FENCE = register(new BlockItem("nether_brick_fence", builder())); - public static final Item NETHER_BRICK_STAIRS = register(new BlockItem("nether_brick_stairs", builder())); - public static final Item SCULK = register(new BlockItem("sculk", builder())); - public static final Item SCULK_VEIN = register(new BlockItem("sculk_vein", builder())); - public static final Item SCULK_CATALYST = register(new BlockItem("sculk_catalyst", builder())); - public static final Item SCULK_SHRIEKER = register(new BlockItem("sculk_shrieker", builder())); - public static final Item ENCHANTING_TABLE = register(new BlockItem("enchanting_table", builder())); - public static final Item END_PORTAL_FRAME = register(new BlockItem("end_portal_frame", builder())); - public static final Item END_STONE = register(new BlockItem("end_stone", builder())); - public static final Item END_STONE_BRICKS = register(new BlockItem("end_stone_bricks", builder())); - public static final Item DRAGON_EGG = register(new BlockItem("dragon_egg", builder())); - public static final Item SANDSTONE_STAIRS = register(new BlockItem("sandstone_stairs", builder())); - public static final Item ENDER_CHEST = register(new ChestItem("ender_chest", builder())); - public static final Item EMERALD_BLOCK = register(new BlockItem("emerald_block", builder())); - public static final Item OAK_STAIRS = register(new BlockItem("oak_stairs", builder())); - public static final Item SPRUCE_STAIRS = register(new BlockItem("spruce_stairs", builder())); - public static final Item BIRCH_STAIRS = register(new BlockItem("birch_stairs", builder())); - public static final Item JUNGLE_STAIRS = register(new BlockItem("jungle_stairs", builder())); - public static final Item ACACIA_STAIRS = register(new BlockItem("acacia_stairs", builder())); - public static final Item CHERRY_STAIRS = register(new BlockItem("cherry_stairs", builder())); - public static final Item DARK_OAK_STAIRS = register(new BlockItem("dark_oak_stairs", builder())); - public static final Item MANGROVE_STAIRS = register(new BlockItem("mangrove_stairs", builder())); - public static final Item BAMBOO_STAIRS = register(new BlockItem("bamboo_stairs", builder())); - public static final Item BAMBOO_MOSAIC_STAIRS = register(new BlockItem("bamboo_mosaic_stairs", builder())); - public static final Item CRIMSON_STAIRS = register(new BlockItem("crimson_stairs", builder())); - public static final Item WARPED_STAIRS = register(new BlockItem("warped_stairs", builder())); - public static final Item COMMAND_BLOCK = register(new BlockItem("command_block", builder())); - public static final Item BEACON = register(new BlockItem("beacon", builder())); - public static final Item COBBLESTONE_WALL = register(new BlockItem("cobblestone_wall", builder())); - public static final Item MOSSY_COBBLESTONE_WALL = register(new BlockItem("mossy_cobblestone_wall", builder())); - public static final Item BRICK_WALL = register(new BlockItem("brick_wall", builder())); - public static final Item PRISMARINE_WALL = register(new BlockItem("prismarine_wall", builder())); - public static final Item RED_SANDSTONE_WALL = register(new BlockItem("red_sandstone_wall", builder())); - public static final Item MOSSY_STONE_BRICK_WALL = register(new BlockItem("mossy_stone_brick_wall", builder())); - public static final Item GRANITE_WALL = register(new BlockItem("granite_wall", builder())); - public static final Item STONE_BRICK_WALL = register(new BlockItem("stone_brick_wall", builder())); - public static final Item MUD_BRICK_WALL = register(new BlockItem("mud_brick_wall", builder())); - public static final Item NETHER_BRICK_WALL = register(new BlockItem("nether_brick_wall", builder())); - public static final Item ANDESITE_WALL = register(new BlockItem("andesite_wall", builder())); - public static final Item RED_NETHER_BRICK_WALL = register(new BlockItem("red_nether_brick_wall", builder())); - public static final Item SANDSTONE_WALL = register(new BlockItem("sandstone_wall", builder())); - public static final Item END_STONE_BRICK_WALL = register(new BlockItem("end_stone_brick_wall", builder())); - public static final Item DIORITE_WALL = register(new BlockItem("diorite_wall", builder())); - public static final Item BLACKSTONE_WALL = register(new BlockItem("blackstone_wall", builder())); - public static final Item POLISHED_BLACKSTONE_WALL = register(new BlockItem("polished_blackstone_wall", builder())); - public static final Item POLISHED_BLACKSTONE_BRICK_WALL = register(new BlockItem("polished_blackstone_brick_wall", builder())); - public static final Item COBBLED_DEEPSLATE_WALL = register(new BlockItem("cobbled_deepslate_wall", builder())); - public static final Item POLISHED_DEEPSLATE_WALL = register(new BlockItem("polished_deepslate_wall", builder())); - public static final Item DEEPSLATE_BRICK_WALL = register(new BlockItem("deepslate_brick_wall", builder())); - public static final Item DEEPSLATE_TILE_WALL = register(new BlockItem("deepslate_tile_wall", builder())); - public static final Item ANVIL = register(new BlockItem("anvil", builder())); - public static final Item CHIPPED_ANVIL = register(new BlockItem("chipped_anvil", builder())); - public static final Item DAMAGED_ANVIL = register(new BlockItem("damaged_anvil", builder())); - public static final Item CHISELED_QUARTZ_BLOCK = register(new BlockItem("chiseled_quartz_block", builder())); - public static final Item QUARTZ_BLOCK = register(new BlockItem("quartz_block", builder())); - public static final Item QUARTZ_BRICKS = register(new BlockItem("quartz_bricks", builder())); - public static final Item QUARTZ_PILLAR = register(new BlockItem("quartz_pillar", builder())); - public static final Item QUARTZ_STAIRS = register(new BlockItem("quartz_stairs", builder())); - public static final Item WHITE_TERRACOTTA = register(new BlockItem("white_terracotta", builder())); - public static final Item ORANGE_TERRACOTTA = register(new BlockItem("orange_terracotta", builder())); - public static final Item MAGENTA_TERRACOTTA = register(new BlockItem("magenta_terracotta", builder())); - public static final Item LIGHT_BLUE_TERRACOTTA = register(new BlockItem("light_blue_terracotta", builder())); - public static final Item YELLOW_TERRACOTTA = register(new BlockItem("yellow_terracotta", builder())); - public static final Item LIME_TERRACOTTA = register(new BlockItem("lime_terracotta", builder())); - public static final Item PINK_TERRACOTTA = register(new BlockItem("pink_terracotta", builder())); - public static final Item GRAY_TERRACOTTA = register(new BlockItem("gray_terracotta", builder())); - public static final Item LIGHT_GRAY_TERRACOTTA = register(new BlockItem("light_gray_terracotta", builder())); - public static final Item CYAN_TERRACOTTA = register(new BlockItem("cyan_terracotta", builder())); - public static final Item PURPLE_TERRACOTTA = register(new BlockItem("purple_terracotta", builder())); - public static final Item BLUE_TERRACOTTA = register(new BlockItem("blue_terracotta", builder())); - public static final Item BROWN_TERRACOTTA = register(new BlockItem("brown_terracotta", builder())); - public static final Item GREEN_TERRACOTTA = register(new BlockItem("green_terracotta", builder())); - public static final Item RED_TERRACOTTA = register(new BlockItem("red_terracotta", builder())); - public static final Item BLACK_TERRACOTTA = register(new BlockItem("black_terracotta", builder())); - public static final Item BARRIER = register(new BlockItem("barrier", builder())); - public static final Item LIGHT = register(new BlockItem("light", builder())); - public static final Item HAY_BLOCK = register(new BlockItem("hay_block", builder())); - public static final Item WHITE_CARPET = register(new BlockItem("white_carpet", builder())); - public static final Item ORANGE_CARPET = register(new BlockItem("orange_carpet", builder())); - public static final Item MAGENTA_CARPET = register(new BlockItem("magenta_carpet", builder())); - public static final Item LIGHT_BLUE_CARPET = register(new BlockItem("light_blue_carpet", builder())); - public static final Item YELLOW_CARPET = register(new BlockItem("yellow_carpet", builder())); - public static final Item LIME_CARPET = register(new BlockItem("lime_carpet", builder())); - public static final Item PINK_CARPET = register(new BlockItem("pink_carpet", builder())); - public static final Item GRAY_CARPET = register(new BlockItem("gray_carpet", builder())); - public static final Item LIGHT_GRAY_CARPET = register(new BlockItem("light_gray_carpet", builder())); - public static final Item CYAN_CARPET = register(new BlockItem("cyan_carpet", builder())); - public static final Item PURPLE_CARPET = register(new BlockItem("purple_carpet", builder())); - public static final Item BLUE_CARPET = register(new BlockItem("blue_carpet", builder())); - public static final Item BROWN_CARPET = register(new BlockItem("brown_carpet", builder())); - public static final Item GREEN_CARPET = register(new BlockItem("green_carpet", builder())); - public static final Item RED_CARPET = register(new BlockItem("red_carpet", builder())); - public static final Item BLACK_CARPET = register(new BlockItem("black_carpet", builder())); - public static final Item TERRACOTTA = register(new BlockItem("terracotta", builder())); - public static final Item PACKED_ICE = register(new BlockItem("packed_ice", builder())); - public static final Item DIRT_PATH = register(new BlockItem("dirt_path", builder())); - public static final Item SUNFLOWER = register(new BlockItem("sunflower", builder())); - public static final Item LILAC = register(new BlockItem("lilac", builder())); - public static final Item ROSE_BUSH = register(new BlockItem("rose_bush", builder())); - public static final Item PEONY = register(new BlockItem("peony", builder())); - public static final Item TALL_GRASS = register(new BlockItem("tall_grass", builder())); - public static final Item LARGE_FERN = register(new BlockItem("large_fern", builder())); - public static final Item WHITE_STAINED_GLASS = register(new BlockItem("white_stained_glass", builder())); - public static final Item ORANGE_STAINED_GLASS = register(new BlockItem("orange_stained_glass", builder())); - public static final Item MAGENTA_STAINED_GLASS = register(new BlockItem("magenta_stained_glass", builder())); - public static final Item LIGHT_BLUE_STAINED_GLASS = register(new BlockItem("light_blue_stained_glass", builder())); - public static final Item YELLOW_STAINED_GLASS = register(new BlockItem("yellow_stained_glass", builder())); - public static final Item LIME_STAINED_GLASS = register(new BlockItem("lime_stained_glass", builder())); - public static final Item PINK_STAINED_GLASS = register(new BlockItem("pink_stained_glass", builder())); - public static final Item GRAY_STAINED_GLASS = register(new BlockItem("gray_stained_glass", builder())); - public static final Item LIGHT_GRAY_STAINED_GLASS = register(new BlockItem("light_gray_stained_glass", builder())); - public static final Item CYAN_STAINED_GLASS = register(new BlockItem("cyan_stained_glass", builder())); - public static final Item PURPLE_STAINED_GLASS = register(new BlockItem("purple_stained_glass", builder())); - public static final Item BLUE_STAINED_GLASS = register(new BlockItem("blue_stained_glass", builder())); - public static final Item BROWN_STAINED_GLASS = register(new BlockItem("brown_stained_glass", builder())); - public static final Item GREEN_STAINED_GLASS = register(new BlockItem("green_stained_glass", builder())); - public static final Item RED_STAINED_GLASS = register(new BlockItem("red_stained_glass", builder())); - public static final Item BLACK_STAINED_GLASS = register(new BlockItem("black_stained_glass", builder())); - public static final Item WHITE_STAINED_GLASS_PANE = register(new BlockItem("white_stained_glass_pane", builder())); - public static final Item ORANGE_STAINED_GLASS_PANE = register(new BlockItem("orange_stained_glass_pane", builder())); - public static final Item MAGENTA_STAINED_GLASS_PANE = register(new BlockItem("magenta_stained_glass_pane", builder())); - public static final Item LIGHT_BLUE_STAINED_GLASS_PANE = register(new BlockItem("light_blue_stained_glass_pane", builder())); - public static final Item YELLOW_STAINED_GLASS_PANE = register(new BlockItem("yellow_stained_glass_pane", builder())); - public static final Item LIME_STAINED_GLASS_PANE = register(new BlockItem("lime_stained_glass_pane", builder())); - public static final Item PINK_STAINED_GLASS_PANE = register(new BlockItem("pink_stained_glass_pane", builder())); - public static final Item GRAY_STAINED_GLASS_PANE = register(new BlockItem("gray_stained_glass_pane", builder())); - public static final Item LIGHT_GRAY_STAINED_GLASS_PANE = register(new BlockItem("light_gray_stained_glass_pane", builder())); - public static final Item CYAN_STAINED_GLASS_PANE = register(new BlockItem("cyan_stained_glass_pane", builder())); - public static final Item PURPLE_STAINED_GLASS_PANE = register(new BlockItem("purple_stained_glass_pane", builder())); - public static final Item BLUE_STAINED_GLASS_PANE = register(new BlockItem("blue_stained_glass_pane", builder())); - public static final Item BROWN_STAINED_GLASS_PANE = register(new BlockItem("brown_stained_glass_pane", builder())); - public static final Item GREEN_STAINED_GLASS_PANE = register(new BlockItem("green_stained_glass_pane", builder())); - public static final Item RED_STAINED_GLASS_PANE = register(new BlockItem("red_stained_glass_pane", builder())); - public static final Item BLACK_STAINED_GLASS_PANE = register(new BlockItem("black_stained_glass_pane", builder())); - public static final Item PRISMARINE = register(new BlockItem("prismarine", builder())); - public static final Item PRISMARINE_BRICKS = register(new BlockItem("prismarine_bricks", builder())); - public static final Item DARK_PRISMARINE = register(new BlockItem("dark_prismarine", builder())); - public static final Item PRISMARINE_STAIRS = register(new BlockItem("prismarine_stairs", builder())); - public static final Item PRISMARINE_BRICK_STAIRS = register(new BlockItem("prismarine_brick_stairs", builder())); - public static final Item DARK_PRISMARINE_STAIRS = register(new BlockItem("dark_prismarine_stairs", builder())); - public static final Item SEA_LANTERN = register(new BlockItem("sea_lantern", builder())); - public static final Item RED_SANDSTONE = register(new BlockItem("red_sandstone", builder())); - public static final Item CHISELED_RED_SANDSTONE = register(new BlockItem("chiseled_red_sandstone", builder())); - public static final Item CUT_RED_SANDSTONE = register(new BlockItem("cut_red_sandstone", builder())); - public static final Item RED_SANDSTONE_STAIRS = register(new BlockItem("red_sandstone_stairs", builder())); - public static final Item REPEATING_COMMAND_BLOCK = register(new BlockItem("repeating_command_block", builder())); - public static final Item CHAIN_COMMAND_BLOCK = register(new BlockItem("chain_command_block", builder())); - public static final Item MAGMA_BLOCK = register(new BlockItem("magma_block", builder())); - public static final Item NETHER_WART_BLOCK = register(new BlockItem("nether_wart_block", builder())); - public static final Item WARPED_WART_BLOCK = register(new BlockItem("warped_wart_block", builder())); - public static final Item RED_NETHER_BRICKS = register(new BlockItem("red_nether_bricks", builder())); - public static final Item BONE_BLOCK = register(new BlockItem("bone_block", builder())); - public static final Item STRUCTURE_VOID = register(new BlockItem("structure_void", builder())); - public static final Item SHULKER_BOX = register(new ShulkerBoxItem("shulker_box", builder().stackSize(1))); - public static final Item WHITE_SHULKER_BOX = register(new ShulkerBoxItem("white_shulker_box", builder().stackSize(1))); - public static final Item ORANGE_SHULKER_BOX = register(new ShulkerBoxItem("orange_shulker_box", builder().stackSize(1))); - public static final Item MAGENTA_SHULKER_BOX = register(new ShulkerBoxItem("magenta_shulker_box", builder().stackSize(1))); - public static final Item LIGHT_BLUE_SHULKER_BOX = register(new ShulkerBoxItem("light_blue_shulker_box", builder().stackSize(1))); - public static final Item YELLOW_SHULKER_BOX = register(new ShulkerBoxItem("yellow_shulker_box", builder().stackSize(1))); - public static final Item LIME_SHULKER_BOX = register(new ShulkerBoxItem("lime_shulker_box", builder().stackSize(1))); - public static final Item PINK_SHULKER_BOX = register(new ShulkerBoxItem("pink_shulker_box", builder().stackSize(1))); - public static final Item GRAY_SHULKER_BOX = register(new ShulkerBoxItem("gray_shulker_box", builder().stackSize(1))); - public static final Item LIGHT_GRAY_SHULKER_BOX = register(new ShulkerBoxItem("light_gray_shulker_box", builder().stackSize(1))); - public static final Item CYAN_SHULKER_BOX = register(new ShulkerBoxItem("cyan_shulker_box", builder().stackSize(1))); - public static final Item PURPLE_SHULKER_BOX = register(new ShulkerBoxItem("purple_shulker_box", builder().stackSize(1))); - public static final Item BLUE_SHULKER_BOX = register(new ShulkerBoxItem("blue_shulker_box", builder().stackSize(1))); - public static final Item BROWN_SHULKER_BOX = register(new ShulkerBoxItem("brown_shulker_box", builder().stackSize(1))); - public static final Item GREEN_SHULKER_BOX = register(new ShulkerBoxItem("green_shulker_box", builder().stackSize(1))); - public static final Item RED_SHULKER_BOX = register(new ShulkerBoxItem("red_shulker_box", builder().stackSize(1))); - public static final Item BLACK_SHULKER_BOX = register(new ShulkerBoxItem("black_shulker_box", builder().stackSize(1))); - public static final Item WHITE_GLAZED_TERRACOTTA = register(new BlockItem("white_glazed_terracotta", builder())); - public static final Item ORANGE_GLAZED_TERRACOTTA = register(new BlockItem("orange_glazed_terracotta", builder())); - public static final Item MAGENTA_GLAZED_TERRACOTTA = register(new BlockItem("magenta_glazed_terracotta", builder())); - public static final Item LIGHT_BLUE_GLAZED_TERRACOTTA = register(new BlockItem("light_blue_glazed_terracotta", builder())); - public static final Item YELLOW_GLAZED_TERRACOTTA = register(new BlockItem("yellow_glazed_terracotta", builder())); - public static final Item LIME_GLAZED_TERRACOTTA = register(new BlockItem("lime_glazed_terracotta", builder())); - public static final Item PINK_GLAZED_TERRACOTTA = register(new BlockItem("pink_glazed_terracotta", builder())); - public static final Item GRAY_GLAZED_TERRACOTTA = register(new BlockItem("gray_glazed_terracotta", builder())); - public static final Item LIGHT_GRAY_GLAZED_TERRACOTTA = register(new BlockItem("light_gray_glazed_terracotta", builder())); - public static final Item CYAN_GLAZED_TERRACOTTA = register(new BlockItem("cyan_glazed_terracotta", builder())); - public static final Item PURPLE_GLAZED_TERRACOTTA = register(new BlockItem("purple_glazed_terracotta", builder())); - public static final Item BLUE_GLAZED_TERRACOTTA = register(new BlockItem("blue_glazed_terracotta", builder())); - public static final Item BROWN_GLAZED_TERRACOTTA = register(new BlockItem("brown_glazed_terracotta", builder())); - public static final Item GREEN_GLAZED_TERRACOTTA = register(new BlockItem("green_glazed_terracotta", builder())); - public static final Item RED_GLAZED_TERRACOTTA = register(new BlockItem("red_glazed_terracotta", builder())); - public static final Item BLACK_GLAZED_TERRACOTTA = register(new BlockItem("black_glazed_terracotta", builder())); - public static final Item WHITE_CONCRETE = register(new BlockItem("white_concrete", builder())); - public static final Item ORANGE_CONCRETE = register(new BlockItem("orange_concrete", builder())); - public static final Item MAGENTA_CONCRETE = register(new BlockItem("magenta_concrete", builder())); - public static final Item LIGHT_BLUE_CONCRETE = register(new BlockItem("light_blue_concrete", builder())); - public static final Item YELLOW_CONCRETE = register(new BlockItem("yellow_concrete", builder())); - public static final Item LIME_CONCRETE = register(new BlockItem("lime_concrete", builder())); - public static final Item PINK_CONCRETE = register(new BlockItem("pink_concrete", builder())); - public static final Item GRAY_CONCRETE = register(new BlockItem("gray_concrete", builder())); - public static final Item LIGHT_GRAY_CONCRETE = register(new BlockItem("light_gray_concrete", builder())); - public static final Item CYAN_CONCRETE = register(new BlockItem("cyan_concrete", builder())); - public static final Item PURPLE_CONCRETE = register(new BlockItem("purple_concrete", builder())); - public static final Item BLUE_CONCRETE = register(new BlockItem("blue_concrete", builder())); - public static final Item BROWN_CONCRETE = register(new BlockItem("brown_concrete", builder())); - public static final Item GREEN_CONCRETE = register(new BlockItem("green_concrete", builder())); - public static final Item RED_CONCRETE = register(new BlockItem("red_concrete", builder())); - public static final Item BLACK_CONCRETE = register(new BlockItem("black_concrete", builder())); - public static final Item WHITE_CONCRETE_POWDER = register(new BlockItem("white_concrete_powder", builder())); - public static final Item ORANGE_CONCRETE_POWDER = register(new BlockItem("orange_concrete_powder", builder())); - public static final Item MAGENTA_CONCRETE_POWDER = register(new BlockItem("magenta_concrete_powder", builder())); - public static final Item LIGHT_BLUE_CONCRETE_POWDER = register(new BlockItem("light_blue_concrete_powder", builder())); - public static final Item YELLOW_CONCRETE_POWDER = register(new BlockItem("yellow_concrete_powder", builder())); - public static final Item LIME_CONCRETE_POWDER = register(new BlockItem("lime_concrete_powder", builder())); - public static final Item PINK_CONCRETE_POWDER = register(new BlockItem("pink_concrete_powder", builder())); - public static final Item GRAY_CONCRETE_POWDER = register(new BlockItem("gray_concrete_powder", builder())); - public static final Item LIGHT_GRAY_CONCRETE_POWDER = register(new BlockItem("light_gray_concrete_powder", builder())); - public static final Item CYAN_CONCRETE_POWDER = register(new BlockItem("cyan_concrete_powder", builder())); - public static final Item PURPLE_CONCRETE_POWDER = register(new BlockItem("purple_concrete_powder", builder())); - public static final Item BLUE_CONCRETE_POWDER = register(new BlockItem("blue_concrete_powder", builder())); - public static final Item BROWN_CONCRETE_POWDER = register(new BlockItem("brown_concrete_powder", builder())); - public static final Item GREEN_CONCRETE_POWDER = register(new BlockItem("green_concrete_powder", builder())); - public static final Item RED_CONCRETE_POWDER = register(new BlockItem("red_concrete_powder", builder())); - public static final Item BLACK_CONCRETE_POWDER = register(new BlockItem("black_concrete_powder", builder())); - public static final Item TURTLE_EGG = register(new BlockItem("turtle_egg", builder())); - public static final Item SNIFFER_EGG = register(new BlockItem("sniffer_egg", builder())); - public static final Item DEAD_TUBE_CORAL_BLOCK = register(new BlockItem("dead_tube_coral_block", builder())); - public static final Item DEAD_BRAIN_CORAL_BLOCK = register(new BlockItem("dead_brain_coral_block", builder())); - public static final Item DEAD_BUBBLE_CORAL_BLOCK = register(new BlockItem("dead_bubble_coral_block", builder())); - public static final Item DEAD_FIRE_CORAL_BLOCK = register(new BlockItem("dead_fire_coral_block", builder())); - public static final Item DEAD_HORN_CORAL_BLOCK = register(new BlockItem("dead_horn_coral_block", builder())); - public static final Item TUBE_CORAL_BLOCK = register(new BlockItem("tube_coral_block", builder())); - public static final Item BRAIN_CORAL_BLOCK = register(new BlockItem("brain_coral_block", builder())); - public static final Item BUBBLE_CORAL_BLOCK = register(new BlockItem("bubble_coral_block", builder())); - public static final Item FIRE_CORAL_BLOCK = register(new BlockItem("fire_coral_block", builder())); - public static final Item HORN_CORAL_BLOCK = register(new BlockItem("horn_coral_block", builder())); - public static final Item TUBE_CORAL = register(new BlockItem("tube_coral", builder())); - public static final Item BRAIN_CORAL = register(new BlockItem("brain_coral", builder())); - public static final Item BUBBLE_CORAL = register(new BlockItem("bubble_coral", builder())); - public static final Item FIRE_CORAL = register(new BlockItem("fire_coral", builder())); - public static final Item HORN_CORAL = register(new BlockItem("horn_coral", builder())); - public static final Item DEAD_BRAIN_CORAL = register(new BlockItem("dead_brain_coral", builder())); - public static final Item DEAD_BUBBLE_CORAL = register(new BlockItem("dead_bubble_coral", builder())); - public static final Item DEAD_FIRE_CORAL = register(new BlockItem("dead_fire_coral", builder())); - public static final Item DEAD_HORN_CORAL = register(new BlockItem("dead_horn_coral", builder())); - public static final Item DEAD_TUBE_CORAL = register(new BlockItem("dead_tube_coral", builder())); - public static final Item TUBE_CORAL_FAN = register(new BlockItem("tube_coral_fan", builder())); - public static final Item BRAIN_CORAL_FAN = register(new BlockItem("brain_coral_fan", builder())); - public static final Item BUBBLE_CORAL_FAN = register(new BlockItem("bubble_coral_fan", builder())); - public static final Item FIRE_CORAL_FAN = register(new BlockItem("fire_coral_fan", builder())); - public static final Item HORN_CORAL_FAN = register(new BlockItem("horn_coral_fan", builder())); - public static final Item DEAD_TUBE_CORAL_FAN = register(new BlockItem("dead_tube_coral_fan", builder())); - public static final Item DEAD_BRAIN_CORAL_FAN = register(new BlockItem("dead_brain_coral_fan", builder())); - public static final Item DEAD_BUBBLE_CORAL_FAN = register(new BlockItem("dead_bubble_coral_fan", builder())); - public static final Item DEAD_FIRE_CORAL_FAN = register(new BlockItem("dead_fire_coral_fan", builder())); - public static final Item DEAD_HORN_CORAL_FAN = register(new BlockItem("dead_horn_coral_fan", builder())); - public static final Item BLUE_ICE = register(new BlockItem("blue_ice", builder())); - public static final Item CONDUIT = register(new BlockItem("conduit", builder())); - public static final Item POLISHED_GRANITE_STAIRS = register(new BlockItem("polished_granite_stairs", builder())); - public static final Item SMOOTH_RED_SANDSTONE_STAIRS = register(new BlockItem("smooth_red_sandstone_stairs", builder())); - public static final Item MOSSY_STONE_BRICK_STAIRS = register(new BlockItem("mossy_stone_brick_stairs", builder())); - public static final Item POLISHED_DIORITE_STAIRS = register(new BlockItem("polished_diorite_stairs", builder())); - public static final Item MOSSY_COBBLESTONE_STAIRS = register(new BlockItem("mossy_cobblestone_stairs", builder())); - public static final Item END_STONE_BRICK_STAIRS = register(new BlockItem("end_stone_brick_stairs", builder())); - public static final Item STONE_STAIRS = register(new BlockItem("stone_stairs", builder())); - public static final Item SMOOTH_SANDSTONE_STAIRS = register(new BlockItem("smooth_sandstone_stairs", builder())); - public static final Item SMOOTH_QUARTZ_STAIRS = register(new BlockItem("smooth_quartz_stairs", builder())); - public static final Item GRANITE_STAIRS = register(new BlockItem("granite_stairs", builder())); - public static final Item ANDESITE_STAIRS = register(new BlockItem("andesite_stairs", builder())); - public static final Item RED_NETHER_BRICK_STAIRS = register(new BlockItem("red_nether_brick_stairs", builder())); - public static final Item POLISHED_ANDESITE_STAIRS = register(new BlockItem("polished_andesite_stairs", builder())); - public static final Item DIORITE_STAIRS = register(new BlockItem("diorite_stairs", builder())); - public static final Item COBBLED_DEEPSLATE_STAIRS = register(new BlockItem("cobbled_deepslate_stairs", builder())); - public static final Item POLISHED_DEEPSLATE_STAIRS = register(new BlockItem("polished_deepslate_stairs", builder())); - public static final Item DEEPSLATE_BRICK_STAIRS = register(new BlockItem("deepslate_brick_stairs", builder())); - public static final Item DEEPSLATE_TILE_STAIRS = register(new BlockItem("deepslate_tile_stairs", builder())); - public static final Item POLISHED_GRANITE_SLAB = register(new BlockItem("polished_granite_slab", builder())); - public static final Item SMOOTH_RED_SANDSTONE_SLAB = register(new BlockItem("smooth_red_sandstone_slab", builder())); - public static final Item MOSSY_STONE_BRICK_SLAB = register(new BlockItem("mossy_stone_brick_slab", builder())); - public static final Item POLISHED_DIORITE_SLAB = register(new BlockItem("polished_diorite_slab", builder())); - public static final Item MOSSY_COBBLESTONE_SLAB = register(new BlockItem("mossy_cobblestone_slab", builder())); - public static final Item END_STONE_BRICK_SLAB = register(new BlockItem("end_stone_brick_slab", builder())); - public static final Item SMOOTH_SANDSTONE_SLAB = register(new BlockItem("smooth_sandstone_slab", builder())); - public static final Item SMOOTH_QUARTZ_SLAB = register(new BlockItem("smooth_quartz_slab", builder())); - public static final Item GRANITE_SLAB = register(new BlockItem("granite_slab", builder())); - public static final Item ANDESITE_SLAB = register(new BlockItem("andesite_slab", builder())); - public static final Item RED_NETHER_BRICK_SLAB = register(new BlockItem("red_nether_brick_slab", builder())); - public static final Item POLISHED_ANDESITE_SLAB = register(new BlockItem("polished_andesite_slab", builder())); - public static final Item DIORITE_SLAB = register(new BlockItem("diorite_slab", builder())); - public static final Item COBBLED_DEEPSLATE_SLAB = register(new BlockItem("cobbled_deepslate_slab", builder())); - public static final Item POLISHED_DEEPSLATE_SLAB = register(new BlockItem("polished_deepslate_slab", builder())); - public static final Item DEEPSLATE_BRICK_SLAB = register(new BlockItem("deepslate_brick_slab", builder())); - public static final Item DEEPSLATE_TILE_SLAB = register(new BlockItem("deepslate_tile_slab", builder())); - public static final Item SCAFFOLDING = register(new BlockItem("scaffolding", builder())); - public static final Item REDSTONE = register(new BlockItem("redstone", builder())); - public static final Item REDSTONE_TORCH = register(new BlockItem("redstone_torch", builder())); - public static final Item REDSTONE_BLOCK = register(new BlockItem("redstone_block", builder())); - public static final Item REPEATER = register(new BlockItem("repeater", builder())); - public static final Item COMPARATOR = register(new BlockItem("comparator", builder())); - public static final Item PISTON = register(new BlockItem("piston", builder())); - public static final Item STICKY_PISTON = register(new BlockItem("sticky_piston", builder())); - public static final Item SLIME_BLOCK = register(new BlockItem("slime_block", builder())); - public static final Item HONEY_BLOCK = register(new BlockItem("honey_block", builder())); - public static final Item OBSERVER = register(new BlockItem("observer", builder())); - public static final Item HOPPER = register(new BlockItem("hopper", builder())); - public static final Item DISPENSER = register(new BlockItem("dispenser", builder())); - public static final Item DROPPER = register(new BlockItem("dropper", builder())); - public static final Item LECTERN = register(new BlockItem("lectern", builder())); - public static final Item TARGET = register(new BlockItem("target", builder())); - public static final Item LEVER = register(new BlockItem("lever", builder())); - public static final Item LIGHTNING_ROD = register(new BlockItem("lightning_rod", builder())); - public static final Item DAYLIGHT_DETECTOR = register(new BlockItem("daylight_detector", builder())); - public static final Item SCULK_SENSOR = register(new BlockItem("sculk_sensor", builder())); - public static final Item CALIBRATED_SCULK_SENSOR = register(new BlockItem("calibrated_sculk_sensor", builder())); - public static final Item TRIPWIRE_HOOK = register(new BlockItem("tripwire_hook", builder())); - public static final Item TRAPPED_CHEST = register(new ChestItem("trapped_chest", builder())); - public static final Item TNT = register(new BlockItem("tnt", builder())); - public static final Item REDSTONE_LAMP = register(new BlockItem("redstone_lamp", builder())); - public static final Item NOTE_BLOCK = register(new BlockItem("note_block", builder())); - public static final Item STONE_BUTTON = register(new BlockItem("stone_button", builder())); - public static final Item POLISHED_BLACKSTONE_BUTTON = register(new BlockItem("polished_blackstone_button", builder())); - public static final Item OAK_BUTTON = register(new BlockItem("oak_button", builder())); - public static final Item SPRUCE_BUTTON = register(new BlockItem("spruce_button", builder())); - public static final Item BIRCH_BUTTON = register(new BlockItem("birch_button", builder())); - public static final Item JUNGLE_BUTTON = register(new BlockItem("jungle_button", builder())); - public static final Item ACACIA_BUTTON = register(new BlockItem("acacia_button", builder())); - public static final Item CHERRY_BUTTON = register(new BlockItem("cherry_button", builder())); - public static final Item DARK_OAK_BUTTON = register(new BlockItem("dark_oak_button", builder())); - public static final Item MANGROVE_BUTTON = register(new BlockItem("mangrove_button", builder())); - public static final Item BAMBOO_BUTTON = register(new BlockItem("bamboo_button", builder())); - public static final Item CRIMSON_BUTTON = register(new BlockItem("crimson_button", builder())); - public static final Item WARPED_BUTTON = register(new BlockItem("warped_button", builder())); - public static final Item STONE_PRESSURE_PLATE = register(new BlockItem("stone_pressure_plate", builder())); - public static final Item POLISHED_BLACKSTONE_PRESSURE_PLATE = register(new BlockItem("polished_blackstone_pressure_plate", builder())); - public static final Item LIGHT_WEIGHTED_PRESSURE_PLATE = register(new BlockItem("light_weighted_pressure_plate", builder())); - public static final Item HEAVY_WEIGHTED_PRESSURE_PLATE = register(new BlockItem("heavy_weighted_pressure_plate", builder())); - public static final Item OAK_PRESSURE_PLATE = register(new BlockItem("oak_pressure_plate", builder())); - public static final Item SPRUCE_PRESSURE_PLATE = register(new BlockItem("spruce_pressure_plate", builder())); - public static final Item BIRCH_PRESSURE_PLATE = register(new BlockItem("birch_pressure_plate", builder())); - public static final Item JUNGLE_PRESSURE_PLATE = register(new BlockItem("jungle_pressure_plate", builder())); - public static final Item ACACIA_PRESSURE_PLATE = register(new BlockItem("acacia_pressure_plate", builder())); - public static final Item CHERRY_PRESSURE_PLATE = register(new BlockItem("cherry_pressure_plate", builder())); - public static final Item DARK_OAK_PRESSURE_PLATE = register(new BlockItem("dark_oak_pressure_plate", builder())); - public static final Item MANGROVE_PRESSURE_PLATE = register(new BlockItem("mangrove_pressure_plate", builder())); - public static final Item BAMBOO_PRESSURE_PLATE = register(new BlockItem("bamboo_pressure_plate", builder())); - public static final Item CRIMSON_PRESSURE_PLATE = register(new BlockItem("crimson_pressure_plate", builder())); - public static final Item WARPED_PRESSURE_PLATE = register(new BlockItem("warped_pressure_plate", builder())); - public static final Item IRON_DOOR = register(new BlockItem("iron_door", builder())); - public static final Item OAK_DOOR = register(new BlockItem("oak_door", builder())); - public static final Item SPRUCE_DOOR = register(new BlockItem("spruce_door", builder())); - public static final Item BIRCH_DOOR = register(new BlockItem("birch_door", builder())); - public static final Item JUNGLE_DOOR = register(new BlockItem("jungle_door", builder())); - public static final Item ACACIA_DOOR = register(new BlockItem("acacia_door", builder())); - public static final Item CHERRY_DOOR = register(new BlockItem("cherry_door", builder())); - public static final Item DARK_OAK_DOOR = register(new BlockItem("dark_oak_door", builder())); - public static final Item MANGROVE_DOOR = register(new BlockItem("mangrove_door", builder())); - public static final Item BAMBOO_DOOR = register(new BlockItem("bamboo_door", builder())); - public static final Item CRIMSON_DOOR = register(new BlockItem("crimson_door", builder())); - public static final Item WARPED_DOOR = register(new BlockItem("warped_door", builder())); - public static final Item COPPER_DOOR = register(new BlockItem("copper_door", builder())); - public static final Item EXPOSED_COPPER_DOOR = register(new BlockItem("exposed_copper_door", builder())); - public static final Item WEATHERED_COPPER_DOOR = register(new BlockItem("weathered_copper_door", builder())); - public static final Item OXIDIZED_COPPER_DOOR = register(new BlockItem("oxidized_copper_door", builder())); - public static final Item WAXED_COPPER_DOOR = register(new BlockItem("waxed_copper_door", builder())); - public static final Item WAXED_EXPOSED_COPPER_DOOR = register(new BlockItem("waxed_exposed_copper_door", builder())); - public static final Item WAXED_WEATHERED_COPPER_DOOR = register(new BlockItem("waxed_weathered_copper_door", builder())); - public static final Item WAXED_OXIDIZED_COPPER_DOOR = register(new BlockItem("waxed_oxidized_copper_door", builder())); - public static final Item IRON_TRAPDOOR = register(new BlockItem("iron_trapdoor", builder())); - public static final Item OAK_TRAPDOOR = register(new BlockItem("oak_trapdoor", builder())); - public static final Item SPRUCE_TRAPDOOR = register(new BlockItem("spruce_trapdoor", builder())); - public static final Item BIRCH_TRAPDOOR = register(new BlockItem("birch_trapdoor", builder())); - public static final Item JUNGLE_TRAPDOOR = register(new BlockItem("jungle_trapdoor", builder())); - public static final Item ACACIA_TRAPDOOR = register(new BlockItem("acacia_trapdoor", builder())); - public static final Item CHERRY_TRAPDOOR = register(new BlockItem("cherry_trapdoor", builder())); - public static final Item DARK_OAK_TRAPDOOR = register(new BlockItem("dark_oak_trapdoor", builder())); - public static final Item MANGROVE_TRAPDOOR = register(new BlockItem("mangrove_trapdoor", builder())); - public static final Item BAMBOO_TRAPDOOR = register(new BlockItem("bamboo_trapdoor", builder())); - public static final Item CRIMSON_TRAPDOOR = register(new BlockItem("crimson_trapdoor", builder())); - public static final Item WARPED_TRAPDOOR = register(new BlockItem("warped_trapdoor", builder())); - public static final Item COPPER_TRAPDOOR = register(new BlockItem("copper_trapdoor", builder())); - public static final Item EXPOSED_COPPER_TRAPDOOR = register(new BlockItem("exposed_copper_trapdoor", builder())); - public static final Item WEATHERED_COPPER_TRAPDOOR = register(new BlockItem("weathered_copper_trapdoor", builder())); - public static final Item OXIDIZED_COPPER_TRAPDOOR = register(new BlockItem("oxidized_copper_trapdoor", builder())); - public static final Item WAXED_COPPER_TRAPDOOR = register(new BlockItem("waxed_copper_trapdoor", builder())); - public static final Item WAXED_EXPOSED_COPPER_TRAPDOOR = register(new BlockItem("waxed_exposed_copper_trapdoor", builder())); - public static final Item WAXED_WEATHERED_COPPER_TRAPDOOR = register(new BlockItem("waxed_weathered_copper_trapdoor", builder())); - public static final Item WAXED_OXIDIZED_COPPER_TRAPDOOR = register(new BlockItem("waxed_oxidized_copper_trapdoor", builder())); - public static final Item OAK_FENCE_GATE = register(new BlockItem("oak_fence_gate", builder())); - public static final Item SPRUCE_FENCE_GATE = register(new BlockItem("spruce_fence_gate", builder())); - public static final Item BIRCH_FENCE_GATE = register(new BlockItem("birch_fence_gate", builder())); - public static final Item JUNGLE_FENCE_GATE = register(new BlockItem("jungle_fence_gate", builder())); - public static final Item ACACIA_FENCE_GATE = register(new BlockItem("acacia_fence_gate", builder())); - public static final Item CHERRY_FENCE_GATE = register(new BlockItem("cherry_fence_gate", builder())); - public static final Item DARK_OAK_FENCE_GATE = register(new BlockItem("dark_oak_fence_gate", builder())); - public static final Item MANGROVE_FENCE_GATE = register(new BlockItem("mangrove_fence_gate", builder())); - public static final Item BAMBOO_FENCE_GATE = register(new BlockItem("bamboo_fence_gate", builder())); - public static final Item CRIMSON_FENCE_GATE = register(new BlockItem("crimson_fence_gate", builder())); - public static final Item WARPED_FENCE_GATE = register(new BlockItem("warped_fence_gate", builder())); - public static final Item POWERED_RAIL = register(new BlockItem("powered_rail", builder())); - public static final Item DETECTOR_RAIL = register(new BlockItem("detector_rail", builder())); - public static final Item RAIL = register(new BlockItem("rail", builder())); - public static final Item ACTIVATOR_RAIL = register(new BlockItem("activator_rail", builder())); - public static final Item SADDLE = register(new Item("saddle", builder().stackSize(1))); - public static final Item MINECART = register(new Item("minecart", builder().stackSize(1))); - public static final Item CHEST_MINECART = register(new Item("chest_minecart", builder().stackSize(1))); - public static final Item FURNACE_MINECART = register(new Item("furnace_minecart", builder().stackSize(1))); - public static final Item TNT_MINECART = register(new Item("tnt_minecart", builder().stackSize(1))); - public static final Item HOPPER_MINECART = register(new Item("hopper_minecart", builder().stackSize(1))); - public static final Item CARROT_ON_A_STICK = register(new Item("carrot_on_a_stick", builder().stackSize(1).maxDamage(25))); - public static final Item WARPED_FUNGUS_ON_A_STICK = register(new Item("warped_fungus_on_a_stick", builder().stackSize(1).maxDamage(100))); - public static final Item ELYTRA = register(new ElytraItem("elytra", builder().stackSize(1).maxDamage(432))); - public static final Item OAK_BOAT = register(new BoatItem("oak_boat", builder().stackSize(1))); - public static final Item OAK_CHEST_BOAT = register(new BoatItem("oak_chest_boat", builder().stackSize(1))); - public static final Item SPRUCE_BOAT = register(new BoatItem("spruce_boat", builder().stackSize(1))); - public static final Item SPRUCE_CHEST_BOAT = register(new BoatItem("spruce_chest_boat", builder().stackSize(1))); - public static final Item BIRCH_BOAT = register(new BoatItem("birch_boat", builder().stackSize(1))); - public static final Item BIRCH_CHEST_BOAT = register(new BoatItem("birch_chest_boat", builder().stackSize(1))); - public static final Item JUNGLE_BOAT = register(new BoatItem("jungle_boat", builder().stackSize(1))); - public static final Item JUNGLE_CHEST_BOAT = register(new BoatItem("jungle_chest_boat", builder().stackSize(1))); - public static final Item ACACIA_BOAT = register(new BoatItem("acacia_boat", builder().stackSize(1))); - public static final Item ACACIA_CHEST_BOAT = register(new BoatItem("acacia_chest_boat", builder().stackSize(1))); - public static final Item CHERRY_BOAT = register(new BoatItem("cherry_boat", builder().stackSize(1))); - public static final Item CHERRY_CHEST_BOAT = register(new BoatItem("cherry_chest_boat", builder().stackSize(1))); - public static final Item DARK_OAK_BOAT = register(new BoatItem("dark_oak_boat", builder().stackSize(1))); - public static final Item DARK_OAK_CHEST_BOAT = register(new BoatItem("dark_oak_chest_boat", builder().stackSize(1))); - public static final Item MANGROVE_BOAT = register(new BoatItem("mangrove_boat", builder().stackSize(1))); - public static final Item MANGROVE_CHEST_BOAT = register(new BoatItem("mangrove_chest_boat", builder().stackSize(1))); - public static final Item BAMBOO_RAFT = register(new BoatItem("bamboo_raft", builder().stackSize(1))); - public static final Item BAMBOO_CHEST_RAFT = register(new BoatItem("bamboo_chest_raft", builder().stackSize(1))); - public static final Item STRUCTURE_BLOCK = register(new BlockItem("structure_block", builder())); - public static final Item JIGSAW = register(new BlockItem("jigsaw", builder())); - public static final Item TURTLE_HELMET = register(new ArmorItem("turtle_helmet", ArmorMaterial.TURTLE, builder().stackSize(1).maxDamage(275))); - public static final Item SCUTE = register(new Item("scute", builder())); - public static final Item FLINT_AND_STEEL = register(new Item("flint_and_steel", builder().stackSize(1).maxDamage(64))); + public static final Item STONE = register(new BlockItem(builder(), Blocks.STONE)); + public static final Item GRANITE = register(new BlockItem(builder(), Blocks.GRANITE)); + public static final Item POLISHED_GRANITE = register(new BlockItem(builder(), Blocks.POLISHED_GRANITE)); + public static final Item DIORITE = register(new BlockItem(builder(), Blocks.DIORITE)); + public static final Item POLISHED_DIORITE = register(new BlockItem(builder(), Blocks.POLISHED_DIORITE)); + public static final Item ANDESITE = register(new BlockItem(builder(), Blocks.ANDESITE)); + public static final Item POLISHED_ANDESITE = register(new BlockItem(builder(), Blocks.POLISHED_ANDESITE)); + public static final Item DEEPSLATE = register(new BlockItem(builder(), Blocks.DEEPSLATE)); + public static final Item COBBLED_DEEPSLATE = register(new BlockItem(builder(), Blocks.COBBLED_DEEPSLATE)); + public static final Item POLISHED_DEEPSLATE = register(new BlockItem(builder(), Blocks.POLISHED_DEEPSLATE)); + public static final Item CALCITE = register(new BlockItem(builder(), Blocks.CALCITE)); + public static final Item TUFF = register(new BlockItem(builder(), Blocks.TUFF)); + public static final Item TUFF_SLAB = register(new BlockItem(builder(), Blocks.TUFF_SLAB)); + public static final Item TUFF_STAIRS = register(new BlockItem(builder(), Blocks.TUFF_STAIRS)); + public static final Item TUFF_WALL = register(new BlockItem(builder(), Blocks.TUFF_WALL)); + public static final Item CHISELED_TUFF = register(new BlockItem(builder(), Blocks.CHISELED_TUFF)); + public static final Item POLISHED_TUFF = register(new BlockItem(builder(), Blocks.POLISHED_TUFF)); + public static final Item POLISHED_TUFF_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_TUFF_SLAB)); + public static final Item POLISHED_TUFF_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_TUFF_STAIRS)); + public static final Item POLISHED_TUFF_WALL = register(new BlockItem(builder(), Blocks.POLISHED_TUFF_WALL)); + public static final Item TUFF_BRICKS = register(new BlockItem(builder(), Blocks.TUFF_BRICKS)); + public static final Item TUFF_BRICK_SLAB = register(new BlockItem(builder(), Blocks.TUFF_BRICK_SLAB)); + public static final Item TUFF_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.TUFF_BRICK_STAIRS)); + public static final Item TUFF_BRICK_WALL = register(new BlockItem(builder(), Blocks.TUFF_BRICK_WALL)); + public static final Item CHISELED_TUFF_BRICKS = register(new BlockItem(builder(), Blocks.CHISELED_TUFF_BRICKS)); + public static final Item DRIPSTONE_BLOCK = register(new BlockItem(builder(), Blocks.DRIPSTONE_BLOCK)); + public static final Item GRASS_BLOCK = register(new BlockItem(builder(), Blocks.GRASS_BLOCK)); + public static final Item DIRT = register(new BlockItem(builder(), Blocks.DIRT)); + public static final Item COARSE_DIRT = register(new BlockItem(builder(), Blocks.COARSE_DIRT)); + public static final Item PODZOL = register(new BlockItem(builder(), Blocks.PODZOL)); + public static final Item ROOTED_DIRT = register(new BlockItem(builder(), Blocks.ROOTED_DIRT)); + public static final Item MUD = register(new BlockItem(builder(), Blocks.MUD)); + public static final Item CRIMSON_NYLIUM = register(new BlockItem(builder(), Blocks.CRIMSON_NYLIUM)); + public static final Item WARPED_NYLIUM = register(new BlockItem(builder(), Blocks.WARPED_NYLIUM)); + public static final Item COBBLESTONE = register(new BlockItem(builder(), Blocks.COBBLESTONE)); + public static final Item OAK_PLANKS = register(new BlockItem(builder(), Blocks.OAK_PLANKS)); + public static final Item SPRUCE_PLANKS = register(new BlockItem(builder(), Blocks.SPRUCE_PLANKS)); + public static final Item BIRCH_PLANKS = register(new BlockItem(builder(), Blocks.BIRCH_PLANKS)); + public static final Item JUNGLE_PLANKS = register(new BlockItem(builder(), Blocks.JUNGLE_PLANKS)); + public static final Item ACACIA_PLANKS = register(new BlockItem(builder(), Blocks.ACACIA_PLANKS)); + public static final Item CHERRY_PLANKS = register(new BlockItem(builder(), Blocks.CHERRY_PLANKS)); + public static final Item DARK_OAK_PLANKS = register(new BlockItem(builder(), Blocks.DARK_OAK_PLANKS)); + public static final Item PALE_OAK_PLANKS = register(new BlockItem(builder(), Blocks.PALE_OAK_PLANKS)); + public static final Item MANGROVE_PLANKS = register(new BlockItem(builder(), Blocks.MANGROVE_PLANKS)); + public static final Item BAMBOO_PLANKS = register(new BlockItem(builder(), Blocks.BAMBOO_PLANKS)); + public static final Item CRIMSON_PLANKS = register(new BlockItem(builder(), Blocks.CRIMSON_PLANKS)); + public static final Item WARPED_PLANKS = register(new BlockItem(builder(), Blocks.WARPED_PLANKS)); + public static final Item BAMBOO_MOSAIC = register(new BlockItem(builder(), Blocks.BAMBOO_MOSAIC)); + public static final Item OAK_SAPLING = register(new BlockItem(builder(), Blocks.OAK_SAPLING)); + public static final Item SPRUCE_SAPLING = register(new BlockItem(builder(), Blocks.SPRUCE_SAPLING)); + public static final Item BIRCH_SAPLING = register(new BlockItem(builder(), Blocks.BIRCH_SAPLING)); + public static final Item JUNGLE_SAPLING = register(new BlockItem(builder(), Blocks.JUNGLE_SAPLING)); + public static final Item ACACIA_SAPLING = register(new BlockItem(builder(), Blocks.ACACIA_SAPLING)); + public static final Item CHERRY_SAPLING = register(new BlockItem(builder(), Blocks.CHERRY_SAPLING)); + public static final Item DARK_OAK_SAPLING = register(new BlockItem(builder(), Blocks.DARK_OAK_SAPLING)); + public static final Item PALE_OAK_SAPLING = register(new BlockItem(builder(), Blocks.PALE_OAK_SAPLING)); + public static final Item MANGROVE_PROPAGULE = register(new BlockItem(builder(), Blocks.MANGROVE_PROPAGULE)); + public static final Item BEDROCK = register(new BlockItem(builder(), Blocks.BEDROCK)); + public static final Item SAND = register(new BlockItem(builder(), Blocks.SAND)); + public static final Item SUSPICIOUS_SAND = register(new BlockItem(builder(), Blocks.SUSPICIOUS_SAND)); + public static final Item SUSPICIOUS_GRAVEL = register(new BlockItem(builder(), Blocks.SUSPICIOUS_GRAVEL)); + public static final Item RED_SAND = register(new BlockItem(builder(), Blocks.RED_SAND)); + public static final Item GRAVEL = register(new BlockItem(builder(), Blocks.GRAVEL)); + public static final Item COAL_ORE = register(new BlockItem(builder(), Blocks.COAL_ORE)); + public static final Item DEEPSLATE_COAL_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_COAL_ORE)); + public static final Item IRON_ORE = register(new BlockItem(builder(), Blocks.IRON_ORE)); + public static final Item DEEPSLATE_IRON_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_IRON_ORE)); + public static final Item COPPER_ORE = register(new BlockItem(builder(), Blocks.COPPER_ORE)); + public static final Item DEEPSLATE_COPPER_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_COPPER_ORE)); + public static final Item GOLD_ORE = register(new BlockItem(builder(), Blocks.GOLD_ORE)); + public static final Item DEEPSLATE_GOLD_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_GOLD_ORE)); + public static final Item REDSTONE_ORE = register(new BlockItem(builder(), Blocks.REDSTONE_ORE)); + public static final Item DEEPSLATE_REDSTONE_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_REDSTONE_ORE)); + public static final Item EMERALD_ORE = register(new BlockItem(builder(), Blocks.EMERALD_ORE)); + public static final Item DEEPSLATE_EMERALD_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_EMERALD_ORE)); + public static final Item LAPIS_ORE = register(new BlockItem(builder(), Blocks.LAPIS_ORE)); + public static final Item DEEPSLATE_LAPIS_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_LAPIS_ORE)); + public static final Item DIAMOND_ORE = register(new BlockItem(builder(), Blocks.DIAMOND_ORE)); + public static final Item DEEPSLATE_DIAMOND_ORE = register(new BlockItem(builder(), Blocks.DEEPSLATE_DIAMOND_ORE)); + public static final Item NETHER_GOLD_ORE = register(new BlockItem(builder(), Blocks.NETHER_GOLD_ORE)); + public static final Item NETHER_QUARTZ_ORE = register(new BlockItem(builder(), Blocks.NETHER_QUARTZ_ORE)); + public static final Item ANCIENT_DEBRIS = register(new BlockItem(builder(), Blocks.ANCIENT_DEBRIS)); + public static final Item COAL_BLOCK = register(new BlockItem(builder(), Blocks.COAL_BLOCK)); + public static final Item RAW_IRON_BLOCK = register(new BlockItem(builder(), Blocks.RAW_IRON_BLOCK)); + public static final Item RAW_COPPER_BLOCK = register(new BlockItem(builder(), Blocks.RAW_COPPER_BLOCK)); + public static final Item RAW_GOLD_BLOCK = register(new BlockItem(builder(), Blocks.RAW_GOLD_BLOCK)); + public static final Item HEAVY_CORE = register(new BlockItem(builder(), Blocks.HEAVY_CORE)); + public static final Item AMETHYST_BLOCK = register(new BlockItem(builder(), Blocks.AMETHYST_BLOCK)); + public static final Item BUDDING_AMETHYST = register(new BlockItem(builder(), Blocks.BUDDING_AMETHYST)); + public static final Item IRON_BLOCK = register(new BlockItem(builder(), Blocks.IRON_BLOCK)); + public static final Item COPPER_BLOCK = register(new BlockItem(builder(), Blocks.COPPER_BLOCK)); + public static final Item GOLD_BLOCK = register(new BlockItem(builder(), Blocks.GOLD_BLOCK)); + public static final Item DIAMOND_BLOCK = register(new BlockItem(builder(), Blocks.DIAMOND_BLOCK)); + public static final Item NETHERITE_BLOCK = register(new BlockItem(builder(), Blocks.NETHERITE_BLOCK)); + public static final Item EXPOSED_COPPER = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER)); + public static final Item WEATHERED_COPPER = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER)); + public static final Item OXIDIZED_COPPER = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER)); + public static final Item CHISELED_COPPER = register(new BlockItem(builder(), Blocks.CHISELED_COPPER)); + public static final Item EXPOSED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.EXPOSED_CHISELED_COPPER)); + public static final Item WEATHERED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.WEATHERED_CHISELED_COPPER)); + public static final Item OXIDIZED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.OXIDIZED_CHISELED_COPPER)); + public static final Item CUT_COPPER = register(new BlockItem(builder(), Blocks.CUT_COPPER)); + public static final Item EXPOSED_CUT_COPPER = register(new BlockItem(builder(), Blocks.EXPOSED_CUT_COPPER)); + public static final Item WEATHERED_CUT_COPPER = register(new BlockItem(builder(), Blocks.WEATHERED_CUT_COPPER)); + public static final Item OXIDIZED_CUT_COPPER = register(new BlockItem(builder(), Blocks.OXIDIZED_CUT_COPPER)); + public static final Item CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.CUT_COPPER_STAIRS)); + public static final Item EXPOSED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.EXPOSED_CUT_COPPER_STAIRS)); + public static final Item WEATHERED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.WEATHERED_CUT_COPPER_STAIRS)); + public static final Item OXIDIZED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.OXIDIZED_CUT_COPPER_STAIRS)); + public static final Item CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.CUT_COPPER_SLAB)); + public static final Item EXPOSED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.EXPOSED_CUT_COPPER_SLAB)); + public static final Item WEATHERED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.WEATHERED_CUT_COPPER_SLAB)); + public static final Item OXIDIZED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.OXIDIZED_CUT_COPPER_SLAB)); + public static final Item WAXED_COPPER_BLOCK = register(new BlockItem(builder(), Blocks.WAXED_COPPER_BLOCK)); + public static final Item WAXED_EXPOSED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER)); + public static final Item WAXED_WEATHERED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER)); + public static final Item WAXED_OXIDIZED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER)); + public static final Item WAXED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_CHISELED_COPPER)); + public static final Item WAXED_EXPOSED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_CHISELED_COPPER)); + public static final Item WAXED_WEATHERED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_CHISELED_COPPER)); + public static final Item WAXED_OXIDIZED_CHISELED_COPPER = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_CHISELED_COPPER)); + public static final Item WAXED_CUT_COPPER = register(new BlockItem(builder(), Blocks.WAXED_CUT_COPPER)); + public static final Item WAXED_EXPOSED_CUT_COPPER = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_CUT_COPPER)); + public static final Item WAXED_WEATHERED_CUT_COPPER = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_CUT_COPPER)); + public static final Item WAXED_OXIDIZED_CUT_COPPER = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_CUT_COPPER)); + public static final Item WAXED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.WAXED_CUT_COPPER_STAIRS)); + public static final Item WAXED_EXPOSED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_CUT_COPPER_STAIRS)); + public static final Item WAXED_WEATHERED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_CUT_COPPER_STAIRS)); + public static final Item WAXED_OXIDIZED_CUT_COPPER_STAIRS = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_CUT_COPPER_STAIRS)); + public static final Item WAXED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.WAXED_CUT_COPPER_SLAB)); + public static final Item WAXED_EXPOSED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_CUT_COPPER_SLAB)); + public static final Item WAXED_WEATHERED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_CUT_COPPER_SLAB)); + public static final Item WAXED_OXIDIZED_CUT_COPPER_SLAB = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_CUT_COPPER_SLAB)); + public static final Item OAK_LOG = register(new BlockItem(builder(), Blocks.OAK_LOG)); + public static final Item SPRUCE_LOG = register(new BlockItem(builder(), Blocks.SPRUCE_LOG)); + public static final Item BIRCH_LOG = register(new BlockItem(builder(), Blocks.BIRCH_LOG)); + public static final Item JUNGLE_LOG = register(new BlockItem(builder(), Blocks.JUNGLE_LOG)); + public static final Item ACACIA_LOG = register(new BlockItem(builder(), Blocks.ACACIA_LOG)); + public static final Item CHERRY_LOG = register(new BlockItem(builder(), Blocks.CHERRY_LOG)); + public static final Item PALE_OAK_LOG = register(new BlockItem(builder(), Blocks.PALE_OAK_LOG)); + public static final Item DARK_OAK_LOG = register(new BlockItem(builder(), Blocks.DARK_OAK_LOG)); + public static final Item MANGROVE_LOG = register(new BlockItem(builder(), Blocks.MANGROVE_LOG)); + public static final Item MANGROVE_ROOTS = register(new BlockItem(builder(), Blocks.MANGROVE_ROOTS)); + public static final Item MUDDY_MANGROVE_ROOTS = register(new BlockItem(builder(), Blocks.MUDDY_MANGROVE_ROOTS)); + public static final Item CRIMSON_STEM = register(new BlockItem(builder(), Blocks.CRIMSON_STEM)); + public static final Item WARPED_STEM = register(new BlockItem(builder(), Blocks.WARPED_STEM)); + public static final Item BAMBOO_BLOCK = register(new BlockItem(builder(), Blocks.BAMBOO_BLOCK)); + public static final Item STRIPPED_OAK_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_OAK_LOG)); + public static final Item STRIPPED_SPRUCE_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_SPRUCE_LOG)); + public static final Item STRIPPED_BIRCH_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_BIRCH_LOG)); + public static final Item STRIPPED_JUNGLE_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_JUNGLE_LOG)); + public static final Item STRIPPED_ACACIA_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_ACACIA_LOG)); + public static final Item STRIPPED_CHERRY_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_CHERRY_LOG)); + public static final Item STRIPPED_DARK_OAK_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_DARK_OAK_LOG)); + public static final Item STRIPPED_PALE_OAK_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_PALE_OAK_LOG)); + public static final Item STRIPPED_MANGROVE_LOG = register(new BlockItem(builder(), Blocks.STRIPPED_MANGROVE_LOG)); + public static final Item STRIPPED_CRIMSON_STEM = register(new BlockItem(builder(), Blocks.STRIPPED_CRIMSON_STEM)); + public static final Item STRIPPED_WARPED_STEM = register(new BlockItem(builder(), Blocks.STRIPPED_WARPED_STEM)); + public static final Item STRIPPED_OAK_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_OAK_WOOD)); + public static final Item STRIPPED_SPRUCE_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_SPRUCE_WOOD)); + public static final Item STRIPPED_BIRCH_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_BIRCH_WOOD)); + public static final Item STRIPPED_JUNGLE_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_JUNGLE_WOOD)); + public static final Item STRIPPED_ACACIA_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_ACACIA_WOOD)); + public static final Item STRIPPED_CHERRY_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_CHERRY_WOOD)); + public static final Item STRIPPED_DARK_OAK_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_DARK_OAK_WOOD)); + public static final Item STRIPPED_PALE_OAK_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_PALE_OAK_WOOD)); + public static final Item STRIPPED_MANGROVE_WOOD = register(new BlockItem(builder(), Blocks.STRIPPED_MANGROVE_WOOD)); + public static final Item STRIPPED_CRIMSON_HYPHAE = register(new BlockItem(builder(), Blocks.STRIPPED_CRIMSON_HYPHAE)); + public static final Item STRIPPED_WARPED_HYPHAE = register(new BlockItem(builder(), Blocks.STRIPPED_WARPED_HYPHAE)); + public static final Item STRIPPED_BAMBOO_BLOCK = register(new BlockItem(builder(), Blocks.STRIPPED_BAMBOO_BLOCK)); + public static final Item OAK_WOOD = register(new BlockItem(builder(), Blocks.OAK_WOOD)); + public static final Item SPRUCE_WOOD = register(new BlockItem(builder(), Blocks.SPRUCE_WOOD)); + public static final Item BIRCH_WOOD = register(new BlockItem(builder(), Blocks.BIRCH_WOOD)); + public static final Item JUNGLE_WOOD = register(new BlockItem(builder(), Blocks.JUNGLE_WOOD)); + public static final Item ACACIA_WOOD = register(new BlockItem(builder(), Blocks.ACACIA_WOOD)); + public static final Item CHERRY_WOOD = register(new BlockItem(builder(), Blocks.CHERRY_WOOD)); + public static final Item PALE_OAK_WOOD = register(new BlockItem(builder(), Blocks.PALE_OAK_WOOD)); + public static final Item DARK_OAK_WOOD = register(new BlockItem(builder(), Blocks.DARK_OAK_WOOD)); + public static final Item MANGROVE_WOOD = register(new BlockItem(builder(), Blocks.MANGROVE_WOOD)); + public static final Item CRIMSON_HYPHAE = register(new BlockItem(builder(), Blocks.CRIMSON_HYPHAE)); + public static final Item WARPED_HYPHAE = register(new BlockItem(builder(), Blocks.WARPED_HYPHAE)); + public static final Item OAK_LEAVES = register(new BlockItem(builder(), Blocks.OAK_LEAVES)); + public static final Item SPRUCE_LEAVES = register(new BlockItem(builder(), Blocks.SPRUCE_LEAVES)); + public static final Item BIRCH_LEAVES = register(new BlockItem(builder(), Blocks.BIRCH_LEAVES)); + public static final Item JUNGLE_LEAVES = register(new BlockItem(builder(), Blocks.JUNGLE_LEAVES)); + public static final Item ACACIA_LEAVES = register(new BlockItem(builder(), Blocks.ACACIA_LEAVES)); + public static final Item CHERRY_LEAVES = register(new BlockItem(builder(), Blocks.CHERRY_LEAVES)); + public static final Item DARK_OAK_LEAVES = register(new BlockItem(builder(), Blocks.DARK_OAK_LEAVES)); + public static final Item PALE_OAK_LEAVES = register(new BlockItem(builder(), Blocks.PALE_OAK_LEAVES)); + public static final Item MANGROVE_LEAVES = register(new BlockItem(builder(), Blocks.MANGROVE_LEAVES)); + public static final Item AZALEA_LEAVES = register(new BlockItem(builder(), Blocks.AZALEA_LEAVES)); + public static final Item FLOWERING_AZALEA_LEAVES = register(new BlockItem(builder(), Blocks.FLOWERING_AZALEA_LEAVES)); + public static final Item SPONGE = register(new BlockItem(builder(), Blocks.SPONGE)); + public static final Item WET_SPONGE = register(new BlockItem(builder(), Blocks.WET_SPONGE)); + public static final Item GLASS = register(new BlockItem(builder(), Blocks.GLASS)); + public static final Item TINTED_GLASS = register(new BlockItem(builder(), Blocks.TINTED_GLASS)); + public static final Item LAPIS_BLOCK = register(new BlockItem(builder(), Blocks.LAPIS_BLOCK)); + public static final Item SANDSTONE = register(new BlockItem(builder(), Blocks.SANDSTONE)); + public static final Item CHISELED_SANDSTONE = register(new BlockItem(builder(), Blocks.CHISELED_SANDSTONE)); + public static final Item CUT_SANDSTONE = register(new BlockItem(builder(), Blocks.CUT_SANDSTONE)); + public static final Item COBWEB = register(new BlockItem(builder(), Blocks.COBWEB)); + public static final Item SHORT_GRASS = register(new BlockItem(builder(), Blocks.SHORT_GRASS)); + public static final Item FERN = register(new BlockItem(builder(), Blocks.FERN)); + public static final Item BUSH = register(new BlockItem(builder(), Blocks.BUSH)); + public static final Item AZALEA = register(new BlockItem(builder(), Blocks.AZALEA)); + public static final Item FLOWERING_AZALEA = register(new BlockItem(builder(), Blocks.FLOWERING_AZALEA)); + public static final Item DEAD_BUSH = register(new BlockItem(builder(), Blocks.DEAD_BUSH)); + public static final Item FIREFLY_BUSH = register(new BlockItem(builder(), Blocks.FIREFLY_BUSH)); + public static final Item SHORT_DRY_GRASS = register(new BlockItem(builder(), Blocks.SHORT_DRY_GRASS)); + public static final Item TALL_DRY_GRASS = register(new BlockItem(builder(), Blocks.TALL_DRY_GRASS)); + public static final Item SEAGRASS = register(new BlockItem(builder(), Blocks.SEAGRASS)); + public static final Item SEA_PICKLE = register(new BlockItem(builder(), Blocks.SEA_PICKLE)); + public static final Item WHITE_WOOL = register(new BlockItem(builder(), Blocks.WHITE_WOOL)); + public static final Item ORANGE_WOOL = register(new BlockItem(builder(), Blocks.ORANGE_WOOL)); + public static final Item MAGENTA_WOOL = register(new BlockItem(builder(), Blocks.MAGENTA_WOOL)); + public static final Item LIGHT_BLUE_WOOL = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_WOOL)); + public static final Item YELLOW_WOOL = register(new BlockItem(builder(), Blocks.YELLOW_WOOL)); + public static final Item LIME_WOOL = register(new BlockItem(builder(), Blocks.LIME_WOOL)); + public static final Item PINK_WOOL = register(new BlockItem(builder(), Blocks.PINK_WOOL)); + public static final Item GRAY_WOOL = register(new BlockItem(builder(), Blocks.GRAY_WOOL)); + public static final Item LIGHT_GRAY_WOOL = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_WOOL)); + public static final Item CYAN_WOOL = register(new BlockItem(builder(), Blocks.CYAN_WOOL)); + public static final Item PURPLE_WOOL = register(new BlockItem(builder(), Blocks.PURPLE_WOOL)); + public static final Item BLUE_WOOL = register(new BlockItem(builder(), Blocks.BLUE_WOOL)); + public static final Item BROWN_WOOL = register(new BlockItem(builder(), Blocks.BROWN_WOOL)); + public static final Item GREEN_WOOL = register(new BlockItem(builder(), Blocks.GREEN_WOOL)); + public static final Item RED_WOOL = register(new BlockItem(builder(), Blocks.RED_WOOL)); + public static final Item BLACK_WOOL = register(new BlockItem(builder(), Blocks.BLACK_WOOL)); + public static final Item DANDELION = register(new BlockItem(builder(), Blocks.DANDELION)); + public static final Item OPEN_EYEBLOSSOM = register(new BlockItem(builder(), Blocks.OPEN_EYEBLOSSOM)); + public static final Item CLOSED_EYEBLOSSOM = register(new BlockItem(builder(), Blocks.CLOSED_EYEBLOSSOM)); + public static final Item POPPY = register(new BlockItem(builder(), Blocks.POPPY)); + public static final Item BLUE_ORCHID = register(new BlockItem(builder(), Blocks.BLUE_ORCHID)); + public static final Item ALLIUM = register(new BlockItem(builder(), Blocks.ALLIUM)); + public static final Item AZURE_BLUET = register(new BlockItem(builder(), Blocks.AZURE_BLUET)); + public static final Item RED_TULIP = register(new BlockItem(builder(), Blocks.RED_TULIP)); + public static final Item ORANGE_TULIP = register(new BlockItem(builder(), Blocks.ORANGE_TULIP)); + public static final Item WHITE_TULIP = register(new BlockItem(builder(), Blocks.WHITE_TULIP)); + public static final Item PINK_TULIP = register(new BlockItem(builder(), Blocks.PINK_TULIP)); + public static final Item OXEYE_DAISY = register(new BlockItem(builder(), Blocks.OXEYE_DAISY)); + public static final Item CORNFLOWER = register(new BlockItem(builder(), Blocks.CORNFLOWER)); + public static final Item LILY_OF_THE_VALLEY = register(new BlockItem(builder(), Blocks.LILY_OF_THE_VALLEY)); + public static final Item WITHER_ROSE = register(new BlockItem(builder(), Blocks.WITHER_ROSE)); + public static final Item TORCHFLOWER = register(new BlockItem(builder(), Blocks.TORCHFLOWER)); + public static final Item PITCHER_PLANT = register(new BlockItem(builder(), Blocks.PITCHER_PLANT)); + public static final Item SPORE_BLOSSOM = register(new BlockItem(builder(), Blocks.SPORE_BLOSSOM)); + public static final Item BROWN_MUSHROOM = register(new BlockItem(builder(), Blocks.BROWN_MUSHROOM)); + public static final Item RED_MUSHROOM = register(new BlockItem(builder(), Blocks.RED_MUSHROOM)); + public static final Item CRIMSON_FUNGUS = register(new BlockItem(builder(), Blocks.CRIMSON_FUNGUS)); + public static final Item WARPED_FUNGUS = register(new BlockItem(builder(), Blocks.WARPED_FUNGUS)); + public static final Item CRIMSON_ROOTS = register(new BlockItem(builder(), Blocks.CRIMSON_ROOTS)); + public static final Item WARPED_ROOTS = register(new BlockItem(builder(), Blocks.WARPED_ROOTS)); + public static final Item NETHER_SPROUTS = register(new BlockItem(builder(), Blocks.NETHER_SPROUTS)); + public static final Item WEEPING_VINES = register(new BlockItem(builder(), Blocks.WEEPING_VINES)); + public static final Item TWISTING_VINES = register(new BlockItem(builder(), Blocks.TWISTING_VINES)); + public static final Item SUGAR_CANE = register(new BlockItem(builder(), Blocks.SUGAR_CANE)); + public static final Item KELP = register(new BlockItem(builder(), Blocks.KELP)); + public static final Item PINK_PETALS = register(new BlockItem(builder(), Blocks.PINK_PETALS)); + public static final Item WILDFLOWERS = register(new BlockItem(builder(), Blocks.WILDFLOWERS)); + public static final Item LEAF_LITTER = register(new BlockItem(builder(), Blocks.LEAF_LITTER)); + public static final Item MOSS_CARPET = register(new BlockItem(builder(), Blocks.MOSS_CARPET)); + public static final Item MOSS_BLOCK = register(new BlockItem(builder(), Blocks.MOSS_BLOCK)); + public static final Item PALE_MOSS_CARPET = register(new BlockItem(builder(), Blocks.PALE_MOSS_CARPET)); + public static final Item PALE_HANGING_MOSS = register(new BlockItem(builder(), Blocks.PALE_HANGING_MOSS)); + public static final Item PALE_MOSS_BLOCK = register(new BlockItem(builder(), Blocks.PALE_MOSS_BLOCK)); + public static final Item HANGING_ROOTS = register(new BlockItem(builder(), Blocks.HANGING_ROOTS)); + public static final Item BIG_DRIPLEAF = register(new BlockItem(builder(), Blocks.BIG_DRIPLEAF, Blocks.BIG_DRIPLEAF_STEM)); + public static final Item SMALL_DRIPLEAF = register(new BlockItem(builder(), Blocks.SMALL_DRIPLEAF)); + public static final Item BAMBOO = register(new BlockItem(builder(), Blocks.BAMBOO)); + public static final Item OAK_SLAB = register(new BlockItem(builder(), Blocks.OAK_SLAB)); + public static final Item SPRUCE_SLAB = register(new BlockItem(builder(), Blocks.SPRUCE_SLAB)); + public static final Item BIRCH_SLAB = register(new BlockItem(builder(), Blocks.BIRCH_SLAB)); + public static final Item JUNGLE_SLAB = register(new BlockItem(builder(), Blocks.JUNGLE_SLAB)); + public static final Item ACACIA_SLAB = register(new BlockItem(builder(), Blocks.ACACIA_SLAB)); + public static final Item CHERRY_SLAB = register(new BlockItem(builder(), Blocks.CHERRY_SLAB)); + public static final Item DARK_OAK_SLAB = register(new BlockItem(builder(), Blocks.DARK_OAK_SLAB)); + public static final Item PALE_OAK_SLAB = register(new BlockItem(builder(), Blocks.PALE_OAK_SLAB)); + public static final Item MANGROVE_SLAB = register(new BlockItem(builder(), Blocks.MANGROVE_SLAB)); + public static final Item BAMBOO_SLAB = register(new BlockItem(builder(), Blocks.BAMBOO_SLAB)); + public static final Item BAMBOO_MOSAIC_SLAB = register(new BlockItem(builder(), Blocks.BAMBOO_MOSAIC_SLAB)); + public static final Item CRIMSON_SLAB = register(new BlockItem(builder(), Blocks.CRIMSON_SLAB)); + public static final Item WARPED_SLAB = register(new BlockItem(builder(), Blocks.WARPED_SLAB)); + public static final Item STONE_SLAB = register(new BlockItem(builder(), Blocks.STONE_SLAB)); + public static final Item SMOOTH_STONE_SLAB = register(new BlockItem(builder(), Blocks.SMOOTH_STONE_SLAB)); + public static final Item SANDSTONE_SLAB = register(new BlockItem(builder(), Blocks.SANDSTONE_SLAB)); + public static final Item CUT_SANDSTONE_SLAB = register(new BlockItem(builder(), Blocks.CUT_SANDSTONE_SLAB)); + public static final Item PETRIFIED_OAK_SLAB = register(new BlockItem(builder(), Blocks.PETRIFIED_OAK_SLAB)); + public static final Item COBBLESTONE_SLAB = register(new BlockItem(builder(), Blocks.COBBLESTONE_SLAB)); + public static final Item BRICK_SLAB = register(new BlockItem(builder(), Blocks.BRICK_SLAB)); + public static final Item STONE_BRICK_SLAB = register(new BlockItem(builder(), Blocks.STONE_BRICK_SLAB)); + public static final Item MUD_BRICK_SLAB = register(new BlockItem(builder(), Blocks.MUD_BRICK_SLAB)); + public static final Item NETHER_BRICK_SLAB = register(new BlockItem(builder(), Blocks.NETHER_BRICK_SLAB)); + public static final Item QUARTZ_SLAB = register(new BlockItem(builder(), Blocks.QUARTZ_SLAB)); + public static final Item RED_SANDSTONE_SLAB = register(new BlockItem(builder(), Blocks.RED_SANDSTONE_SLAB)); + public static final Item CUT_RED_SANDSTONE_SLAB = register(new BlockItem(builder(), Blocks.CUT_RED_SANDSTONE_SLAB)); + public static final Item PURPUR_SLAB = register(new BlockItem(builder(), Blocks.PURPUR_SLAB)); + public static final Item PRISMARINE_SLAB = register(new BlockItem(builder(), Blocks.PRISMARINE_SLAB)); + public static final Item PRISMARINE_BRICK_SLAB = register(new BlockItem(builder(), Blocks.PRISMARINE_BRICK_SLAB)); + public static final Item DARK_PRISMARINE_SLAB = register(new BlockItem(builder(), Blocks.DARK_PRISMARINE_SLAB)); + public static final Item SMOOTH_QUARTZ = register(new BlockItem(builder(), Blocks.SMOOTH_QUARTZ)); + public static final Item SMOOTH_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.SMOOTH_RED_SANDSTONE)); + public static final Item SMOOTH_SANDSTONE = register(new BlockItem(builder(), Blocks.SMOOTH_SANDSTONE)); + public static final Item SMOOTH_STONE = register(new BlockItem(builder(), Blocks.SMOOTH_STONE)); + public static final Item BRICKS = register(new BlockItem(builder(), Blocks.BRICKS)); + public static final Item ACACIA_SHELF = register(new BlockItem(builder(), Blocks.ACACIA_SHELF)); + public static final Item BAMBOO_SHELF = register(new BlockItem(builder(), Blocks.BAMBOO_SHELF)); + public static final Item BIRCH_SHELF = register(new BlockItem(builder(), Blocks.BIRCH_SHELF)); + public static final Item CHERRY_SHELF = register(new BlockItem(builder(), Blocks.CHERRY_SHELF)); + public static final Item CRIMSON_SHELF = register(new BlockItem(builder(), Blocks.CRIMSON_SHELF)); + public static final Item DARK_OAK_SHELF = register(new BlockItem(builder(), Blocks.DARK_OAK_SHELF)); + public static final Item JUNGLE_SHELF = register(new BlockItem(builder(), Blocks.JUNGLE_SHELF)); + public static final Item MANGROVE_SHELF = register(new BlockItem(builder(), Blocks.MANGROVE_SHELF)); + public static final Item OAK_SHELF = register(new BlockItem(builder(), Blocks.OAK_SHELF)); + public static final Item PALE_OAK_SHELF = register(new BlockItem(builder(), Blocks.PALE_OAK_SHELF)); + public static final Item SPRUCE_SHELF = register(new BlockItem(builder(), Blocks.SPRUCE_SHELF)); + public static final Item WARPED_SHELF = register(new BlockItem(builder(), Blocks.WARPED_SHELF)); + public static final Item BOOKSHELF = register(new BlockItem(builder(), Blocks.BOOKSHELF)); + public static final Item CHISELED_BOOKSHELF = register(new BlockItem(builder(), Blocks.CHISELED_BOOKSHELF)); + public static final Item DECORATED_POT = register(new DecoratedPotItem(builder(), Blocks.DECORATED_POT)); + public static final Item MOSSY_COBBLESTONE = register(new BlockItem(builder(), Blocks.MOSSY_COBBLESTONE)); + public static final Item OBSIDIAN = register(new BlockItem(builder(), Blocks.OBSIDIAN)); + public static final Item TORCH = register(new BlockItem(builder(), Blocks.TORCH, Blocks.WALL_TORCH)); + public static final Item END_ROD = register(new BlockItem(builder(), Blocks.END_ROD)); + public static final Item CHORUS_PLANT = register(new BlockItem(builder(), Blocks.CHORUS_PLANT)); + public static final Item CHORUS_FLOWER = register(new BlockItem(builder(), Blocks.CHORUS_FLOWER)); + public static final Item PURPUR_BLOCK = register(new BlockItem(builder(), Blocks.PURPUR_BLOCK)); + public static final Item PURPUR_PILLAR = register(new BlockItem(builder(), Blocks.PURPUR_PILLAR)); + public static final Item PURPUR_STAIRS = register(new BlockItem(builder(), Blocks.PURPUR_STAIRS)); + public static final Item SPAWNER = register(new BlockItem(builder(), Blocks.SPAWNER)); + public static final Item CREAKING_HEART = register(new BlockItem(builder(), Blocks.CREAKING_HEART)); + public static final Item CHEST = register(new BlockItem(builder(), Blocks.CHEST)); + public static final Item CRAFTING_TABLE = register(new BlockItem(builder(), Blocks.CRAFTING_TABLE)); + public static final Item FARMLAND = register(new BlockItem(builder(), Blocks.FARMLAND)); + public static final Item FURNACE = register(new BlockItem(builder(), Blocks.FURNACE)); + public static final Item LADDER = register(new BlockItem(builder(), Blocks.LADDER)); + public static final Item COBBLESTONE_STAIRS = register(new BlockItem(builder(), Blocks.COBBLESTONE_STAIRS)); + public static final Item SNOW = register(new BlockItem(builder(), Blocks.SNOW)); + public static final Item ICE = register(new BlockItem(builder(), Blocks.ICE)); + public static final Item SNOW_BLOCK = register(new BlockItem(builder(), Blocks.SNOW_BLOCK)); + public static final Item CACTUS = register(new BlockItem(builder(), Blocks.CACTUS)); + public static final Item CACTUS_FLOWER = register(new BlockItem(builder(), Blocks.CACTUS_FLOWER)); + public static final Item CLAY = register(new BlockItem(builder(), Blocks.CLAY)); + public static final Item JUKEBOX = register(new BlockItem(builder(), Blocks.JUKEBOX)); + public static final Item OAK_FENCE = register(new BlockItem(builder(), Blocks.OAK_FENCE)); + public static final Item SPRUCE_FENCE = register(new BlockItem(builder(), Blocks.SPRUCE_FENCE)); + public static final Item BIRCH_FENCE = register(new BlockItem(builder(), Blocks.BIRCH_FENCE)); + public static final Item JUNGLE_FENCE = register(new BlockItem(builder(), Blocks.JUNGLE_FENCE)); + public static final Item ACACIA_FENCE = register(new BlockItem(builder(), Blocks.ACACIA_FENCE)); + public static final Item CHERRY_FENCE = register(new BlockItem(builder(), Blocks.CHERRY_FENCE)); + public static final Item DARK_OAK_FENCE = register(new BlockItem(builder(), Blocks.DARK_OAK_FENCE)); + public static final Item PALE_OAK_FENCE = register(new BlockItem(builder(), Blocks.PALE_OAK_FENCE)); + public static final Item MANGROVE_FENCE = register(new BlockItem(builder(), Blocks.MANGROVE_FENCE)); + public static final Item BAMBOO_FENCE = register(new BlockItem(builder(), Blocks.BAMBOO_FENCE)); + public static final Item CRIMSON_FENCE = register(new BlockItem(builder(), Blocks.CRIMSON_FENCE)); + public static final Item WARPED_FENCE = register(new BlockItem(builder(), Blocks.WARPED_FENCE)); + public static final Item PUMPKIN = register(new BlockItem(builder(), Blocks.PUMPKIN)); + public static final Item CARVED_PUMPKIN = register(new BlockItem(builder(), Blocks.CARVED_PUMPKIN)); + public static final Item JACK_O_LANTERN = register(new BlockItem(builder(), Blocks.JACK_O_LANTERN)); + public static final Item NETHERRACK = register(new BlockItem(builder(), Blocks.NETHERRACK)); + public static final Item SOUL_SAND = register(new BlockItem(builder(), Blocks.SOUL_SAND)); + public static final Item SOUL_SOIL = register(new BlockItem(builder(), Blocks.SOUL_SOIL)); + public static final Item BASALT = register(new BlockItem(builder(), Blocks.BASALT)); + public static final Item POLISHED_BASALT = register(new BlockItem(builder(), Blocks.POLISHED_BASALT)); + public static final Item SMOOTH_BASALT = register(new BlockItem(builder(), Blocks.SMOOTH_BASALT)); + public static final Item SOUL_TORCH = register(new BlockItem(builder(), Blocks.SOUL_TORCH, Blocks.SOUL_WALL_TORCH)); + public static final Item COPPER_TORCH = register(new BlockItem(builder(), Blocks.COPPER_TORCH, Blocks.COPPER_WALL_TORCH)); + public static final Item GLOWSTONE = register(new BlockItem(builder(), Blocks.GLOWSTONE)); + public static final Item INFESTED_STONE = register(new BlockItem(builder(), Blocks.INFESTED_STONE)); + public static final Item INFESTED_COBBLESTONE = register(new BlockItem(builder(), Blocks.INFESTED_COBBLESTONE)); + public static final Item INFESTED_STONE_BRICKS = register(new BlockItem(builder(), Blocks.INFESTED_STONE_BRICKS)); + public static final Item INFESTED_MOSSY_STONE_BRICKS = register(new BlockItem(builder(), Blocks.INFESTED_MOSSY_STONE_BRICKS)); + public static final Item INFESTED_CRACKED_STONE_BRICKS = register(new BlockItem(builder(), Blocks.INFESTED_CRACKED_STONE_BRICKS)); + public static final Item INFESTED_CHISELED_STONE_BRICKS = register(new BlockItem(builder(), Blocks.INFESTED_CHISELED_STONE_BRICKS)); + public static final Item INFESTED_DEEPSLATE = register(new BlockItem(builder(), Blocks.INFESTED_DEEPSLATE)); + public static final Item STONE_BRICKS = register(new BlockItem(builder(), Blocks.STONE_BRICKS)); + public static final Item MOSSY_STONE_BRICKS = register(new BlockItem(builder(), Blocks.MOSSY_STONE_BRICKS)); + public static final Item CRACKED_STONE_BRICKS = register(new BlockItem(builder(), Blocks.CRACKED_STONE_BRICKS)); + public static final Item CHISELED_STONE_BRICKS = register(new BlockItem(builder(), Blocks.CHISELED_STONE_BRICKS)); + public static final Item PACKED_MUD = register(new BlockItem(builder(), Blocks.PACKED_MUD)); + public static final Item MUD_BRICKS = register(new BlockItem(builder(), Blocks.MUD_BRICKS)); + public static final Item DEEPSLATE_BRICKS = register(new BlockItem(builder(), Blocks.DEEPSLATE_BRICKS)); + public static final Item CRACKED_DEEPSLATE_BRICKS = register(new BlockItem(builder(), Blocks.CRACKED_DEEPSLATE_BRICKS)); + public static final Item DEEPSLATE_TILES = register(new BlockItem(builder(), Blocks.DEEPSLATE_TILES)); + public static final Item CRACKED_DEEPSLATE_TILES = register(new BlockItem(builder(), Blocks.CRACKED_DEEPSLATE_TILES)); + public static final Item CHISELED_DEEPSLATE = register(new BlockItem(builder(), Blocks.CHISELED_DEEPSLATE)); + public static final Item REINFORCED_DEEPSLATE = register(new BlockItem(builder(), Blocks.REINFORCED_DEEPSLATE)); + public static final Item BROWN_MUSHROOM_BLOCK = register(new BlockItem(builder(), Blocks.BROWN_MUSHROOM_BLOCK)); + public static final Item RED_MUSHROOM_BLOCK = register(new BlockItem(builder(), Blocks.RED_MUSHROOM_BLOCK)); + public static final Item MUSHROOM_STEM = register(new BlockItem(builder(), Blocks.MUSHROOM_STEM)); + public static final Item IRON_BARS = register(new BlockItem(builder(), Blocks.IRON_BARS)); + public static final Item COPPER_BARS = register(new BlockItem(builder(), Blocks.COPPER_BARS)); + public static final Item EXPOSED_COPPER_BARS = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_BARS)); + public static final Item WEATHERED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_BARS)); + public static final Item OXIDIZED_COPPER_BARS = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_BARS)); + public static final Item WAXED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_COPPER_BARS)); + public static final Item WAXED_EXPOSED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_BARS)); + public static final Item WAXED_WEATHERED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_BARS)); + public static final Item WAXED_OXIDIZED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_BARS)); + public static final Item IRON_CHAIN = register(new BlockItem(builder(), Blocks.IRON_CHAIN)); + public static final Item COPPER_CHAIN = register(new BlockItem(builder(), Blocks.COPPER_CHAIN)); + public static final Item EXPOSED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_CHAIN)); + public static final Item WEATHERED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_CHAIN)); + public static final Item OXIDIZED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_CHAIN)); + public static final Item WAXED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_COPPER_CHAIN)); + public static final Item WAXED_EXPOSED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_CHAIN)); + public static final Item WAXED_WEATHERED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_CHAIN)); + public static final Item WAXED_OXIDIZED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_CHAIN)); + public static final Item GLASS_PANE = register(new BlockItem(builder(), Blocks.GLASS_PANE)); + public static final Item MELON = register(new BlockItem(builder(), Blocks.MELON)); + public static final Item VINE = register(new BlockItem(builder(), Blocks.VINE)); + public static final Item GLOW_LICHEN = register(new BlockItem(builder(), Blocks.GLOW_LICHEN)); + public static final Item RESIN_CLUMP = register(new BlockItem(builder(), Blocks.RESIN_CLUMP)); + public static final Item RESIN_BLOCK = register(new BlockItem(builder(), Blocks.RESIN_BLOCK)); + public static final Item RESIN_BRICKS = register(new BlockItem(builder(), Blocks.RESIN_BRICKS)); + public static final Item RESIN_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.RESIN_BRICK_STAIRS)); + public static final Item RESIN_BRICK_SLAB = register(new BlockItem(builder(), Blocks.RESIN_BRICK_SLAB)); + public static final Item RESIN_BRICK_WALL = register(new BlockItem(builder(), Blocks.RESIN_BRICK_WALL)); + public static final Item CHISELED_RESIN_BRICKS = register(new BlockItem(builder(), Blocks.CHISELED_RESIN_BRICKS)); + public static final Item BRICK_STAIRS = register(new BlockItem(builder(), Blocks.BRICK_STAIRS)); + public static final Item STONE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.STONE_BRICK_STAIRS)); + public static final Item MUD_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.MUD_BRICK_STAIRS)); + public static final Item MYCELIUM = register(new BlockItem(builder(), Blocks.MYCELIUM)); + public static final Item LILY_PAD = register(new BlockItem(builder(), Blocks.LILY_PAD)); + public static final Item NETHER_BRICKS = register(new BlockItem(builder(), Blocks.NETHER_BRICKS)); + public static final Item CRACKED_NETHER_BRICKS = register(new BlockItem(builder(), Blocks.CRACKED_NETHER_BRICKS)); + public static final Item CHISELED_NETHER_BRICKS = register(new BlockItem(builder(), Blocks.CHISELED_NETHER_BRICKS)); + public static final Item NETHER_BRICK_FENCE = register(new BlockItem(builder(), Blocks.NETHER_BRICK_FENCE)); + public static final Item NETHER_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.NETHER_BRICK_STAIRS)); + public static final Item SCULK = register(new BlockItem(builder(), Blocks.SCULK)); + public static final Item SCULK_VEIN = register(new BlockItem(builder(), Blocks.SCULK_VEIN)); + public static final Item SCULK_CATALYST = register(new BlockItem(builder(), Blocks.SCULK_CATALYST)); + public static final Item SCULK_SHRIEKER = register(new BlockItem(builder(), Blocks.SCULK_SHRIEKER)); + public static final Item ENCHANTING_TABLE = register(new BlockItem(builder(), Blocks.ENCHANTING_TABLE)); + public static final Item END_PORTAL_FRAME = register(new BlockItem(builder(), Blocks.END_PORTAL_FRAME)); + public static final Item END_STONE = register(new BlockItem(builder(), Blocks.END_STONE)); + public static final Item END_STONE_BRICKS = register(new BlockItem(builder(), Blocks.END_STONE_BRICKS)); + public static final Item DRAGON_EGG = register(new BlockItem(builder(), Blocks.DRAGON_EGG)); + public static final Item SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SANDSTONE_STAIRS)); + public static final Item ENDER_CHEST = register(new BlockItem(builder(), Blocks.ENDER_CHEST)); + public static final Item EMERALD_BLOCK = register(new BlockItem(builder(), Blocks.EMERALD_BLOCK)); + public static final Item OAK_STAIRS = register(new BlockItem(builder(), Blocks.OAK_STAIRS)); + public static final Item SPRUCE_STAIRS = register(new BlockItem(builder(), Blocks.SPRUCE_STAIRS)); + public static final Item BIRCH_STAIRS = register(new BlockItem(builder(), Blocks.BIRCH_STAIRS)); + public static final Item JUNGLE_STAIRS = register(new BlockItem(builder(), Blocks.JUNGLE_STAIRS)); + public static final Item ACACIA_STAIRS = register(new BlockItem(builder(), Blocks.ACACIA_STAIRS)); + public static final Item CHERRY_STAIRS = register(new BlockItem(builder(), Blocks.CHERRY_STAIRS)); + public static final Item DARK_OAK_STAIRS = register(new BlockItem(builder(), Blocks.DARK_OAK_STAIRS)); + public static final Item PALE_OAK_STAIRS = register(new BlockItem(builder(), Blocks.PALE_OAK_STAIRS)); + public static final Item MANGROVE_STAIRS = register(new BlockItem(builder(), Blocks.MANGROVE_STAIRS)); + public static final Item BAMBOO_STAIRS = register(new BlockItem(builder(), Blocks.BAMBOO_STAIRS)); + public static final Item BAMBOO_MOSAIC_STAIRS = register(new BlockItem(builder(), Blocks.BAMBOO_MOSAIC_STAIRS)); + public static final Item CRIMSON_STAIRS = register(new BlockItem(builder(), Blocks.CRIMSON_STAIRS)); + public static final Item WARPED_STAIRS = register(new BlockItem(builder(), Blocks.WARPED_STAIRS)); + public static final Item COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.COMMAND_BLOCK)); + public static final Item BEACON = register(new BlockItem(builder(), Blocks.BEACON)); + public static final Item COBBLESTONE_WALL = register(new BlockItem(builder(), Blocks.COBBLESTONE_WALL)); + public static final Item MOSSY_COBBLESTONE_WALL = register(new BlockItem(builder(), Blocks.MOSSY_COBBLESTONE_WALL)); + public static final Item BRICK_WALL = register(new BlockItem(builder(), Blocks.BRICK_WALL)); + public static final Item PRISMARINE_WALL = register(new BlockItem(builder(), Blocks.PRISMARINE_WALL)); + public static final Item RED_SANDSTONE_WALL = register(new BlockItem(builder(), Blocks.RED_SANDSTONE_WALL)); + public static final Item MOSSY_STONE_BRICK_WALL = register(new BlockItem(builder(), Blocks.MOSSY_STONE_BRICK_WALL)); + public static final Item GRANITE_WALL = register(new BlockItem(builder(), Blocks.GRANITE_WALL)); + public static final Item STONE_BRICK_WALL = register(new BlockItem(builder(), Blocks.STONE_BRICK_WALL)); + public static final Item MUD_BRICK_WALL = register(new BlockItem(builder(), Blocks.MUD_BRICK_WALL)); + public static final Item NETHER_BRICK_WALL = register(new BlockItem(builder(), Blocks.NETHER_BRICK_WALL)); + public static final Item ANDESITE_WALL = register(new BlockItem(builder(), Blocks.ANDESITE_WALL)); + public static final Item RED_NETHER_BRICK_WALL = register(new BlockItem(builder(), Blocks.RED_NETHER_BRICK_WALL)); + public static final Item SANDSTONE_WALL = register(new BlockItem(builder(), Blocks.SANDSTONE_WALL)); + public static final Item END_STONE_BRICK_WALL = register(new BlockItem(builder(), Blocks.END_STONE_BRICK_WALL)); + public static final Item DIORITE_WALL = register(new BlockItem(builder(), Blocks.DIORITE_WALL)); + public static final Item BLACKSTONE_WALL = register(new BlockItem(builder(), Blocks.BLACKSTONE_WALL)); + public static final Item POLISHED_BLACKSTONE_WALL = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_WALL)); + public static final Item POLISHED_BLACKSTONE_BRICK_WALL = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_BRICK_WALL)); + public static final Item COBBLED_DEEPSLATE_WALL = register(new BlockItem(builder(), Blocks.COBBLED_DEEPSLATE_WALL)); + public static final Item POLISHED_DEEPSLATE_WALL = register(new BlockItem(builder(), Blocks.POLISHED_DEEPSLATE_WALL)); + public static final Item DEEPSLATE_BRICK_WALL = register(new BlockItem(builder(), Blocks.DEEPSLATE_BRICK_WALL)); + public static final Item DEEPSLATE_TILE_WALL = register(new BlockItem(builder(), Blocks.DEEPSLATE_TILE_WALL)); + public static final Item ANVIL = register(new BlockItem(builder(), Blocks.ANVIL)); + public static final Item CHIPPED_ANVIL = register(new BlockItem(builder(), Blocks.CHIPPED_ANVIL)); + public static final Item DAMAGED_ANVIL = register(new BlockItem(builder(), Blocks.DAMAGED_ANVIL)); + public static final Item CHISELED_QUARTZ_BLOCK = register(new BlockItem(builder(), Blocks.CHISELED_QUARTZ_BLOCK)); + public static final Item QUARTZ_BLOCK = register(new BlockItem(builder(), Blocks.QUARTZ_BLOCK)); + public static final Item QUARTZ_BRICKS = register(new BlockItem(builder(), Blocks.QUARTZ_BRICKS)); + public static final Item QUARTZ_PILLAR = register(new BlockItem(builder(), Blocks.QUARTZ_PILLAR)); + public static final Item QUARTZ_STAIRS = register(new BlockItem(builder(), Blocks.QUARTZ_STAIRS)); + public static final Item WHITE_TERRACOTTA = register(new BlockItem(builder(), Blocks.WHITE_TERRACOTTA)); + public static final Item ORANGE_TERRACOTTA = register(new BlockItem(builder(), Blocks.ORANGE_TERRACOTTA)); + public static final Item MAGENTA_TERRACOTTA = register(new BlockItem(builder(), Blocks.MAGENTA_TERRACOTTA)); + public static final Item LIGHT_BLUE_TERRACOTTA = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_TERRACOTTA)); + public static final Item YELLOW_TERRACOTTA = register(new BlockItem(builder(), Blocks.YELLOW_TERRACOTTA)); + public static final Item LIME_TERRACOTTA = register(new BlockItem(builder(), Blocks.LIME_TERRACOTTA)); + public static final Item PINK_TERRACOTTA = register(new BlockItem(builder(), Blocks.PINK_TERRACOTTA)); + public static final Item GRAY_TERRACOTTA = register(new BlockItem(builder(), Blocks.GRAY_TERRACOTTA)); + public static final Item LIGHT_GRAY_TERRACOTTA = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_TERRACOTTA)); + public static final Item CYAN_TERRACOTTA = register(new BlockItem(builder(), Blocks.CYAN_TERRACOTTA)); + public static final Item PURPLE_TERRACOTTA = register(new BlockItem(builder(), Blocks.PURPLE_TERRACOTTA)); + public static final Item BLUE_TERRACOTTA = register(new BlockItem(builder(), Blocks.BLUE_TERRACOTTA)); + public static final Item BROWN_TERRACOTTA = register(new BlockItem(builder(), Blocks.BROWN_TERRACOTTA)); + public static final Item GREEN_TERRACOTTA = register(new BlockItem(builder(), Blocks.GREEN_TERRACOTTA)); + public static final Item RED_TERRACOTTA = register(new BlockItem(builder(), Blocks.RED_TERRACOTTA)); + public static final Item BLACK_TERRACOTTA = register(new BlockItem(builder(), Blocks.BLACK_TERRACOTTA)); + public static final Item BARRIER = register(new BlockItem(builder(), Blocks.BARRIER)); + public static final Item LIGHT = register(new LightItem(builder(), Blocks.LIGHT)); + public static final Item HAY_BLOCK = register(new BlockItem(builder(), Blocks.HAY_BLOCK)); + public static final Item WHITE_CARPET = register(new BlockItem(builder(), Blocks.WHITE_CARPET)); + public static final Item ORANGE_CARPET = register(new BlockItem(builder(), Blocks.ORANGE_CARPET)); + public static final Item MAGENTA_CARPET = register(new BlockItem(builder(), Blocks.MAGENTA_CARPET)); + public static final Item LIGHT_BLUE_CARPET = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_CARPET)); + public static final Item YELLOW_CARPET = register(new BlockItem(builder(), Blocks.YELLOW_CARPET)); + public static final Item LIME_CARPET = register(new BlockItem(builder(), Blocks.LIME_CARPET)); + public static final Item PINK_CARPET = register(new BlockItem(builder(), Blocks.PINK_CARPET)); + public static final Item GRAY_CARPET = register(new BlockItem(builder(), Blocks.GRAY_CARPET)); + public static final Item LIGHT_GRAY_CARPET = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_CARPET)); + public static final Item CYAN_CARPET = register(new BlockItem(builder(), Blocks.CYAN_CARPET)); + public static final Item PURPLE_CARPET = register(new BlockItem(builder(), Blocks.PURPLE_CARPET)); + public static final Item BLUE_CARPET = register(new BlockItem(builder(), Blocks.BLUE_CARPET)); + public static final Item BROWN_CARPET = register(new BlockItem(builder(), Blocks.BROWN_CARPET)); + public static final Item GREEN_CARPET = register(new BlockItem(builder(), Blocks.GREEN_CARPET)); + public static final Item RED_CARPET = register(new BlockItem(builder(), Blocks.RED_CARPET)); + public static final Item BLACK_CARPET = register(new BlockItem(builder(), Blocks.BLACK_CARPET)); + public static final Item TERRACOTTA = register(new BlockItem(builder(), Blocks.TERRACOTTA)); + public static final Item PACKED_ICE = register(new BlockItem(builder(), Blocks.PACKED_ICE)); + public static final Item DIRT_PATH = register(new BlockItem(builder(), Blocks.DIRT_PATH)); + public static final Item SUNFLOWER = register(new BlockItem(builder(), Blocks.SUNFLOWER)); + public static final Item LILAC = register(new BlockItem(builder(), Blocks.LILAC)); + public static final Item ROSE_BUSH = register(new BlockItem(builder(), Blocks.ROSE_BUSH)); + public static final Item PEONY = register(new BlockItem(builder(), Blocks.PEONY)); + public static final Item TALL_GRASS = register(new BlockItem(builder(), Blocks.TALL_GRASS)); + public static final Item LARGE_FERN = register(new BlockItem(builder(), Blocks.LARGE_FERN)); + public static final Item WHITE_STAINED_GLASS = register(new BlockItem(builder(), Blocks.WHITE_STAINED_GLASS)); + public static final Item ORANGE_STAINED_GLASS = register(new BlockItem(builder(), Blocks.ORANGE_STAINED_GLASS)); + public static final Item MAGENTA_STAINED_GLASS = register(new BlockItem(builder(), Blocks.MAGENTA_STAINED_GLASS)); + public static final Item LIGHT_BLUE_STAINED_GLASS = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_STAINED_GLASS)); + public static final Item YELLOW_STAINED_GLASS = register(new BlockItem(builder(), Blocks.YELLOW_STAINED_GLASS)); + public static final Item LIME_STAINED_GLASS = register(new BlockItem(builder(), Blocks.LIME_STAINED_GLASS)); + public static final Item PINK_STAINED_GLASS = register(new BlockItem(builder(), Blocks.PINK_STAINED_GLASS)); + public static final Item GRAY_STAINED_GLASS = register(new BlockItem(builder(), Blocks.GRAY_STAINED_GLASS)); + public static final Item LIGHT_GRAY_STAINED_GLASS = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_STAINED_GLASS)); + public static final Item CYAN_STAINED_GLASS = register(new BlockItem(builder(), Blocks.CYAN_STAINED_GLASS)); + public static final Item PURPLE_STAINED_GLASS = register(new BlockItem(builder(), Blocks.PURPLE_STAINED_GLASS)); + public static final Item BLUE_STAINED_GLASS = register(new BlockItem(builder(), Blocks.BLUE_STAINED_GLASS)); + public static final Item BROWN_STAINED_GLASS = register(new BlockItem(builder(), Blocks.BROWN_STAINED_GLASS)); + public static final Item GREEN_STAINED_GLASS = register(new BlockItem(builder(), Blocks.GREEN_STAINED_GLASS)); + public static final Item RED_STAINED_GLASS = register(new BlockItem(builder(), Blocks.RED_STAINED_GLASS)); + public static final Item BLACK_STAINED_GLASS = register(new BlockItem(builder(), Blocks.BLACK_STAINED_GLASS)); + public static final Item WHITE_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.WHITE_STAINED_GLASS_PANE)); + public static final Item ORANGE_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.ORANGE_STAINED_GLASS_PANE)); + public static final Item MAGENTA_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.MAGENTA_STAINED_GLASS_PANE)); + public static final Item LIGHT_BLUE_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_STAINED_GLASS_PANE)); + public static final Item YELLOW_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.YELLOW_STAINED_GLASS_PANE)); + public static final Item LIME_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.LIME_STAINED_GLASS_PANE)); + public static final Item PINK_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.PINK_STAINED_GLASS_PANE)); + public static final Item GRAY_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.GRAY_STAINED_GLASS_PANE)); + public static final Item LIGHT_GRAY_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_STAINED_GLASS_PANE)); + public static final Item CYAN_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.CYAN_STAINED_GLASS_PANE)); + public static final Item PURPLE_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.PURPLE_STAINED_GLASS_PANE)); + public static final Item BLUE_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.BLUE_STAINED_GLASS_PANE)); + public static final Item BROWN_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.BROWN_STAINED_GLASS_PANE)); + public static final Item GREEN_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.GREEN_STAINED_GLASS_PANE)); + public static final Item RED_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.RED_STAINED_GLASS_PANE)); + public static final Item BLACK_STAINED_GLASS_PANE = register(new BlockItem(builder(), Blocks.BLACK_STAINED_GLASS_PANE)); + public static final Item PRISMARINE = register(new BlockItem(builder(), Blocks.PRISMARINE)); + public static final Item PRISMARINE_BRICKS = register(new BlockItem(builder(), Blocks.PRISMARINE_BRICKS)); + public static final Item DARK_PRISMARINE = register(new BlockItem(builder(), Blocks.DARK_PRISMARINE)); + public static final Item PRISMARINE_STAIRS = register(new BlockItem(builder(), Blocks.PRISMARINE_STAIRS)); + public static final Item PRISMARINE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.PRISMARINE_BRICK_STAIRS)); + public static final Item DARK_PRISMARINE_STAIRS = register(new BlockItem(builder(), Blocks.DARK_PRISMARINE_STAIRS)); + public static final Item SEA_LANTERN = register(new BlockItem(builder(), Blocks.SEA_LANTERN)); + public static final Item RED_SANDSTONE = register(new BlockItem(builder(), Blocks.RED_SANDSTONE)); + public static final Item CHISELED_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.CHISELED_RED_SANDSTONE)); + public static final Item CUT_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.CUT_RED_SANDSTONE)); + public static final Item RED_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.RED_SANDSTONE_STAIRS)); + public static final Item REPEATING_COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.REPEATING_COMMAND_BLOCK)); + public static final Item CHAIN_COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.CHAIN_COMMAND_BLOCK)); + public static final Item MAGMA_BLOCK = register(new BlockItem(builder(), Blocks.MAGMA_BLOCK)); + public static final Item NETHER_WART_BLOCK = register(new BlockItem(builder(), Blocks.NETHER_WART_BLOCK)); + public static final Item WARPED_WART_BLOCK = register(new BlockItem(builder(), Blocks.WARPED_WART_BLOCK)); + public static final Item RED_NETHER_BRICKS = register(new BlockItem(builder(), Blocks.RED_NETHER_BRICKS)); + public static final Item BONE_BLOCK = register(new BlockItem(builder(), Blocks.BONE_BLOCK)); + public static final Item STRUCTURE_VOID = register(new BlockItem(builder(), Blocks.STRUCTURE_VOID)); + public static final Item SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.SHULKER_BOX)); + public static final Item WHITE_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.WHITE_SHULKER_BOX)); + public static final Item ORANGE_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.ORANGE_SHULKER_BOX)); + public static final Item MAGENTA_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.MAGENTA_SHULKER_BOX)); + public static final Item LIGHT_BLUE_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.LIGHT_BLUE_SHULKER_BOX)); + public static final Item YELLOW_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.YELLOW_SHULKER_BOX)); + public static final Item LIME_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.LIME_SHULKER_BOX)); + public static final Item PINK_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.PINK_SHULKER_BOX)); + public static final Item GRAY_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.GRAY_SHULKER_BOX)); + public static final Item LIGHT_GRAY_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.LIGHT_GRAY_SHULKER_BOX)); + public static final Item CYAN_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.CYAN_SHULKER_BOX)); + public static final Item PURPLE_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.PURPLE_SHULKER_BOX)); + public static final Item BLUE_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.BLUE_SHULKER_BOX)); + public static final Item BROWN_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.BROWN_SHULKER_BOX)); + public static final Item GREEN_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.GREEN_SHULKER_BOX)); + public static final Item RED_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.RED_SHULKER_BOX)); + public static final Item BLACK_SHULKER_BOX = register(new ShulkerBoxItem(builder(), Blocks.BLACK_SHULKER_BOX)); + public static final Item WHITE_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.WHITE_GLAZED_TERRACOTTA)); + public static final Item ORANGE_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.ORANGE_GLAZED_TERRACOTTA)); + public static final Item MAGENTA_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.MAGENTA_GLAZED_TERRACOTTA)); + public static final Item LIGHT_BLUE_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_GLAZED_TERRACOTTA)); + public static final Item YELLOW_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.YELLOW_GLAZED_TERRACOTTA)); + public static final Item LIME_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.LIME_GLAZED_TERRACOTTA)); + public static final Item PINK_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.PINK_GLAZED_TERRACOTTA)); + public static final Item GRAY_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.GRAY_GLAZED_TERRACOTTA)); + public static final Item LIGHT_GRAY_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_GLAZED_TERRACOTTA)); + public static final Item CYAN_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.CYAN_GLAZED_TERRACOTTA)); + public static final Item PURPLE_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.PURPLE_GLAZED_TERRACOTTA)); + public static final Item BLUE_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.BLUE_GLAZED_TERRACOTTA)); + public static final Item BROWN_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.BROWN_GLAZED_TERRACOTTA)); + public static final Item GREEN_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.GREEN_GLAZED_TERRACOTTA)); + public static final Item RED_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.RED_GLAZED_TERRACOTTA)); + public static final Item BLACK_GLAZED_TERRACOTTA = register(new BlockItem(builder(), Blocks.BLACK_GLAZED_TERRACOTTA)); + public static final Item WHITE_CONCRETE = register(new BlockItem(builder(), Blocks.WHITE_CONCRETE)); + public static final Item ORANGE_CONCRETE = register(new BlockItem(builder(), Blocks.ORANGE_CONCRETE)); + public static final Item MAGENTA_CONCRETE = register(new BlockItem(builder(), Blocks.MAGENTA_CONCRETE)); + public static final Item LIGHT_BLUE_CONCRETE = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_CONCRETE)); + public static final Item YELLOW_CONCRETE = register(new BlockItem(builder(), Blocks.YELLOW_CONCRETE)); + public static final Item LIME_CONCRETE = register(new BlockItem(builder(), Blocks.LIME_CONCRETE)); + public static final Item PINK_CONCRETE = register(new BlockItem(builder(), Blocks.PINK_CONCRETE)); + public static final Item GRAY_CONCRETE = register(new BlockItem(builder(), Blocks.GRAY_CONCRETE)); + public static final Item LIGHT_GRAY_CONCRETE = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_CONCRETE)); + public static final Item CYAN_CONCRETE = register(new BlockItem(builder(), Blocks.CYAN_CONCRETE)); + public static final Item PURPLE_CONCRETE = register(new BlockItem(builder(), Blocks.PURPLE_CONCRETE)); + public static final Item BLUE_CONCRETE = register(new BlockItem(builder(), Blocks.BLUE_CONCRETE)); + public static final Item BROWN_CONCRETE = register(new BlockItem(builder(), Blocks.BROWN_CONCRETE)); + public static final Item GREEN_CONCRETE = register(new BlockItem(builder(), Blocks.GREEN_CONCRETE)); + public static final Item RED_CONCRETE = register(new BlockItem(builder(), Blocks.RED_CONCRETE)); + public static final Item BLACK_CONCRETE = register(new BlockItem(builder(), Blocks.BLACK_CONCRETE)); + public static final Item WHITE_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.WHITE_CONCRETE_POWDER)); + public static final Item ORANGE_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.ORANGE_CONCRETE_POWDER)); + public static final Item MAGENTA_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.MAGENTA_CONCRETE_POWDER)); + public static final Item LIGHT_BLUE_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_CONCRETE_POWDER)); + public static final Item YELLOW_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.YELLOW_CONCRETE_POWDER)); + public static final Item LIME_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.LIME_CONCRETE_POWDER)); + public static final Item PINK_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.PINK_CONCRETE_POWDER)); + public static final Item GRAY_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.GRAY_CONCRETE_POWDER)); + public static final Item LIGHT_GRAY_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_CONCRETE_POWDER)); + public static final Item CYAN_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.CYAN_CONCRETE_POWDER)); + public static final Item PURPLE_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.PURPLE_CONCRETE_POWDER)); + public static final Item BLUE_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.BLUE_CONCRETE_POWDER)); + public static final Item BROWN_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.BROWN_CONCRETE_POWDER)); + public static final Item GREEN_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.GREEN_CONCRETE_POWDER)); + public static final Item RED_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.RED_CONCRETE_POWDER)); + public static final Item BLACK_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.BLACK_CONCRETE_POWDER)); + public static final Item TURTLE_EGG = register(new BlockItem(builder(), Blocks.TURTLE_EGG)); + public static final Item SNIFFER_EGG = register(new BlockItem(builder(), Blocks.SNIFFER_EGG)); + public static final Item DRIED_GHAST = register(new BlockItem(builder(), Blocks.DRIED_GHAST)); + public static final Item DEAD_TUBE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_TUBE_CORAL_BLOCK)); + public static final Item DEAD_BRAIN_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_BRAIN_CORAL_BLOCK)); + public static final Item DEAD_BUBBLE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_BUBBLE_CORAL_BLOCK)); + public static final Item DEAD_FIRE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_FIRE_CORAL_BLOCK)); + public static final Item DEAD_HORN_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_HORN_CORAL_BLOCK)); + public static final Item TUBE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.TUBE_CORAL_BLOCK)); + public static final Item BRAIN_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.BRAIN_CORAL_BLOCK)); + public static final Item BUBBLE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.BUBBLE_CORAL_BLOCK)); + public static final Item FIRE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.FIRE_CORAL_BLOCK)); + public static final Item HORN_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.HORN_CORAL_BLOCK)); + public static final Item TUBE_CORAL = register(new BlockItem(builder(), Blocks.TUBE_CORAL)); + public static final Item BRAIN_CORAL = register(new BlockItem(builder(), Blocks.BRAIN_CORAL)); + public static final Item BUBBLE_CORAL = register(new BlockItem(builder(), Blocks.BUBBLE_CORAL)); + public static final Item FIRE_CORAL = register(new BlockItem(builder(), Blocks.FIRE_CORAL)); + public static final Item HORN_CORAL = register(new BlockItem(builder(), Blocks.HORN_CORAL)); + public static final Item DEAD_BRAIN_CORAL = register(new BlockItem(builder(), Blocks.DEAD_BRAIN_CORAL)); + public static final Item DEAD_BUBBLE_CORAL = register(new BlockItem(builder(), Blocks.DEAD_BUBBLE_CORAL)); + public static final Item DEAD_FIRE_CORAL = register(new BlockItem(builder(), Blocks.DEAD_FIRE_CORAL)); + public static final Item DEAD_HORN_CORAL = register(new BlockItem(builder(), Blocks.DEAD_HORN_CORAL)); + public static final Item DEAD_TUBE_CORAL = register(new BlockItem(builder(), Blocks.DEAD_TUBE_CORAL)); + public static final Item TUBE_CORAL_FAN = register(new BlockItem(builder(), Blocks.TUBE_CORAL_FAN, Blocks.TUBE_CORAL_WALL_FAN)); + public static final Item BRAIN_CORAL_FAN = register(new BlockItem(builder(), Blocks.BRAIN_CORAL_FAN, Blocks.BRAIN_CORAL_WALL_FAN)); + public static final Item BUBBLE_CORAL_FAN = register(new BlockItem(builder(), Blocks.BUBBLE_CORAL_FAN, Blocks.BUBBLE_CORAL_WALL_FAN)); + public static final Item FIRE_CORAL_FAN = register(new BlockItem(builder(), Blocks.FIRE_CORAL_FAN, Blocks.FIRE_CORAL_WALL_FAN)); + public static final Item HORN_CORAL_FAN = register(new BlockItem(builder(), Blocks.HORN_CORAL_FAN, Blocks.HORN_CORAL_WALL_FAN)); + public static final Item DEAD_TUBE_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_TUBE_CORAL_FAN, Blocks.DEAD_TUBE_CORAL_WALL_FAN)); + public static final Item DEAD_BRAIN_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_BRAIN_CORAL_FAN, Blocks.DEAD_BRAIN_CORAL_WALL_FAN)); + public static final Item DEAD_BUBBLE_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_BUBBLE_CORAL_FAN, Blocks.DEAD_BUBBLE_CORAL_WALL_FAN)); + public static final Item DEAD_FIRE_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_FIRE_CORAL_FAN, Blocks.DEAD_FIRE_CORAL_WALL_FAN)); + public static final Item DEAD_HORN_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_HORN_CORAL_FAN, Blocks.DEAD_HORN_CORAL_WALL_FAN)); + public static final Item BLUE_ICE = register(new BlockItem(builder(), Blocks.BLUE_ICE)); + public static final Item CONDUIT = register(new BlockItem(builder(), Blocks.CONDUIT)); + public static final Item POLISHED_GRANITE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_GRANITE_STAIRS)); + public static final Item SMOOTH_RED_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SMOOTH_RED_SANDSTONE_STAIRS)); + public static final Item MOSSY_STONE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.MOSSY_STONE_BRICK_STAIRS)); + public static final Item POLISHED_DIORITE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_DIORITE_STAIRS)); + public static final Item MOSSY_COBBLESTONE_STAIRS = register(new BlockItem(builder(), Blocks.MOSSY_COBBLESTONE_STAIRS)); + public static final Item END_STONE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.END_STONE_BRICK_STAIRS)); + public static final Item STONE_STAIRS = register(new BlockItem(builder(), Blocks.STONE_STAIRS)); + public static final Item SMOOTH_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SMOOTH_SANDSTONE_STAIRS)); + public static final Item SMOOTH_QUARTZ_STAIRS = register(new BlockItem(builder(), Blocks.SMOOTH_QUARTZ_STAIRS)); + public static final Item GRANITE_STAIRS = register(new BlockItem(builder(), Blocks.GRANITE_STAIRS)); + public static final Item ANDESITE_STAIRS = register(new BlockItem(builder(), Blocks.ANDESITE_STAIRS)); + public static final Item RED_NETHER_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.RED_NETHER_BRICK_STAIRS)); + public static final Item POLISHED_ANDESITE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_ANDESITE_STAIRS)); + public static final Item DIORITE_STAIRS = register(new BlockItem(builder(), Blocks.DIORITE_STAIRS)); + public static final Item COBBLED_DEEPSLATE_STAIRS = register(new BlockItem(builder(), Blocks.COBBLED_DEEPSLATE_STAIRS)); + public static final Item POLISHED_DEEPSLATE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_DEEPSLATE_STAIRS)); + public static final Item DEEPSLATE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.DEEPSLATE_BRICK_STAIRS)); + public static final Item DEEPSLATE_TILE_STAIRS = register(new BlockItem(builder(), Blocks.DEEPSLATE_TILE_STAIRS)); + public static final Item POLISHED_GRANITE_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_GRANITE_SLAB)); + public static final Item SMOOTH_RED_SANDSTONE_SLAB = register(new BlockItem(builder(), Blocks.SMOOTH_RED_SANDSTONE_SLAB)); + public static final Item MOSSY_STONE_BRICK_SLAB = register(new BlockItem(builder(), Blocks.MOSSY_STONE_BRICK_SLAB)); + public static final Item POLISHED_DIORITE_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_DIORITE_SLAB)); + public static final Item MOSSY_COBBLESTONE_SLAB = register(new BlockItem(builder(), Blocks.MOSSY_COBBLESTONE_SLAB)); + public static final Item END_STONE_BRICK_SLAB = register(new BlockItem(builder(), Blocks.END_STONE_BRICK_SLAB)); + public static final Item SMOOTH_SANDSTONE_SLAB = register(new BlockItem(builder(), Blocks.SMOOTH_SANDSTONE_SLAB)); + public static final Item SMOOTH_QUARTZ_SLAB = register(new BlockItem(builder(), Blocks.SMOOTH_QUARTZ_SLAB)); + public static final Item GRANITE_SLAB = register(new BlockItem(builder(), Blocks.GRANITE_SLAB)); + public static final Item ANDESITE_SLAB = register(new BlockItem(builder(), Blocks.ANDESITE_SLAB)); + public static final Item RED_NETHER_BRICK_SLAB = register(new BlockItem(builder(), Blocks.RED_NETHER_BRICK_SLAB)); + public static final Item POLISHED_ANDESITE_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_ANDESITE_SLAB)); + public static final Item DIORITE_SLAB = register(new BlockItem(builder(), Blocks.DIORITE_SLAB)); + public static final Item COBBLED_DEEPSLATE_SLAB = register(new BlockItem(builder(), Blocks.COBBLED_DEEPSLATE_SLAB)); + public static final Item POLISHED_DEEPSLATE_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_DEEPSLATE_SLAB)); + public static final Item DEEPSLATE_BRICK_SLAB = register(new BlockItem(builder(), Blocks.DEEPSLATE_BRICK_SLAB)); + public static final Item DEEPSLATE_TILE_SLAB = register(new BlockItem(builder(), Blocks.DEEPSLATE_TILE_SLAB)); + public static final Item SCAFFOLDING = register(new BlockItem(builder(), Blocks.SCAFFOLDING)); + public static final Item REDSTONE = register(new BlockItem("redstone", builder(), Blocks.REDSTONE_WIRE)); + public static final Item REDSTONE_TORCH = register(new BlockItem(builder(), Blocks.REDSTONE_TORCH, Blocks.REDSTONE_WALL_TORCH)); + public static final Item REDSTONE_BLOCK = register(new BlockItem(builder(), Blocks.REDSTONE_BLOCK)); + public static final Item REPEATER = register(new BlockItem(builder(), Blocks.REPEATER)); + public static final Item COMPARATOR = register(new BlockItem(builder(), Blocks.COMPARATOR)); + public static final Item PISTON = register(new BlockItem(builder(), Blocks.PISTON)); + public static final Item STICKY_PISTON = register(new BlockItem(builder(), Blocks.STICKY_PISTON)); + public static final Item SLIME_BLOCK = register(new BlockItem(builder(), Blocks.SLIME_BLOCK)); + public static final Item HONEY_BLOCK = register(new BlockItem(builder(), Blocks.HONEY_BLOCK)); + public static final Item OBSERVER = register(new BlockItem(builder(), Blocks.OBSERVER)); + public static final Item HOPPER = register(new BlockItem(builder(), Blocks.HOPPER)); + public static final Item DISPENSER = register(new BlockItem(builder(), Blocks.DISPENSER)); + public static final Item DROPPER = register(new BlockItem(builder(), Blocks.DROPPER)); + public static final Item LECTERN = register(new BlockItem(builder(), Blocks.LECTERN)); + public static final Item TARGET = register(new BlockItem(builder(), Blocks.TARGET)); + public static final Item LEVER = register(new BlockItem(builder(), Blocks.LEVER)); + public static final Item LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.LIGHTNING_ROD)); + public static final Item EXPOSED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.EXPOSED_LIGHTNING_ROD)); + public static final Item WEATHERED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WEATHERED_LIGHTNING_ROD)); + public static final Item OXIDIZED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.OXIDIZED_LIGHTNING_ROD)); + public static final Item WAXED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_LIGHTNING_ROD)); + public static final Item WAXED_EXPOSED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_LIGHTNING_ROD)); + public static final Item WAXED_WEATHERED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_LIGHTNING_ROD)); + public static final Item WAXED_OXIDIZED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_LIGHTNING_ROD)); + public static final Item DAYLIGHT_DETECTOR = register(new BlockItem(builder(), Blocks.DAYLIGHT_DETECTOR)); + public static final Item SCULK_SENSOR = register(new BlockItem(builder(), Blocks.SCULK_SENSOR)); + public static final Item CALIBRATED_SCULK_SENSOR = register(new BlockItem(builder(), Blocks.CALIBRATED_SCULK_SENSOR)); + public static final Item TRIPWIRE_HOOK = register(new BlockItem(builder(), Blocks.TRIPWIRE_HOOK)); + public static final Item TRAPPED_CHEST = register(new BlockItem(builder(), Blocks.TRAPPED_CHEST)); + public static final Item TNT = register(new BlockItem(builder(), Blocks.TNT)); + public static final Item REDSTONE_LAMP = register(new BlockItem(builder(), Blocks.REDSTONE_LAMP)); + public static final Item NOTE_BLOCK = register(new BlockItem(builder(), Blocks.NOTE_BLOCK)); + public static final Item STONE_BUTTON = register(new BlockItem(builder(), Blocks.STONE_BUTTON)); + public static final Item POLISHED_BLACKSTONE_BUTTON = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_BUTTON)); + public static final Item OAK_BUTTON = register(new BlockItem(builder(), Blocks.OAK_BUTTON)); + public static final Item SPRUCE_BUTTON = register(new BlockItem(builder(), Blocks.SPRUCE_BUTTON)); + public static final Item BIRCH_BUTTON = register(new BlockItem(builder(), Blocks.BIRCH_BUTTON)); + public static final Item JUNGLE_BUTTON = register(new BlockItem(builder(), Blocks.JUNGLE_BUTTON)); + public static final Item ACACIA_BUTTON = register(new BlockItem(builder(), Blocks.ACACIA_BUTTON)); + public static final Item CHERRY_BUTTON = register(new BlockItem(builder(), Blocks.CHERRY_BUTTON)); + public static final Item DARK_OAK_BUTTON = register(new BlockItem(builder(), Blocks.DARK_OAK_BUTTON)); + public static final Item PALE_OAK_BUTTON = register(new BlockItem(builder(), Blocks.PALE_OAK_BUTTON)); + public static final Item MANGROVE_BUTTON = register(new BlockItem(builder(), Blocks.MANGROVE_BUTTON)); + public static final Item BAMBOO_BUTTON = register(new BlockItem(builder(), Blocks.BAMBOO_BUTTON)); + public static final Item CRIMSON_BUTTON = register(new BlockItem(builder(), Blocks.CRIMSON_BUTTON)); + public static final Item WARPED_BUTTON = register(new BlockItem(builder(), Blocks.WARPED_BUTTON)); + public static final Item STONE_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.STONE_PRESSURE_PLATE)); + public static final Item POLISHED_BLACKSTONE_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_PRESSURE_PLATE)); + public static final Item LIGHT_WEIGHTED_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.LIGHT_WEIGHTED_PRESSURE_PLATE)); + public static final Item HEAVY_WEIGHTED_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.HEAVY_WEIGHTED_PRESSURE_PLATE)); + public static final Item OAK_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.OAK_PRESSURE_PLATE)); + public static final Item SPRUCE_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.SPRUCE_PRESSURE_PLATE)); + public static final Item BIRCH_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.BIRCH_PRESSURE_PLATE)); + public static final Item JUNGLE_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.JUNGLE_PRESSURE_PLATE)); + public static final Item ACACIA_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.ACACIA_PRESSURE_PLATE)); + public static final Item CHERRY_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.CHERRY_PRESSURE_PLATE)); + public static final Item DARK_OAK_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.DARK_OAK_PRESSURE_PLATE)); + public static final Item PALE_OAK_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.PALE_OAK_PRESSURE_PLATE)); + public static final Item MANGROVE_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.MANGROVE_PRESSURE_PLATE)); + public static final Item BAMBOO_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.BAMBOO_PRESSURE_PLATE)); + public static final Item CRIMSON_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.CRIMSON_PRESSURE_PLATE)); + public static final Item WARPED_PRESSURE_PLATE = register(new BlockItem(builder(), Blocks.WARPED_PRESSURE_PLATE)); + public static final Item IRON_DOOR = register(new BlockItem(builder(), Blocks.IRON_DOOR)); + public static final Item OAK_DOOR = register(new BlockItem(builder(), Blocks.OAK_DOOR)); + public static final Item SPRUCE_DOOR = register(new BlockItem(builder(), Blocks.SPRUCE_DOOR)); + public static final Item BIRCH_DOOR = register(new BlockItem(builder(), Blocks.BIRCH_DOOR)); + public static final Item JUNGLE_DOOR = register(new BlockItem(builder(), Blocks.JUNGLE_DOOR)); + public static final Item ACACIA_DOOR = register(new BlockItem(builder(), Blocks.ACACIA_DOOR)); + public static final Item CHERRY_DOOR = register(new BlockItem(builder(), Blocks.CHERRY_DOOR)); + public static final Item DARK_OAK_DOOR = register(new BlockItem(builder(), Blocks.DARK_OAK_DOOR)); + public static final Item PALE_OAK_DOOR = register(new BlockItem(builder(), Blocks.PALE_OAK_DOOR)); + public static final Item MANGROVE_DOOR = register(new BlockItem(builder(), Blocks.MANGROVE_DOOR)); + public static final Item BAMBOO_DOOR = register(new BlockItem(builder(), Blocks.BAMBOO_DOOR)); + public static final Item CRIMSON_DOOR = register(new BlockItem(builder(), Blocks.CRIMSON_DOOR)); + public static final Item WARPED_DOOR = register(new BlockItem(builder(), Blocks.WARPED_DOOR)); + public static final Item COPPER_DOOR = register(new BlockItem(builder(), Blocks.COPPER_DOOR)); + public static final Item EXPOSED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_DOOR)); + public static final Item WEATHERED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_DOOR)); + public static final Item OXIDIZED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_DOOR)); + public static final Item WAXED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.WAXED_COPPER_DOOR)); + public static final Item WAXED_EXPOSED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_DOOR)); + public static final Item WAXED_WEATHERED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_DOOR)); + public static final Item WAXED_OXIDIZED_COPPER_DOOR = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_DOOR)); + public static final Item IRON_TRAPDOOR = register(new BlockItem(builder(), Blocks.IRON_TRAPDOOR)); + public static final Item OAK_TRAPDOOR = register(new BlockItem(builder(), Blocks.OAK_TRAPDOOR)); + public static final Item SPRUCE_TRAPDOOR = register(new BlockItem(builder(), Blocks.SPRUCE_TRAPDOOR)); + public static final Item BIRCH_TRAPDOOR = register(new BlockItem(builder(), Blocks.BIRCH_TRAPDOOR)); + public static final Item JUNGLE_TRAPDOOR = register(new BlockItem(builder(), Blocks.JUNGLE_TRAPDOOR)); + public static final Item ACACIA_TRAPDOOR = register(new BlockItem(builder(), Blocks.ACACIA_TRAPDOOR)); + public static final Item CHERRY_TRAPDOOR = register(new BlockItem(builder(), Blocks.CHERRY_TRAPDOOR)); + public static final Item DARK_OAK_TRAPDOOR = register(new BlockItem(builder(), Blocks.DARK_OAK_TRAPDOOR)); + public static final Item PALE_OAK_TRAPDOOR = register(new BlockItem(builder(), Blocks.PALE_OAK_TRAPDOOR)); + public static final Item MANGROVE_TRAPDOOR = register(new BlockItem(builder(), Blocks.MANGROVE_TRAPDOOR)); + public static final Item BAMBOO_TRAPDOOR = register(new BlockItem(builder(), Blocks.BAMBOO_TRAPDOOR)); + public static final Item CRIMSON_TRAPDOOR = register(new BlockItem(builder(), Blocks.CRIMSON_TRAPDOOR)); + public static final Item WARPED_TRAPDOOR = register(new BlockItem(builder(), Blocks.WARPED_TRAPDOOR)); + public static final Item COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.COPPER_TRAPDOOR)); + public static final Item EXPOSED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_TRAPDOOR)); + public static final Item WEATHERED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_TRAPDOOR)); + public static final Item OXIDIZED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_TRAPDOOR)); + public static final Item WAXED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.WAXED_COPPER_TRAPDOOR)); + public static final Item WAXED_EXPOSED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_TRAPDOOR)); + public static final Item WAXED_WEATHERED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_TRAPDOOR)); + public static final Item WAXED_OXIDIZED_COPPER_TRAPDOOR = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_TRAPDOOR)); + public static final Item OAK_FENCE_GATE = register(new BlockItem(builder(), Blocks.OAK_FENCE_GATE)); + public static final Item SPRUCE_FENCE_GATE = register(new BlockItem(builder(), Blocks.SPRUCE_FENCE_GATE)); + public static final Item BIRCH_FENCE_GATE = register(new BlockItem(builder(), Blocks.BIRCH_FENCE_GATE)); + public static final Item JUNGLE_FENCE_GATE = register(new BlockItem(builder(), Blocks.JUNGLE_FENCE_GATE)); + public static final Item ACACIA_FENCE_GATE = register(new BlockItem(builder(), Blocks.ACACIA_FENCE_GATE)); + public static final Item CHERRY_FENCE_GATE = register(new BlockItem(builder(), Blocks.CHERRY_FENCE_GATE)); + public static final Item DARK_OAK_FENCE_GATE = register(new BlockItem(builder(), Blocks.DARK_OAK_FENCE_GATE)); + public static final Item PALE_OAK_FENCE_GATE = register(new BlockItem(builder(), Blocks.PALE_OAK_FENCE_GATE)); + public static final Item MANGROVE_FENCE_GATE = register(new BlockItem(builder(), Blocks.MANGROVE_FENCE_GATE)); + public static final Item BAMBOO_FENCE_GATE = register(new BlockItem(builder(), Blocks.BAMBOO_FENCE_GATE)); + public static final Item CRIMSON_FENCE_GATE = register(new BlockItem(builder(), Blocks.CRIMSON_FENCE_GATE)); + public static final Item WARPED_FENCE_GATE = register(new BlockItem(builder(), Blocks.WARPED_FENCE_GATE)); + public static final Item POWERED_RAIL = register(new BlockItem(builder(), Blocks.POWERED_RAIL)); + public static final Item DETECTOR_RAIL = register(new BlockItem(builder(), Blocks.DETECTOR_RAIL)); + public static final Item RAIL = register(new BlockItem(builder(), Blocks.RAIL)); + public static final Item ACTIVATOR_RAIL = register(new BlockItem(builder(), Blocks.ACTIVATOR_RAIL)); + public static final Item SADDLE = register(new Item("saddle", builder())); + public static final Item WHITE_HARNESS = register(new Item("white_harness", builder())); + public static final Item ORANGE_HARNESS = register(new Item("orange_harness", builder())); + public static final Item MAGENTA_HARNESS = register(new Item("magenta_harness", builder())); + public static final Item LIGHT_BLUE_HARNESS = register(new Item("light_blue_harness", builder())); + public static final Item YELLOW_HARNESS = register(new Item("yellow_harness", builder())); + public static final Item LIME_HARNESS = register(new Item("lime_harness", builder())); + public static final Item PINK_HARNESS = register(new Item("pink_harness", builder())); + public static final Item GRAY_HARNESS = register(new Item("gray_harness", builder())); + public static final Item LIGHT_GRAY_HARNESS = register(new Item("light_gray_harness", builder())); + public static final Item CYAN_HARNESS = register(new Item("cyan_harness", builder())); + public static final Item PURPLE_HARNESS = register(new Item("purple_harness", builder())); + public static final Item BLUE_HARNESS = register(new Item("blue_harness", builder())); + public static final Item BROWN_HARNESS = register(new Item("brown_harness", builder())); + public static final Item GREEN_HARNESS = register(new Item("green_harness", builder())); + public static final Item RED_HARNESS = register(new Item("red_harness", builder())); + public static final Item BLACK_HARNESS = register(new Item("black_harness", builder())); + public static final Item MINECART = register(new Item("minecart", builder())); + public static final Item CHEST_MINECART = register(new Item("chest_minecart", builder())); + public static final Item FURNACE_MINECART = register(new Item("furnace_minecart", builder())); + public static final Item TNT_MINECART = register(new Item("tnt_minecart", builder())); + public static final Item HOPPER_MINECART = register(new Item("hopper_minecart", builder())); + public static final Item CARROT_ON_A_STICK = register(new Item("carrot_on_a_stick", builder())); + public static final Item WARPED_FUNGUS_ON_A_STICK = register(new Item("warped_fungus_on_a_stick", builder())); + public static final Item PHANTOM_MEMBRANE = register(new Item("phantom_membrane", builder())); + public static final Item ELYTRA = register(new Item("elytra", builder())); + public static final Item OAK_BOAT = register(new BoatItem("oak_boat", builder())); + public static final Item OAK_CHEST_BOAT = register(new BoatItem("oak_chest_boat", builder())); + public static final Item SPRUCE_BOAT = register(new BoatItem("spruce_boat", builder())); + public static final Item SPRUCE_CHEST_BOAT = register(new BoatItem("spruce_chest_boat", builder())); + public static final Item BIRCH_BOAT = register(new BoatItem("birch_boat", builder())); + public static final Item BIRCH_CHEST_BOAT = register(new BoatItem("birch_chest_boat", builder())); + public static final Item JUNGLE_BOAT = register(new BoatItem("jungle_boat", builder())); + public static final Item JUNGLE_CHEST_BOAT = register(new BoatItem("jungle_chest_boat", builder())); + public static final Item ACACIA_BOAT = register(new BoatItem("acacia_boat", builder())); + public static final Item ACACIA_CHEST_BOAT = register(new BoatItem("acacia_chest_boat", builder())); + public static final Item CHERRY_BOAT = register(new BoatItem("cherry_boat", builder())); + public static final Item CHERRY_CHEST_BOAT = register(new BoatItem("cherry_chest_boat", builder())); + public static final Item DARK_OAK_BOAT = register(new BoatItem("dark_oak_boat", builder())); + public static final Item DARK_OAK_CHEST_BOAT = register(new BoatItem("dark_oak_chest_boat", builder())); + public static final Item PALE_OAK_BOAT = register(new BoatItem("pale_oak_boat", builder())); + public static final Item PALE_OAK_CHEST_BOAT = register(new BoatItem("pale_oak_chest_boat", builder())); + public static final Item MANGROVE_BOAT = register(new BoatItem("mangrove_boat", builder())); + public static final Item MANGROVE_CHEST_BOAT = register(new BoatItem("mangrove_chest_boat", builder())); + public static final Item BAMBOO_RAFT = register(new BoatItem("bamboo_raft", builder())); + public static final Item BAMBOO_CHEST_RAFT = register(new BoatItem("bamboo_chest_raft", builder())); + public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder(), Blocks.STRUCTURE_BLOCK)); + public static final Item JIGSAW = register(new BlockItem(builder(), Blocks.JIGSAW)); + public static final Item TEST_BLOCK = register(new BlockItem(builder(), Blocks.TEST_BLOCK)); + public static final Item TEST_INSTANCE_BLOCK = register(new BlockItem(builder(), Blocks.TEST_INSTANCE_BLOCK)); + public static final Item TURTLE_HELMET = register(new ArmorItem("turtle_helmet", builder())); + public static final Item TURTLE_SCUTE = register(new Item("turtle_scute", builder())); + public static final Item ARMADILLO_SCUTE = register(new Item("armadillo_scute", builder())); + public static final Item WOLF_ARMOR = register(new WolfArmorItem("wolf_armor", builder())); + public static final Item FLINT_AND_STEEL = register(new Item("flint_and_steel", builder())); + public static final Item BOWL = register(new Item("bowl", builder())); public static final Item APPLE = register(new Item("apple", builder())); - public static final Item BOW = register(new Item("bow", builder().stackSize(1).maxDamage(384))); + public static final Item BOW = register(new Item("bow", builder())); public static final Item ARROW = register(new ArrowItem("arrow", builder())); public static final Item COAL = register(new Item("coal", builder())); public static final Item CHARCOAL = register(new Item("charcoal", builder())); @@ -852,123 +977,151 @@ public final class Items { public static final Item GOLD_INGOT = register(new Item("gold_ingot", builder())); public static final Item NETHERITE_INGOT = register(new Item("netherite_ingot", builder())); public static final Item NETHERITE_SCRAP = register(new Item("netherite_scrap", builder())); - public static final Item WOODEN_SWORD = register(new TieredItem("wooden_sword", ToolTier.WOODEN, builder().stackSize(1).attackDamage(4).maxDamage(59))); - public static final Item WOODEN_SHOVEL = register(new TieredItem("wooden_shovel", ToolTier.WOODEN, builder().stackSize(1).attackDamage(2.5).maxDamage(59))); - public static final Item WOODEN_PICKAXE = register(new TieredItem("wooden_pickaxe", ToolTier.WOODEN, builder().stackSize(1).attackDamage(2).maxDamage(59))); - public static final Item WOODEN_AXE = register(new TieredItem("wooden_axe", ToolTier.WOODEN, builder().stackSize(1).attackDamage(7).maxDamage(59))); - public static final Item WOODEN_HOE = register(new TieredItem("wooden_hoe", ToolTier.WOODEN, builder().stackSize(1).attackDamage(1).maxDamage(59))); - public static final Item STONE_SWORD = register(new TieredItem("stone_sword", ToolTier.STONE, builder().stackSize(1).attackDamage(5).maxDamage(131))); - public static final Item STONE_SHOVEL = register(new TieredItem("stone_shovel", ToolTier.STONE, builder().stackSize(1).attackDamage(3.5).maxDamage(131))); - public static final Item STONE_PICKAXE = register(new TieredItem("stone_pickaxe", ToolTier.STONE, builder().stackSize(1).attackDamage(3).maxDamage(131))); - public static final Item STONE_AXE = register(new TieredItem("stone_axe", ToolTier.STONE, builder().stackSize(1).attackDamage(9).maxDamage(131))); - public static final Item STONE_HOE = register(new TieredItem("stone_hoe", ToolTier.STONE, builder().stackSize(1).attackDamage(1).maxDamage(131))); - public static final Item GOLDEN_SWORD = register(new TieredItem("golden_sword", ToolTier.GOLDEN, builder().stackSize(1).attackDamage(4).maxDamage(32))); - public static final Item GOLDEN_SHOVEL = register(new TieredItem("golden_shovel", ToolTier.GOLDEN, builder().stackSize(1).attackDamage(2.5).maxDamage(32))); - public static final Item GOLDEN_PICKAXE = register(new TieredItem("golden_pickaxe", ToolTier.GOLDEN, builder().stackSize(1).attackDamage(2).maxDamage(32))); - public static final Item GOLDEN_AXE = register(new TieredItem("golden_axe", ToolTier.GOLDEN, builder().stackSize(1).attackDamage(7).maxDamage(32))); - public static final Item GOLDEN_HOE = register(new TieredItem("golden_hoe", ToolTier.GOLDEN, builder().stackSize(1).attackDamage(1).maxDamage(32))); - public static final Item IRON_SWORD = register(new TieredItem("iron_sword", ToolTier.IRON, builder().stackSize(1).attackDamage(6).maxDamage(250))); - public static final Item IRON_SHOVEL = register(new TieredItem("iron_shovel", ToolTier.IRON, builder().stackSize(1).attackDamage(4.5).maxDamage(250))); - public static final Item IRON_PICKAXE = register(new TieredItem("iron_pickaxe", ToolTier.IRON, builder().stackSize(1).attackDamage(4).maxDamage(250))); - public static final Item IRON_AXE = register(new TieredItem("iron_axe", ToolTier.IRON, builder().stackSize(1).attackDamage(9).maxDamage(250))); - public static final Item IRON_HOE = register(new TieredItem("iron_hoe", ToolTier.IRON, builder().stackSize(1).attackDamage(1).maxDamage(250))); - public static final Item DIAMOND_SWORD = register(new TieredItem("diamond_sword", ToolTier.DIAMOND, builder().stackSize(1).attackDamage(7).maxDamage(1561))); - public static final Item DIAMOND_SHOVEL = register(new TieredItem("diamond_shovel", ToolTier.DIAMOND, builder().stackSize(1).attackDamage(5.5).maxDamage(1561))); - public static final Item DIAMOND_PICKAXE = register(new TieredItem("diamond_pickaxe", ToolTier.DIAMOND, builder().stackSize(1).attackDamage(5).maxDamage(1561))); - public static final Item DIAMOND_AXE = register(new TieredItem("diamond_axe", ToolTier.DIAMOND, builder().stackSize(1).attackDamage(9).maxDamage(1561))); - public static final Item DIAMOND_HOE = register(new TieredItem("diamond_hoe", ToolTier.DIAMOND, builder().stackSize(1).attackDamage(1).maxDamage(1561))); - public static final Item NETHERITE_SWORD = register(new TieredItem("netherite_sword", ToolTier.NETHERITE, builder().stackSize(1).attackDamage(8).maxDamage(2031))); - public static final Item NETHERITE_SHOVEL = register(new TieredItem("netherite_shovel", ToolTier.NETHERITE, builder().stackSize(1).attackDamage(6.5).maxDamage(2031))); - public static final Item NETHERITE_PICKAXE = register(new TieredItem("netherite_pickaxe", ToolTier.NETHERITE, builder().stackSize(1).attackDamage(6).maxDamage(2031))); - public static final Item NETHERITE_AXE = register(new TieredItem("netherite_axe", ToolTier.NETHERITE, builder().stackSize(1).attackDamage(10).maxDamage(2031))); - public static final Item NETHERITE_HOE = register(new TieredItem("netherite_hoe", ToolTier.NETHERITE, builder().stackSize(1).attackDamage(1).maxDamage(2031))); + public static final Item WOODEN_SWORD = register(new Item("wooden_sword", builder().attackDamage(4.0))); + public static final Item WOODEN_SHOVEL = register(new Item("wooden_shovel", builder().attackDamage(2.5))); + public static final Item WOODEN_PICKAXE = register(new Item("wooden_pickaxe", builder().attackDamage(2.0))); + public static final Item WOODEN_AXE = register(new Item("wooden_axe", builder().attackDamage(7.0))); + public static final Item WOODEN_HOE = register(new Item("wooden_hoe", builder().attackDamage(1.0))); + public static final Item COPPER_SWORD = register(new Item("copper_sword", builder().attackDamage(5.0))); + public static final Item COPPER_SHOVEL = register(new Item("copper_shovel", builder().attackDamage(3.5))); + public static final Item COPPER_PICKAXE = register(new Item("copper_pickaxe", builder().attackDamage(3.0))); + public static final Item COPPER_AXE = register(new Item("copper_axe", builder().attackDamage(9.0))); + public static final Item COPPER_HOE = register(new Item("copper_hoe", builder().attackDamage(1.0))); + public static final Item STONE_SWORD = register(new Item("stone_sword", builder().attackDamage(5.0))); + public static final Item STONE_SHOVEL = register(new Item("stone_shovel", builder().attackDamage(3.5))); + public static final Item STONE_PICKAXE = register(new Item("stone_pickaxe", builder().attackDamage(3.0))); + public static final Item STONE_AXE = register(new Item("stone_axe", builder().attackDamage(9.0))); + public static final Item STONE_HOE = register(new Item("stone_hoe", builder().attackDamage(1.0))); + public static final Item GOLDEN_SWORD = register(new Item("golden_sword", builder().attackDamage(4.0))); + public static final Item GOLDEN_SHOVEL = register(new Item("golden_shovel", builder().attackDamage(2.5))); + public static final Item GOLDEN_PICKAXE = register(new Item("golden_pickaxe", builder().attackDamage(2.0))); + public static final Item GOLDEN_AXE = register(new Item("golden_axe", builder().attackDamage(7.0))); + public static final Item GOLDEN_HOE = register(new Item("golden_hoe", builder().attackDamage(1.0))); + public static final Item IRON_SWORD = register(new Item("iron_sword", builder().attackDamage(6.0))); + public static final Item IRON_SHOVEL = register(new Item("iron_shovel", builder().attackDamage(4.5))); + public static final Item IRON_PICKAXE = register(new Item("iron_pickaxe", builder().attackDamage(4.0))); + public static final Item IRON_AXE = register(new Item("iron_axe", builder().attackDamage(9.0))); + public static final Item IRON_HOE = register(new Item("iron_hoe", builder().attackDamage(1.0))); + public static final Item DIAMOND_SWORD = register(new Item("diamond_sword", builder().attackDamage(7.0))); + public static final Item DIAMOND_SHOVEL = register(new Item("diamond_shovel", builder().attackDamage(5.5))); + public static final Item DIAMOND_PICKAXE = register(new Item("diamond_pickaxe", builder().attackDamage(5.0))); + public static final Item DIAMOND_AXE = register(new Item("diamond_axe", builder().attackDamage(9.0))); + public static final Item DIAMOND_HOE = register(new Item("diamond_hoe", builder().attackDamage(1.0))); + public static final Item NETHERITE_SWORD = register(new Item("netherite_sword", builder().attackDamage(8.0))); + public static final Item NETHERITE_SHOVEL = register(new Item("netherite_shovel", builder().attackDamage(6.5))); + public static final Item NETHERITE_PICKAXE = register(new Item("netherite_pickaxe", builder().attackDamage(6.0))); + public static final Item NETHERITE_AXE = register(new Item("netherite_axe", builder().attackDamage(10.0))); + public static final Item NETHERITE_HOE = register(new Item("netherite_hoe", builder().attackDamage(1.0))); public static final Item STICK = register(new Item("stick", builder())); - public static final Item BOWL = register(new Item("bowl", builder())); - public static final Item MUSHROOM_STEW = register(new Item("mushroom_stew", builder().stackSize(1))); - public static final Item STRING = register(new BlockItem("string", builder())); + public static final Item MUSHROOM_STEW = register(new Item("mushroom_stew", builder())); + public static final Item STRING = register(new BlockItem("string", builder(), Blocks.TRIPWIRE)); public static final Item FEATHER = register(new Item("feather", builder())); public static final Item GUNPOWDER = register(new Item("gunpowder", builder())); - public static final Item WHEAT_SEEDS = register(new BlockItem("wheat_seeds", builder())); + public static final Item WHEAT_SEEDS = register(new BlockItem("wheat_seeds", builder(), Blocks.WHEAT)); public static final Item WHEAT = register(new Item("wheat", builder())); public static final Item BREAD = register(new Item("bread", builder())); - public static final Item LEATHER_HELMET = register(new DyeableArmorItem("leather_helmet", ArmorMaterial.LEATHER, builder().stackSize(1).maxDamage(55))); - public static final Item LEATHER_CHESTPLATE = register(new DyeableArmorItem("leather_chestplate", ArmorMaterial.LEATHER, builder().stackSize(1).maxDamage(80))); - public static final Item LEATHER_LEGGINGS = register(new DyeableArmorItem("leather_leggings", ArmorMaterial.LEATHER, builder().stackSize(1).maxDamage(75))); - public static final Item LEATHER_BOOTS = register(new DyeableArmorItem("leather_boots", ArmorMaterial.LEATHER, builder().stackSize(1).maxDamage(65))); - public static final Item CHAINMAIL_HELMET = register(new ArmorItem("chainmail_helmet", ArmorMaterial.CHAIN, builder().stackSize(1).maxDamage(165))); - public static final Item CHAINMAIL_CHESTPLATE = register(new ArmorItem("chainmail_chestplate", ArmorMaterial.CHAIN, builder().stackSize(1).maxDamage(240))); - public static final Item CHAINMAIL_LEGGINGS = register(new ArmorItem("chainmail_leggings", ArmorMaterial.CHAIN, builder().stackSize(1).maxDamage(225))); - public static final Item CHAINMAIL_BOOTS = register(new ArmorItem("chainmail_boots", ArmorMaterial.CHAIN, builder().stackSize(1).maxDamage(195))); - public static final Item IRON_HELMET = register(new ArmorItem("iron_helmet", ArmorMaterial.IRON, builder().stackSize(1).maxDamage(165))); - public static final Item IRON_CHESTPLATE = register(new ArmorItem("iron_chestplate", ArmorMaterial.IRON, builder().stackSize(1).maxDamage(240))); - public static final Item IRON_LEGGINGS = register(new ArmorItem("iron_leggings", ArmorMaterial.IRON, builder().stackSize(1).maxDamage(225))); - public static final Item IRON_BOOTS = register(new ArmorItem("iron_boots", ArmorMaterial.IRON, builder().stackSize(1).maxDamage(195))); - public static final Item DIAMOND_HELMET = register(new ArmorItem("diamond_helmet", ArmorMaterial.DIAMOND, builder().stackSize(1).maxDamage(363))); - public static final Item DIAMOND_CHESTPLATE = register(new ArmorItem("diamond_chestplate", ArmorMaterial.DIAMOND, builder().stackSize(1).maxDamage(528))); - public static final Item DIAMOND_LEGGINGS = register(new ArmorItem("diamond_leggings", ArmorMaterial.DIAMOND, builder().stackSize(1).maxDamage(495))); - public static final Item DIAMOND_BOOTS = register(new ArmorItem("diamond_boots", ArmorMaterial.DIAMOND, builder().stackSize(1).maxDamage(429))); - public static final Item GOLDEN_HELMET = register(new ArmorItem("golden_helmet", ArmorMaterial.GOLD, builder().stackSize(1).maxDamage(77))); - public static final Item GOLDEN_CHESTPLATE = register(new ArmorItem("golden_chestplate", ArmorMaterial.GOLD, builder().stackSize(1).maxDamage(112))); - public static final Item GOLDEN_LEGGINGS = register(new ArmorItem("golden_leggings", ArmorMaterial.GOLD, builder().stackSize(1).maxDamage(105))); - public static final Item GOLDEN_BOOTS = register(new ArmorItem("golden_boots", ArmorMaterial.GOLD, builder().stackSize(1).maxDamage(91))); - public static final Item NETHERITE_HELMET = register(new ArmorItem("netherite_helmet", ArmorMaterial.NETHERITE, builder().stackSize(1).maxDamage(407))); - public static final Item NETHERITE_CHESTPLATE = register(new ArmorItem("netherite_chestplate", ArmorMaterial.NETHERITE, builder().stackSize(1).maxDamage(592))); - public static final Item NETHERITE_LEGGINGS = register(new ArmorItem("netherite_leggings", ArmorMaterial.NETHERITE, builder().stackSize(1).maxDamage(555))); - public static final Item NETHERITE_BOOTS = register(new ArmorItem("netherite_boots", ArmorMaterial.NETHERITE, builder().stackSize(1).maxDamage(481))); + public static final Item LEATHER_HELMET = register(new DyeableArmorItem("leather_helmet", builder())); + public static final Item LEATHER_CHESTPLATE = register(new DyeableArmorItem("leather_chestplate", builder())); + public static final Item LEATHER_LEGGINGS = register(new DyeableArmorItem("leather_leggings", builder())); + public static final Item LEATHER_BOOTS = register(new DyeableArmorItem("leather_boots", builder())); + public static final Item COPPER_HELMET = register(new ArmorItem("copper_helmet", builder())); + public static final Item COPPER_CHESTPLATE = register(new ArmorItem("copper_chestplate", builder())); + public static final Item COPPER_LEGGINGS = register(new ArmorItem("copper_leggings", builder())); + public static final Item COPPER_BOOTS = register(new ArmorItem("copper_boots", builder())); + public static final Item CHAINMAIL_HELMET = register(new ArmorItem("chainmail_helmet", builder())); + public static final Item CHAINMAIL_CHESTPLATE = register(new ArmorItem("chainmail_chestplate", builder())); + public static final Item CHAINMAIL_LEGGINGS = register(new ArmorItem("chainmail_leggings", builder())); + public static final Item CHAINMAIL_BOOTS = register(new ArmorItem("chainmail_boots", builder())); + public static final Item IRON_HELMET = register(new ArmorItem("iron_helmet", builder())); + public static final Item IRON_CHESTPLATE = register(new ArmorItem("iron_chestplate", builder())); + public static final Item IRON_LEGGINGS = register(new ArmorItem("iron_leggings", builder())); + public static final Item IRON_BOOTS = register(new ArmorItem("iron_boots", builder())); + public static final Item DIAMOND_HELMET = register(new ArmorItem("diamond_helmet", builder())); + public static final Item DIAMOND_CHESTPLATE = register(new ArmorItem("diamond_chestplate", builder())); + public static final Item DIAMOND_LEGGINGS = register(new ArmorItem("diamond_leggings", builder())); + public static final Item DIAMOND_BOOTS = register(new ArmorItem("diamond_boots", builder())); + public static final Item GOLDEN_HELMET = register(new ArmorItem("golden_helmet", builder())); + public static final Item GOLDEN_CHESTPLATE = register(new ArmorItem("golden_chestplate", builder())); + public static final Item GOLDEN_LEGGINGS = register(new ArmorItem("golden_leggings", builder())); + public static final Item GOLDEN_BOOTS = register(new ArmorItem("golden_boots", builder())); + public static final Item NETHERITE_HELMET = register(new ArmorItem("netherite_helmet", builder())); + public static final Item NETHERITE_CHESTPLATE = register(new ArmorItem("netherite_chestplate", builder())); + public static final Item NETHERITE_LEGGINGS = register(new ArmorItem("netherite_leggings", builder())); + public static final Item NETHERITE_BOOTS = register(new ArmorItem("netherite_boots", builder())); public static final Item FLINT = register(new Item("flint", builder())); public static final Item PORKCHOP = register(new Item("porkchop", builder())); public static final Item COOKED_PORKCHOP = register(new Item("cooked_porkchop", builder())); public static final Item PAINTING = register(new Item("painting", builder())); public static final Item GOLDEN_APPLE = register(new Item("golden_apple", builder())); public static final Item ENCHANTED_GOLDEN_APPLE = register(new Item("enchanted_golden_apple", builder())); - public static final Item OAK_SIGN = register(new BlockItem("oak_sign", builder().stackSize(16))); - public static final Item SPRUCE_SIGN = register(new BlockItem("spruce_sign", builder().stackSize(16))); - public static final Item BIRCH_SIGN = register(new BlockItem("birch_sign", builder().stackSize(16))); - public static final Item JUNGLE_SIGN = register(new BlockItem("jungle_sign", builder().stackSize(16))); - public static final Item ACACIA_SIGN = register(new BlockItem("acacia_sign", builder().stackSize(16))); - public static final Item CHERRY_SIGN = register(new BlockItem("cherry_sign", builder().stackSize(16))); - public static final Item DARK_OAK_SIGN = register(new BlockItem("dark_oak_sign", builder().stackSize(16))); - public static final Item MANGROVE_SIGN = register(new BlockItem("mangrove_sign", builder().stackSize(16))); - public static final Item BAMBOO_SIGN = register(new BlockItem("bamboo_sign", builder().stackSize(16))); - public static final Item CRIMSON_SIGN = register(new BlockItem("crimson_sign", builder().stackSize(16))); - public static final Item WARPED_SIGN = register(new BlockItem("warped_sign", builder().stackSize(16))); - public static final Item OAK_HANGING_SIGN = register(new BlockItem("oak_hanging_sign", builder().stackSize(16))); - public static final Item SPRUCE_HANGING_SIGN = register(new BlockItem("spruce_hanging_sign", builder().stackSize(16))); - public static final Item BIRCH_HANGING_SIGN = register(new BlockItem("birch_hanging_sign", builder().stackSize(16))); - public static final Item JUNGLE_HANGING_SIGN = register(new BlockItem("jungle_hanging_sign", builder().stackSize(16))); - public static final Item ACACIA_HANGING_SIGN = register(new BlockItem("acacia_hanging_sign", builder().stackSize(16))); - public static final Item CHERRY_HANGING_SIGN = register(new BlockItem("cherry_hanging_sign", builder().stackSize(16))); - public static final Item DARK_OAK_HANGING_SIGN = register(new BlockItem("dark_oak_hanging_sign", builder().stackSize(16))); - public static final Item MANGROVE_HANGING_SIGN = register(new BlockItem("mangrove_hanging_sign", builder().stackSize(16))); - public static final Item BAMBOO_HANGING_SIGN = register(new BlockItem("bamboo_hanging_sign", builder().stackSize(16))); - public static final Item CRIMSON_HANGING_SIGN = register(new BlockItem("crimson_hanging_sign", builder().stackSize(16))); - public static final Item WARPED_HANGING_SIGN = register(new BlockItem("warped_hanging_sign", builder().stackSize(16))); - public static final Item BUCKET = register(new Item("bucket", builder().stackSize(16))); - public static final Item WATER_BUCKET = register(new Item("water_bucket", builder().stackSize(1))); - public static final Item LAVA_BUCKET = register(new Item("lava_bucket", builder().stackSize(1))); - public static final Item POWDER_SNOW_BUCKET = register(new BlockItem("powder_snow_bucket", builder().stackSize(1))); - public static final Item SNOWBALL = register(new Item("snowball", builder().stackSize(16))); + public static final Item OAK_SIGN = register(new BlockItem(builder(), Blocks.OAK_SIGN, Blocks.OAK_WALL_SIGN)); + public static final Item SPRUCE_SIGN = register(new BlockItem(builder(), Blocks.SPRUCE_SIGN, Blocks.SPRUCE_WALL_SIGN)); + public static final Item BIRCH_SIGN = register(new BlockItem(builder(), Blocks.BIRCH_SIGN, Blocks.BIRCH_WALL_SIGN)); + public static final Item JUNGLE_SIGN = register(new BlockItem(builder(), Blocks.JUNGLE_SIGN, Blocks.JUNGLE_WALL_SIGN)); + public static final Item ACACIA_SIGN = register(new BlockItem(builder(), Blocks.ACACIA_SIGN, Blocks.ACACIA_WALL_SIGN)); + public static final Item CHERRY_SIGN = register(new BlockItem(builder(), Blocks.CHERRY_SIGN, Blocks.CHERRY_WALL_SIGN)); + public static final Item DARK_OAK_SIGN = register(new BlockItem(builder(), Blocks.DARK_OAK_SIGN, Blocks.DARK_OAK_WALL_SIGN)); + public static final Item PALE_OAK_SIGN = register(new BlockItem(builder(), Blocks.PALE_OAK_SIGN, Blocks.PALE_OAK_WALL_SIGN)); + public static final Item MANGROVE_SIGN = register(new BlockItem(builder(), Blocks.MANGROVE_SIGN, Blocks.MANGROVE_WALL_SIGN)); + public static final Item BAMBOO_SIGN = register(new BlockItem(builder(), Blocks.BAMBOO_SIGN, Blocks.BAMBOO_WALL_SIGN)); + public static final Item CRIMSON_SIGN = register(new BlockItem(builder(), Blocks.CRIMSON_SIGN, Blocks.CRIMSON_WALL_SIGN)); + public static final Item WARPED_SIGN = register(new BlockItem(builder(), Blocks.WARPED_SIGN, Blocks.WARPED_WALL_SIGN)); + public static final Item OAK_HANGING_SIGN = register(new BlockItem(builder(), Blocks.OAK_HANGING_SIGN, Blocks.OAK_WALL_HANGING_SIGN)); + public static final Item SPRUCE_HANGING_SIGN = register(new BlockItem(builder(), Blocks.SPRUCE_HANGING_SIGN, Blocks.SPRUCE_WALL_HANGING_SIGN)); + public static final Item BIRCH_HANGING_SIGN = register(new BlockItem(builder(), Blocks.BIRCH_HANGING_SIGN, Blocks.BIRCH_WALL_HANGING_SIGN)); + public static final Item JUNGLE_HANGING_SIGN = register(new BlockItem(builder(), Blocks.JUNGLE_HANGING_SIGN, Blocks.JUNGLE_WALL_HANGING_SIGN)); + public static final Item ACACIA_HANGING_SIGN = register(new BlockItem(builder(), Blocks.ACACIA_HANGING_SIGN, Blocks.ACACIA_WALL_HANGING_SIGN)); + public static final Item CHERRY_HANGING_SIGN = register(new BlockItem(builder(), Blocks.CHERRY_HANGING_SIGN, Blocks.CHERRY_WALL_HANGING_SIGN)); + public static final Item DARK_OAK_HANGING_SIGN = register(new BlockItem(builder(), Blocks.DARK_OAK_HANGING_SIGN, Blocks.DARK_OAK_WALL_HANGING_SIGN)); + public static final Item PALE_OAK_HANGING_SIGN = register(new BlockItem(builder(), Blocks.PALE_OAK_HANGING_SIGN, Blocks.PALE_OAK_WALL_HANGING_SIGN)); + public static final Item MANGROVE_HANGING_SIGN = register(new BlockItem(builder(), Blocks.MANGROVE_HANGING_SIGN, Blocks.MANGROVE_WALL_HANGING_SIGN)); + public static final Item BAMBOO_HANGING_SIGN = register(new BlockItem(builder(), Blocks.BAMBOO_HANGING_SIGN, Blocks.BAMBOO_WALL_HANGING_SIGN)); + public static final Item CRIMSON_HANGING_SIGN = register(new BlockItem(builder(), Blocks.CRIMSON_HANGING_SIGN, Blocks.CRIMSON_WALL_HANGING_SIGN)); + public static final Item WARPED_HANGING_SIGN = register(new BlockItem(builder(), Blocks.WARPED_HANGING_SIGN, Blocks.WARPED_WALL_HANGING_SIGN)); + public static final Item BUCKET = register(new Item("bucket", builder())); + public static final Item WATER_BUCKET = register(new Item("water_bucket", builder())); + public static final Item LAVA_BUCKET = register(new Item("lava_bucket", builder())); + public static final Item POWDER_SNOW_BUCKET = register(new BlockItem("powder_snow_bucket", builder(), Blocks.POWDER_SNOW)); + public static final Item SNOWBALL = register(new Item("snowball", builder())); public static final Item LEATHER = register(new Item("leather", builder())); - public static final Item MILK_BUCKET = register(new Item("milk_bucket", builder().stackSize(1))); - public static final Item PUFFERFISH_BUCKET = register(new Item("pufferfish_bucket", builder().stackSize(1))); - public static final Item SALMON_BUCKET = register(new Item("salmon_bucket", builder().stackSize(1))); - public static final Item COD_BUCKET = register(new Item("cod_bucket", builder().stackSize(1))); - public static final Item TROPICAL_FISH_BUCKET = register(new TropicalFishBucketItem("tropical_fish_bucket", builder().stackSize(1))); - public static final Item AXOLOTL_BUCKET = register(new AxolotlBucketItem("axolotl_bucket", builder().stackSize(1))); - public static final Item TADPOLE_BUCKET = register(new Item("tadpole_bucket", builder().stackSize(1))); + public static final Item MILK_BUCKET = register(new Item("milk_bucket", builder())); + public static final Item PUFFERFISH_BUCKET = register(new Item("pufferfish_bucket", builder())); + public static final Item SALMON_BUCKET = register(new Item("salmon_bucket", builder())); + public static final Item COD_BUCKET = register(new Item("cod_bucket", builder())); + public static final Item TROPICAL_FISH_BUCKET = register(new TropicalFishBucketItem("tropical_fish_bucket", builder())); + public static final Item AXOLOTL_BUCKET = register(new AxolotlBucketItem("axolotl_bucket", builder())); + public static final Item TADPOLE_BUCKET = register(new Item("tadpole_bucket", builder())); public static final Item BRICK = register(new Item("brick", builder())); public static final Item CLAY_BALL = register(new Item("clay_ball", builder())); - public static final Item DRIED_KELP_BLOCK = register(new BlockItem("dried_kelp_block", builder())); + public static final Item DRIED_KELP_BLOCK = register(new BlockItem(builder(), Blocks.DRIED_KELP_BLOCK)); public static final Item PAPER = register(new Item("paper", builder())); public static final Item BOOK = register(new Item("book", builder())); public static final Item SLIME_BALL = register(new Item("slime_ball", builder())); - public static final Item EGG = register(new Item("egg", builder().stackSize(16))); + public static final Item EGG = register(new Item("egg", builder())); + public static final Item BLUE_EGG = register(new Item("blue_egg", builder())); + public static final Item BROWN_EGG = register(new Item("brown_egg", builder())); public static final Item COMPASS = register(new CompassItem("compass", builder())); public static final Item RECOVERY_COMPASS = register(new Item("recovery_compass", builder())); - public static final Item BUNDLE = register(new Item("bundle", builder().stackSize(1))); - public static final Item FISHING_ROD = register(new FishingRodItem("fishing_rod", builder().stackSize(1).maxDamage(64))); + public static final Item BUNDLE = register(new Item("bundle", builder())); + public static final Item WHITE_BUNDLE = register(new Item("white_bundle", builder())); + public static final Item ORANGE_BUNDLE = register(new Item("orange_bundle", builder())); + public static final Item MAGENTA_BUNDLE = register(new Item("magenta_bundle", builder())); + public static final Item LIGHT_BLUE_BUNDLE = register(new Item("light_blue_bundle", builder())); + public static final Item YELLOW_BUNDLE = register(new Item("yellow_bundle", builder())); + public static final Item LIME_BUNDLE = register(new Item("lime_bundle", builder())); + public static final Item PINK_BUNDLE = register(new Item("pink_bundle", builder())); + public static final Item GRAY_BUNDLE = register(new Item("gray_bundle", builder())); + public static final Item LIGHT_GRAY_BUNDLE = register(new Item("light_gray_bundle", builder())); + public static final Item CYAN_BUNDLE = register(new Item("cyan_bundle", builder())); + public static final Item PURPLE_BUNDLE = register(new Item("purple_bundle", builder())); + public static final Item BLUE_BUNDLE = register(new Item("blue_bundle", builder())); + public static final Item BROWN_BUNDLE = register(new Item("brown_bundle", builder())); + public static final Item GREEN_BUNDLE = register(new Item("green_bundle", builder())); + public static final Item RED_BUNDLE = register(new Item("red_bundle", builder())); + public static final Item BLACK_BUNDLE = register(new Item("black_bundle", builder())); + public static final Item FISHING_ROD = register(new FishingRodItem("fishing_rod", builder())); public static final Item CLOCK = register(new Item("clock", builder())); - public static final Item SPYGLASS = register(new Item("spyglass", builder().stackSize(1))); + public static final Item SPYGLASS = register(new Item("spyglass", builder())); public static final Item GLOWSTONE_DUST = register(new Item("glowstone_dust", builder())); public static final Item COD = register(new Item("cod", builder())); public static final Item SALMON = register(new Item("salmon", builder())); @@ -978,7 +1131,7 @@ public final class Items { public static final Item COOKED_SALMON = register(new Item("cooked_salmon", builder())); public static final Item INK_SAC = register(new Item("ink_sac", builder())); public static final Item GLOW_INK_SAC = register(new Item("glow_ink_sac", builder())); - public static final Item COCOA_BEANS = register(new BlockItem("cocoa_beans", builder())); + public static final Item COCOA_BEANS = register(new BlockItem("cocoa_beans", builder(), Blocks.COCOA)); public static final Item WHITE_DYE = register(new DyeItem("white_dye", 0, builder())); public static final Item ORANGE_DYE = register(new DyeItem("orange_dye", 1, builder())); public static final Item MAGENTA_DYE = register(new DyeItem("magenta_dye", 2, builder())); @@ -998,62 +1151,65 @@ public final class Items { public static final Item BONE_MEAL = register(new Item("bone_meal", builder())); public static final Item BONE = register(new Item("bone", builder())); public static final Item SUGAR = register(new Item("sugar", builder())); - public static final Item CAKE = register(new BlockItem("cake", builder().stackSize(1))); - public static final Item WHITE_BED = register(new BlockItem("white_bed", builder().stackSize(1))); - public static final Item ORANGE_BED = register(new BlockItem("orange_bed", builder().stackSize(1))); - public static final Item MAGENTA_BED = register(new BlockItem("magenta_bed", builder().stackSize(1))); - public static final Item LIGHT_BLUE_BED = register(new BlockItem("light_blue_bed", builder().stackSize(1))); - public static final Item YELLOW_BED = register(new BlockItem("yellow_bed", builder().stackSize(1))); - public static final Item LIME_BED = register(new BlockItem("lime_bed", builder().stackSize(1))); - public static final Item PINK_BED = register(new BlockItem("pink_bed", builder().stackSize(1))); - public static final Item GRAY_BED = register(new BlockItem("gray_bed", builder().stackSize(1))); - public static final Item LIGHT_GRAY_BED = register(new BlockItem("light_gray_bed", builder().stackSize(1))); - public static final Item CYAN_BED = register(new BlockItem("cyan_bed", builder().stackSize(1))); - public static final Item PURPLE_BED = register(new BlockItem("purple_bed", builder().stackSize(1))); - public static final Item BLUE_BED = register(new BlockItem("blue_bed", builder().stackSize(1))); - public static final Item BROWN_BED = register(new BlockItem("brown_bed", builder().stackSize(1))); - public static final Item GREEN_BED = register(new BlockItem("green_bed", builder().stackSize(1))); - public static final Item RED_BED = register(new BlockItem("red_bed", builder().stackSize(1))); - public static final Item BLACK_BED = register(new BlockItem("black_bed", builder().stackSize(1))); + public static final Item CAKE = register(new BlockItem(builder(), Blocks.CAKE)); + public static final Item WHITE_BED = register(new BlockItem(builder(), Blocks.WHITE_BED)); + public static final Item ORANGE_BED = register(new BlockItem(builder(), Blocks.ORANGE_BED)); + public static final Item MAGENTA_BED = register(new BlockItem(builder(), Blocks.MAGENTA_BED)); + public static final Item LIGHT_BLUE_BED = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_BED)); + public static final Item YELLOW_BED = register(new BlockItem(builder(), Blocks.YELLOW_BED)); + public static final Item LIME_BED = register(new BlockItem(builder(), Blocks.LIME_BED)); + public static final Item PINK_BED = register(new BlockItem(builder(), Blocks.PINK_BED)); + public static final Item GRAY_BED = register(new BlockItem(builder(), Blocks.GRAY_BED)); + public static final Item LIGHT_GRAY_BED = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_BED)); + public static final Item CYAN_BED = register(new BlockItem(builder(), Blocks.CYAN_BED)); + public static final Item PURPLE_BED = register(new BlockItem(builder(), Blocks.PURPLE_BED)); + public static final Item BLUE_BED = register(new BlockItem(builder(), Blocks.BLUE_BED)); + public static final Item BROWN_BED = register(new BlockItem(builder(), Blocks.BROWN_BED)); + public static final Item GREEN_BED = register(new BlockItem(builder(), Blocks.GREEN_BED)); + public static final Item RED_BED = register(new BlockItem(builder(), Blocks.RED_BED)); + public static final Item BLACK_BED = register(new BlockItem(builder(), Blocks.BLACK_BED)); public static final Item COOKIE = register(new Item("cookie", builder())); - public static final Item CRAFTER = register(new BlockItem("crafter", builder())); + public static final Item CRAFTER = register(new BlockItem(builder(), Blocks.CRAFTER)); public static final Item FILLED_MAP = register(new FilledMapItem("filled_map", builder())); - public static final Item SHEARS = register(new Item("shears", builder().stackSize(1).maxDamage(238))); + public static final Item SHEARS = register(new Item("shears", builder())); public static final Item MELON_SLICE = register(new Item("melon_slice", builder())); public static final Item DRIED_KELP = register(new Item("dried_kelp", builder())); - public static final Item PUMPKIN_SEEDS = register(new BlockItem("pumpkin_seeds", builder())); - public static final Item MELON_SEEDS = register(new BlockItem("melon_seeds", builder())); + public static final Item PUMPKIN_SEEDS = register(new BlockItem("pumpkin_seeds", builder(), Blocks.PUMPKIN_STEM)); + public static final Item MELON_SEEDS = register(new BlockItem("melon_seeds", builder(), Blocks.MELON_STEM)); public static final Item BEEF = register(new Item("beef", builder())); public static final Item COOKED_BEEF = register(new Item("cooked_beef", builder())); public static final Item CHICKEN = register(new Item("chicken", builder())); public static final Item COOKED_CHICKEN = register(new Item("cooked_chicken", builder())); public static final Item ROTTEN_FLESH = register(new Item("rotten_flesh", builder())); - public static final Item ENDER_PEARL = register(new Item("ender_pearl", builder().stackSize(16))); + public static final Item ENDER_PEARL = register(new Item("ender_pearl", builder())); public static final Item BLAZE_ROD = register(new Item("blaze_rod", builder())); public static final Item GHAST_TEAR = register(new Item("ghast_tear", builder())); public static final Item GOLD_NUGGET = register(new Item("gold_nugget", builder())); - public static final Item NETHER_WART = register(new BlockItem("nether_wart", builder())); - public static final Item POTION = register(new PotionItem("potion", builder().stackSize(1))); + public static final Item NETHER_WART = register(new BlockItem(builder(), Blocks.NETHER_WART)); public static final Item GLASS_BOTTLE = register(new Item("glass_bottle", builder())); + public static final Item POTION = register(new PotionItem("potion", builder())); public static final Item SPIDER_EYE = register(new Item("spider_eye", builder())); public static final Item FERMENTED_SPIDER_EYE = register(new Item("fermented_spider_eye", builder())); public static final Item BLAZE_POWDER = register(new Item("blaze_powder", builder())); public static final Item MAGMA_CREAM = register(new Item("magma_cream", builder())); - public static final Item BREWING_STAND = register(new BlockItem("brewing_stand", builder())); - public static final Item CAULDRON = register(new BlockItem("cauldron", builder())); + public static final Item BREWING_STAND = register(new BlockItem(builder(), Blocks.BREWING_STAND)); + public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.LAVA_CAULDRON)); public static final Item ENDER_EYE = register(new Item("ender_eye", builder())); public static final Item GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder())); + public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", builder())); public static final Item ALLAY_SPAWN_EGG = register(new SpawnEggItem("allay_spawn_egg", builder())); public static final Item AXOLOTL_SPAWN_EGG = register(new SpawnEggItem("axolotl_spawn_egg", builder())); public static final Item BAT_SPAWN_EGG = register(new SpawnEggItem("bat_spawn_egg", builder())); public static final Item BEE_SPAWN_EGG = register(new SpawnEggItem("bee_spawn_egg", builder())); public static final Item BLAZE_SPAWN_EGG = register(new SpawnEggItem("blaze_spawn_egg", builder())); + public static final Item BOGGED_SPAWN_EGG = register(new SpawnEggItem("bogged_spawn_egg", builder())); public static final Item BREEZE_SPAWN_EGG = register(new SpawnEggItem("breeze_spawn_egg", builder())); public static final Item CAT_SPAWN_EGG = register(new SpawnEggItem("cat_spawn_egg", builder())); public static final Item CAMEL_SPAWN_EGG = register(new SpawnEggItem("camel_spawn_egg", builder())); public static final Item CAVE_SPIDER_SPAWN_EGG = register(new SpawnEggItem("cave_spider_spawn_egg", builder())); public static final Item CHICKEN_SPAWN_EGG = register(new SpawnEggItem("chicken_spawn_egg", builder())); public static final Item COD_SPAWN_EGG = register(new SpawnEggItem("cod_spawn_egg", builder())); + public static final Item COPPER_GOLEM_SPAWN_EGG = register(new SpawnEggItem("copper_golem_spawn_egg", builder())); public static final Item COW_SPAWN_EGG = register(new SpawnEggItem("cow_spawn_egg", builder())); public static final Item CREEPER_SPAWN_EGG = register(new SpawnEggItem("creeper_spawn_egg", builder())); public static final Item DOLPHIN_SPAWN_EGG = register(new SpawnEggItem("dolphin_spawn_egg", builder())); @@ -1067,6 +1223,7 @@ public final class Items { public static final Item FOX_SPAWN_EGG = register(new SpawnEggItem("fox_spawn_egg", builder())); public static final Item FROG_SPAWN_EGG = register(new SpawnEggItem("frog_spawn_egg", builder())); public static final Item GHAST_SPAWN_EGG = register(new SpawnEggItem("ghast_spawn_egg", builder())); + public static final Item HAPPY_GHAST_SPAWN_EGG = register(new SpawnEggItem("happy_ghast_spawn_egg", builder())); public static final Item GLOW_SQUID_SPAWN_EGG = register(new SpawnEggItem("glow_squid_spawn_egg", builder())); public static final Item GOAT_SPAWN_EGG = register(new SpawnEggItem("goat_spawn_egg", builder())); public static final Item GUARDIAN_SPAWN_EGG = register(new SpawnEggItem("guardian_spawn_egg", builder())); @@ -1117,184 +1274,207 @@ public final class Items { public static final Item WITHER_SKELETON_SPAWN_EGG = register(new SpawnEggItem("wither_skeleton_spawn_egg", builder())); public static final Item WOLF_SPAWN_EGG = register(new SpawnEggItem("wolf_spawn_egg", builder())); public static final Item ZOGLIN_SPAWN_EGG = register(new SpawnEggItem("zoglin_spawn_egg", builder())); + public static final Item CREAKING_SPAWN_EGG = register(new SpawnEggItem("creaking_spawn_egg", builder())); public static final Item ZOMBIE_SPAWN_EGG = register(new SpawnEggItem("zombie_spawn_egg", builder())); public static final Item ZOMBIE_HORSE_SPAWN_EGG = register(new SpawnEggItem("zombie_horse_spawn_egg", builder())); public static final Item ZOMBIE_VILLAGER_SPAWN_EGG = register(new SpawnEggItem("zombie_villager_spawn_egg", builder())); public static final Item ZOMBIFIED_PIGLIN_SPAWN_EGG = register(new SpawnEggItem("zombified_piglin_spawn_egg", builder())); public static final Item EXPERIENCE_BOTTLE = register(new Item("experience_bottle", builder())); public static final Item FIRE_CHARGE = register(new Item("fire_charge", builder())); - public static final Item WRITABLE_BOOK = register(new WritableBookItem("writable_book", builder().stackSize(1))); - public static final Item WRITTEN_BOOK = register(new WrittenBookItem("written_book", builder().stackSize(16))); + public static final Item WIND_CHARGE = register(new Item("wind_charge", builder())); + public static final Item WRITABLE_BOOK = register(new WritableBookItem("writable_book", builder())); + public static final Item WRITTEN_BOOK = register(new WrittenBookItem("written_book", builder())); + public static final Item BREEZE_ROD = register(new Item("breeze_rod", builder())); + public static final Item MACE = register(new Item("mace", builder().attackDamage(6.0))); public static final Item ITEM_FRAME = register(new Item("item_frame", builder())); public static final Item GLOW_ITEM_FRAME = register(new Item("glow_item_frame", builder())); - public static final Item FLOWER_POT = register(new BlockItem("flower_pot", builder())); - public static final Item CARROT = register(new BlockItem("carrot", builder())); - public static final Item POTATO = register(new BlockItem("potato", builder())); + public static final Item FLOWER_POT = register(new BlockItem(builder(), Blocks.FLOWER_POT)); + public static final Item CARROT = register(new BlockItem("carrot", builder(), Blocks.CARROTS)); + public static final Item POTATO = register(new BlockItem("potato", builder(), Blocks.POTATOES)); public static final Item BAKED_POTATO = register(new Item("baked_potato", builder())); public static final Item POISONOUS_POTATO = register(new Item("poisonous_potato", builder())); public static final Item MAP = register(new MapItem("map", builder())); public static final Item GOLDEN_CARROT = register(new Item("golden_carrot", builder())); - public static final Item SKELETON_SKULL = register(new BlockItem("skeleton_skull", builder())); - public static final Item WITHER_SKELETON_SKULL = register(new BlockItem("wither_skeleton_skull", builder())); - public static final Item PLAYER_HEAD = register(new PlayerHeadItem("player_head", builder())); - public static final Item ZOMBIE_HEAD = register(new BlockItem("zombie_head", builder())); - public static final Item CREEPER_HEAD = register(new BlockItem("creeper_head", builder())); - public static final Item DRAGON_HEAD = register(new BlockItem("dragon_head", builder())); - public static final Item PIGLIN_HEAD = register(new BlockItem("piglin_head", builder())); + public static final Item SKELETON_SKULL = register(new BlockItem(builder(), Blocks.SKELETON_SKULL, Blocks.SKELETON_WALL_SKULL)); + public static final Item WITHER_SKELETON_SKULL = register(new BlockItem(builder(), Blocks.WITHER_SKELETON_SKULL, Blocks.WITHER_SKELETON_WALL_SKULL)); + public static final Item PLAYER_HEAD = register(new PlayerHeadItem(builder(), Blocks.PLAYER_HEAD, Blocks.PLAYER_WALL_HEAD)); + public static final Item ZOMBIE_HEAD = register(new BlockItem(builder(), Blocks.ZOMBIE_HEAD, Blocks.ZOMBIE_WALL_HEAD)); + public static final Item CREEPER_HEAD = register(new BlockItem(builder(), Blocks.CREEPER_HEAD, Blocks.CREEPER_WALL_HEAD)); + public static final Item DRAGON_HEAD = register(new BlockItem(builder(), Blocks.DRAGON_HEAD, Blocks.DRAGON_WALL_HEAD)); + public static final Item PIGLIN_HEAD = register(new BlockItem(builder(), Blocks.PIGLIN_HEAD, Blocks.PIGLIN_WALL_HEAD)); public static final Item NETHER_STAR = register(new Item("nether_star", builder())); public static final Item PUMPKIN_PIE = register(new Item("pumpkin_pie", builder())); public static final Item FIREWORK_ROCKET = register(new FireworkRocketItem("firework_rocket", builder())); public static final Item FIREWORK_STAR = register(new FireworkStarItem("firework_star", builder())); - public static final Item ENCHANTED_BOOK = register(new EnchantedBookItem("enchanted_book", builder().stackSize(1))); + public static final Item ENCHANTED_BOOK = register(new EnchantedBookItem("enchanted_book", builder())); public static final Item NETHER_BRICK = register(new Item("nether_brick", builder())); + public static final Item RESIN_BRICK = register(new Item("resin_brick", builder())); public static final Item PRISMARINE_SHARD = register(new Item("prismarine_shard", builder())); public static final Item PRISMARINE_CRYSTALS = register(new Item("prismarine_crystals", builder())); public static final Item RABBIT = register(new Item("rabbit", builder())); public static final Item COOKED_RABBIT = register(new Item("cooked_rabbit", builder())); - public static final Item RABBIT_STEW = register(new Item("rabbit_stew", builder().stackSize(1))); + public static final Item RABBIT_STEW = register(new Item("rabbit_stew", builder())); public static final Item RABBIT_FOOT = register(new Item("rabbit_foot", builder())); public static final Item RABBIT_HIDE = register(new Item("rabbit_hide", builder())); - public static final Item ARMOR_STAND = register(new Item("armor_stand", builder().stackSize(16))); - public static final Item IRON_HORSE_ARMOR = register(new Item("iron_horse_armor", builder().stackSize(1))); - public static final Item GOLDEN_HORSE_ARMOR = register(new Item("golden_horse_armor", builder().stackSize(1))); - public static final Item DIAMOND_HORSE_ARMOR = register(new Item("diamond_horse_armor", builder().stackSize(1))); - public static final Item LEATHER_HORSE_ARMOR = register(new DyeableHorseArmorItem("leather_horse_armor", builder().stackSize(1))); + public static final Item ARMOR_STAND = register(new Item("armor_stand", builder())); + public static final Item COPPER_HORSE_ARMOR = register(new Item("copper_horse_armor", builder())); + public static final Item IRON_HORSE_ARMOR = register(new Item("iron_horse_armor", builder())); + public static final Item GOLDEN_HORSE_ARMOR = register(new Item("golden_horse_armor", builder())); + public static final Item DIAMOND_HORSE_ARMOR = register(new Item("diamond_horse_armor", builder())); + public static final Item LEATHER_HORSE_ARMOR = register(new DyeableArmorItem("leather_horse_armor", builder())); public static final Item LEAD = register(new Item("lead", builder())); public static final Item NAME_TAG = register(new Item("name_tag", builder())); - public static final Item COMMAND_BLOCK_MINECART = register(new Item("command_block_minecart", builder().stackSize(1))); + public static final Item COMMAND_BLOCK_MINECART = register(new Item("command_block_minecart", builder())); public static final Item MUTTON = register(new Item("mutton", builder())); public static final Item COOKED_MUTTON = register(new Item("cooked_mutton", builder())); - public static final Item WHITE_BANNER = register(new BannerItem("white_banner", builder().stackSize(16))); - public static final Item ORANGE_BANNER = register(new BannerItem("orange_banner", builder().stackSize(16))); - public static final Item MAGENTA_BANNER = register(new BannerItem("magenta_banner", builder().stackSize(16))); - public static final Item LIGHT_BLUE_BANNER = register(new BannerItem("light_blue_banner", builder().stackSize(16))); - public static final Item YELLOW_BANNER = register(new BannerItem("yellow_banner", builder().stackSize(16))); - public static final Item LIME_BANNER = register(new BannerItem("lime_banner", builder().stackSize(16))); - public static final Item PINK_BANNER = register(new BannerItem("pink_banner", builder().stackSize(16))); - public static final Item GRAY_BANNER = register(new BannerItem("gray_banner", builder().stackSize(16))); - public static final Item LIGHT_GRAY_BANNER = register(new BannerItem("light_gray_banner", builder().stackSize(16))); - public static final Item CYAN_BANNER = register(new BannerItem("cyan_banner", builder().stackSize(16))); - public static final Item PURPLE_BANNER = register(new BannerItem("purple_banner", builder().stackSize(16))); - public static final Item BLUE_BANNER = register(new BannerItem("blue_banner", builder().stackSize(16))); - public static final Item BROWN_BANNER = register(new BannerItem("brown_banner", builder().stackSize(16))); - public static final Item GREEN_BANNER = register(new BannerItem("green_banner", builder().stackSize(16))); - public static final Item RED_BANNER = register(new BannerItem("red_banner", builder().stackSize(16))); - public static final Item BLACK_BANNER = register(new BannerItem("black_banner", builder().stackSize(16))); + public static final Item WHITE_BANNER = register(new BannerItem(builder(), Blocks.WHITE_BANNER, Blocks.WHITE_WALL_BANNER)); + public static final Item ORANGE_BANNER = register(new BannerItem(builder(), Blocks.ORANGE_BANNER, Blocks.ORANGE_WALL_BANNER)); + public static final Item MAGENTA_BANNER = register(new BannerItem(builder(), Blocks.MAGENTA_BANNER, Blocks.MAGENTA_WALL_BANNER)); + public static final Item LIGHT_BLUE_BANNER = register(new BannerItem(builder(), Blocks.LIGHT_BLUE_BANNER, Blocks.LIGHT_BLUE_WALL_BANNER)); + public static final Item YELLOW_BANNER = register(new BannerItem(builder(), Blocks.YELLOW_BANNER, Blocks.YELLOW_WALL_BANNER)); + public static final Item LIME_BANNER = register(new BannerItem(builder(), Blocks.LIME_BANNER, Blocks.LIME_WALL_BANNER)); + public static final Item PINK_BANNER = register(new BannerItem(builder(), Blocks.PINK_BANNER, Blocks.PINK_WALL_BANNER)); + public static final Item GRAY_BANNER = register(new BannerItem(builder(), Blocks.GRAY_BANNER, Blocks.GRAY_WALL_BANNER)); + public static final Item LIGHT_GRAY_BANNER = register(new BannerItem(builder(), Blocks.LIGHT_GRAY_BANNER, Blocks.LIGHT_GRAY_WALL_BANNER)); + public static final Item CYAN_BANNER = register(new BannerItem(builder(), Blocks.CYAN_BANNER, Blocks.CYAN_WALL_BANNER)); + public static final Item PURPLE_BANNER = register(new BannerItem(builder(), Blocks.PURPLE_BANNER, Blocks.PURPLE_WALL_BANNER)); + public static final Item BLUE_BANNER = register(new BannerItem(builder(), Blocks.BLUE_BANNER, Blocks.BLUE_WALL_BANNER)); + public static final Item BROWN_BANNER = register(new BannerItem(builder(), Blocks.BROWN_BANNER, Blocks.BROWN_WALL_BANNER)); + public static final Item GREEN_BANNER = register(new BannerItem(builder(), Blocks.GREEN_BANNER, Blocks.GREEN_WALL_BANNER)); + public static final Item RED_BANNER = register(new BannerItem(builder(), Blocks.RED_BANNER, Blocks.RED_WALL_BANNER)); + public static final Item BLACK_BANNER = register(new BannerItem(builder(), Blocks.BLACK_BANNER, Blocks.BLACK_WALL_BANNER)); public static final Item END_CRYSTAL = register(new Item("end_crystal", builder())); public static final Item CHORUS_FRUIT = register(new Item("chorus_fruit", builder())); public static final Item POPPED_CHORUS_FRUIT = register(new Item("popped_chorus_fruit", builder())); - public static final Item TORCHFLOWER_SEEDS = register(new BlockItem("torchflower_seeds", builder())); - public static final Item PITCHER_POD = register(new BlockItem("pitcher_pod", builder())); + public static final Item TORCHFLOWER_SEEDS = register(new BlockItem("torchflower_seeds", builder(), Blocks.TORCHFLOWER_CROP)); + public static final Item PITCHER_POD = register(new BlockItem("pitcher_pod", builder(), Blocks.PITCHER_CROP)); public static final Item BEETROOT = register(new Item("beetroot", builder())); - public static final Item BEETROOT_SEEDS = register(new BlockItem("beetroot_seeds", builder())); - public static final Item BEETROOT_SOUP = register(new Item("beetroot_soup", builder().stackSize(1))); + public static final Item BEETROOT_SEEDS = register(new BlockItem("beetroot_seeds", builder(), Blocks.BEETROOTS)); + public static final Item BEETROOT_SOUP = register(new Item("beetroot_soup", builder())); public static final Item DRAGON_BREATH = register(new Item("dragon_breath", builder())); - public static final Item SPLASH_POTION = register(new PotionItem("splash_potion", builder().stackSize(1))); + public static final Item SPLASH_POTION = register(new PotionItem("splash_potion", builder())); public static final Item SPECTRAL_ARROW = register(new Item("spectral_arrow", builder())); public static final Item TIPPED_ARROW = register(new TippedArrowItem("tipped_arrow", builder())); - public static final Item LINGERING_POTION = register(new PotionItem("lingering_potion", builder().stackSize(1))); - public static final Item SHIELD = register(new ShieldItem("shield", builder().stackSize(1).maxDamage(336))); - public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder().stackSize(1))); + public static final Item LINGERING_POTION = register(new PotionItem("lingering_potion", builder())); + public static final Item SHIELD = register(new ShieldItem("shield", builder())); + public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder())); public static final Item SHULKER_SHELL = register(new Item("shulker_shell", builder())); public static final Item IRON_NUGGET = register(new Item("iron_nugget", builder())); - public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder().stackSize(1))); - public static final Item DEBUG_STICK = register(new Item("debug_stick", builder().stackSize(1))); - public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder().stackSize(1))); - public static final Item MUSIC_DISC_CAT = register(new Item("music_disc_cat", builder().stackSize(1))); - public static final Item MUSIC_DISC_BLOCKS = register(new Item("music_disc_blocks", builder().stackSize(1))); - public static final Item MUSIC_DISC_CHIRP = register(new Item("music_disc_chirp", builder().stackSize(1))); - public static final Item MUSIC_DISC_FAR = register(new Item("music_disc_far", builder().stackSize(1))); - public static final Item MUSIC_DISC_MALL = register(new Item("music_disc_mall", builder().stackSize(1))); - public static final Item MUSIC_DISC_MELLOHI = register(new Item("music_disc_mellohi", builder().stackSize(1))); - public static final Item MUSIC_DISC_STAL = register(new Item("music_disc_stal", builder().stackSize(1))); - public static final Item MUSIC_DISC_STRAD = register(new Item("music_disc_strad", builder().stackSize(1))); - public static final Item MUSIC_DISC_WARD = register(new Item("music_disc_ward", builder().stackSize(1))); - public static final Item MUSIC_DISC_11 = register(new Item("music_disc_11", builder().stackSize(1))); - public static final Item MUSIC_DISC_WAIT = register(new Item("music_disc_wait", builder().stackSize(1))); - public static final Item MUSIC_DISC_OTHERSIDE = register(new Item("music_disc_otherside", builder().stackSize(1))); - public static final Item MUSIC_DISC_RELIC = register(new Item("music_disc_relic", builder().stackSize(1))); - public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder().stackSize(1))); - public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder().stackSize(1))); + public static final Item COPPER_NUGGET = register(new Item("copper_nugget", builder())); + public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder())); + public static final Item DEBUG_STICK = register(new Item("debug_stick", builder())); + public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder())); + public static final Item MUSIC_DISC_CAT = register(new Item("music_disc_cat", builder())); + public static final Item MUSIC_DISC_BLOCKS = register(new Item("music_disc_blocks", builder())); + public static final Item MUSIC_DISC_CHIRP = register(new Item("music_disc_chirp", builder())); + public static final Item MUSIC_DISC_CREATOR = register(new Item("music_disc_creator", builder())); + public static final Item MUSIC_DISC_CREATOR_MUSIC_BOX = register(new Item("music_disc_creator_music_box", builder())); + public static final Item MUSIC_DISC_FAR = register(new Item("music_disc_far", builder())); + public static final Item MUSIC_DISC_LAVA_CHICKEN = register(new Item("music_disc_lava_chicken", builder())); + public static final Item MUSIC_DISC_MALL = register(new Item("music_disc_mall", builder())); + public static final Item MUSIC_DISC_MELLOHI = register(new Item("music_disc_mellohi", builder())); + public static final Item MUSIC_DISC_STAL = register(new Item("music_disc_stal", builder())); + public static final Item MUSIC_DISC_STRAD = register(new Item("music_disc_strad", builder())); + public static final Item MUSIC_DISC_WARD = register(new Item("music_disc_ward", builder())); + public static final Item MUSIC_DISC_11 = register(new Item("music_disc_11", builder())); + public static final Item MUSIC_DISC_WAIT = register(new Item("music_disc_wait", builder())); + public static final Item MUSIC_DISC_OTHERSIDE = register(new Item("music_disc_otherside", builder())); + public static final Item MUSIC_DISC_RELIC = register(new Item("music_disc_relic", builder())); + public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder())); + public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder())); + public static final Item MUSIC_DISC_PRECIPICE = register(new Item("music_disc_precipice", builder())); + public static final Item MUSIC_DISC_TEARS = register(new Item("music_disc_tears", builder())); public static final Item DISC_FRAGMENT_5 = register(new Item("disc_fragment_5", builder())); - public static final Item TRIDENT = register(new Item("trident", builder().stackSize(1).attackDamage(9).maxDamage(250))); - public static final Item PHANTOM_MEMBRANE = register(new Item("phantom_membrane", builder())); + public static final Item TRIDENT = register(new Item("trident", builder().attackDamage(9.0))); public static final Item NAUTILUS_SHELL = register(new Item("nautilus_shell", builder())); public static final Item HEART_OF_THE_SEA = register(new Item("heart_of_the_sea", builder())); - public static final Item CROSSBOW = register(new CrossbowItem("crossbow", builder().stackSize(1).maxDamage(465))); - public static final Item SUSPICIOUS_STEW = register(new Item("suspicious_stew", builder().stackSize(1))); - public static final Item LOOM = register(new BlockItem("loom", builder())); - public static final Item FLOWER_BANNER_PATTERN = register(new Item("flower_banner_pattern", builder().stackSize(1))); - public static final Item CREEPER_BANNER_PATTERN = register(new Item("creeper_banner_pattern", builder().stackSize(1))); - public static final Item SKULL_BANNER_PATTERN = register(new Item("skull_banner_pattern", builder().stackSize(1))); - public static final Item MOJANG_BANNER_PATTERN = register(new Item("mojang_banner_pattern", builder().stackSize(1))); - public static final Item GLOBE_BANNER_PATTERN = register(new Item("globe_banner_pattern", builder().stackSize(1))); - public static final Item PIGLIN_BANNER_PATTERN = register(new Item("piglin_banner_pattern", builder().stackSize(1))); - public static final Item GOAT_HORN = register(new GoatHornItem("goat_horn", builder().stackSize(1))); - public static final Item COMPOSTER = register(new BlockItem("composter", builder())); - public static final Item BARREL = register(new ChestItem("barrel", builder())); - public static final Item SMOKER = register(new BlockItem("smoker", builder())); - public static final Item BLAST_FURNACE = register(new BlockItem("blast_furnace", builder())); - public static final Item CARTOGRAPHY_TABLE = register(new BlockItem("cartography_table", builder())); - public static final Item FLETCHING_TABLE = register(new BlockItem("fletching_table", builder())); - public static final Item GRINDSTONE = register(new BlockItem("grindstone", builder())); - public static final Item SMITHING_TABLE = register(new BlockItem("smithing_table", builder())); - public static final Item STONECUTTER = register(new BlockItem("stonecutter", builder())); - public static final Item BELL = register(new BlockItem("bell", builder())); - public static final Item LANTERN = register(new BlockItem("lantern", builder())); - public static final Item SOUL_LANTERN = register(new BlockItem("soul_lantern", builder())); - public static final Item SWEET_BERRIES = register(new BlockItem("sweet_berries", builder())); - public static final Item GLOW_BERRIES = register(new BlockItem("glow_berries", builder())); - public static final Item CAMPFIRE = register(new BlockItem("campfire", builder())); - public static final Item SOUL_CAMPFIRE = register(new BlockItem("soul_campfire", builder())); - public static final Item SHROOMLIGHT = register(new BlockItem("shroomlight", builder())); + public static final Item CROSSBOW = register(new CrossbowItem("crossbow", builder())); + public static final Item SUSPICIOUS_STEW = register(new Item("suspicious_stew", builder())); + public static final Item LOOM = register(new BlockItem(builder(), Blocks.LOOM)); + public static final Item FLOWER_BANNER_PATTERN = register(new Item("flower_banner_pattern", builder())); + public static final Item CREEPER_BANNER_PATTERN = register(new Item("creeper_banner_pattern", builder())); + public static final Item SKULL_BANNER_PATTERN = register(new Item("skull_banner_pattern", builder())); + public static final Item MOJANG_BANNER_PATTERN = register(new Item("mojang_banner_pattern", builder())); + public static final Item GLOBE_BANNER_PATTERN = register(new Item("globe_banner_pattern", builder())); + public static final Item PIGLIN_BANNER_PATTERN = register(new Item("piglin_banner_pattern", builder())); + public static final Item FLOW_BANNER_PATTERN = register(new Item("flow_banner_pattern", builder())); + public static final Item GUSTER_BANNER_PATTERN = register(new Item("guster_banner_pattern", builder())); + public static final Item FIELD_MASONED_BANNER_PATTERN = register(new Item("field_masoned_banner_pattern", builder())); + public static final Item BORDURE_INDENTED_BANNER_PATTERN = register(new Item("bordure_indented_banner_pattern", builder())); + public static final Item GOAT_HORN = register(new GoatHornItem("goat_horn", builder())); + public static final Item COMPOSTER = register(new BlockItem(builder(), Blocks.COMPOSTER)); + public static final Item BARREL = register(new BlockItem(builder(), Blocks.BARREL)); + public static final Item SMOKER = register(new BlockItem(builder(), Blocks.SMOKER)); + public static final Item BLAST_FURNACE = register(new BlockItem(builder(), Blocks.BLAST_FURNACE)); + public static final Item CARTOGRAPHY_TABLE = register(new BlockItem(builder(), Blocks.CARTOGRAPHY_TABLE)); + public static final Item FLETCHING_TABLE = register(new BlockItem(builder(), Blocks.FLETCHING_TABLE)); + public static final Item GRINDSTONE = register(new BlockItem(builder(), Blocks.GRINDSTONE)); + public static final Item SMITHING_TABLE = register(new BlockItem(builder(), Blocks.SMITHING_TABLE)); + public static final Item STONECUTTER = register(new BlockItem(builder(), Blocks.STONECUTTER)); + public static final Item BELL = register(new BlockItem(builder(), Blocks.BELL)); + public static final Item LANTERN = register(new BlockItem(builder(), Blocks.LANTERN)); + public static final Item SOUL_LANTERN = register(new BlockItem(builder(), Blocks.SOUL_LANTERN)); + public static final Item COPPER_LANTERN = register(new BlockItem(builder(), Blocks.COPPER_LANTERN)); + public static final Item EXPOSED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_LANTERN)); + public static final Item WEATHERED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_LANTERN)); + public static final Item OXIDIZED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_LANTERN)); + public static final Item WAXED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_COPPER_LANTERN)); + public static final Item WAXED_EXPOSED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_LANTERN)); + public static final Item WAXED_WEATHERED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_LANTERN)); + public static final Item WAXED_OXIDIZED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_LANTERN)); + public static final Item SWEET_BERRIES = register(new BlockItem("sweet_berries", builder(), Blocks.SWEET_BERRY_BUSH)); + public static final Item GLOW_BERRIES = register(new BlockItem("glow_berries", builder(), Blocks.CAVE_VINES)); + public static final Item CAMPFIRE = register(new BlockItem(builder(), Blocks.CAMPFIRE)); + public static final Item SOUL_CAMPFIRE = register(new BlockItem(builder(), Blocks.SOUL_CAMPFIRE)); + public static final Item SHROOMLIGHT = register(new BlockItem(builder(), Blocks.SHROOMLIGHT)); public static final Item HONEYCOMB = register(new Item("honeycomb", builder())); - public static final Item BEE_NEST = register(new BlockItem("bee_nest", builder())); - public static final Item BEEHIVE = register(new BlockItem("beehive", builder())); - public static final Item HONEY_BOTTLE = register(new Item("honey_bottle", builder().stackSize(16))); - public static final Item HONEYCOMB_BLOCK = register(new BlockItem("honeycomb_block", builder())); - public static final Item LODESTONE = register(new BlockItem("lodestone", builder())); - public static final Item CRYING_OBSIDIAN = register(new BlockItem("crying_obsidian", builder())); - public static final Item BLACKSTONE = register(new BlockItem("blackstone", builder())); - public static final Item BLACKSTONE_SLAB = register(new BlockItem("blackstone_slab", builder())); - public static final Item BLACKSTONE_STAIRS = register(new BlockItem("blackstone_stairs", builder())); - public static final Item GILDED_BLACKSTONE = register(new BlockItem("gilded_blackstone", builder())); - public static final Item POLISHED_BLACKSTONE = register(new BlockItem("polished_blackstone", builder())); - public static final Item POLISHED_BLACKSTONE_SLAB = register(new BlockItem("polished_blackstone_slab", builder())); - public static final Item POLISHED_BLACKSTONE_STAIRS = register(new BlockItem("polished_blackstone_stairs", builder())); - public static final Item CHISELED_POLISHED_BLACKSTONE = register(new BlockItem("chiseled_polished_blackstone", builder())); - public static final Item POLISHED_BLACKSTONE_BRICKS = register(new BlockItem("polished_blackstone_bricks", builder())); - public static final Item POLISHED_BLACKSTONE_BRICK_SLAB = register(new BlockItem("polished_blackstone_brick_slab", builder())); - public static final Item POLISHED_BLACKSTONE_BRICK_STAIRS = register(new BlockItem("polished_blackstone_brick_stairs", builder())); - public static final Item CRACKED_POLISHED_BLACKSTONE_BRICKS = register(new BlockItem("cracked_polished_blackstone_bricks", builder())); - public static final Item RESPAWN_ANCHOR = register(new BlockItem("respawn_anchor", builder())); - public static final Item CANDLE = register(new BlockItem("candle", builder())); - public static final Item WHITE_CANDLE = register(new BlockItem("white_candle", builder())); - public static final Item ORANGE_CANDLE = register(new BlockItem("orange_candle", builder())); - public static final Item MAGENTA_CANDLE = register(new BlockItem("magenta_candle", builder())); - public static final Item LIGHT_BLUE_CANDLE = register(new BlockItem("light_blue_candle", builder())); - public static final Item YELLOW_CANDLE = register(new BlockItem("yellow_candle", builder())); - public static final Item LIME_CANDLE = register(new BlockItem("lime_candle", builder())); - public static final Item PINK_CANDLE = register(new BlockItem("pink_candle", builder())); - public static final Item GRAY_CANDLE = register(new BlockItem("gray_candle", builder())); - public static final Item LIGHT_GRAY_CANDLE = register(new BlockItem("light_gray_candle", builder())); - public static final Item CYAN_CANDLE = register(new BlockItem("cyan_candle", builder())); - public static final Item PURPLE_CANDLE = register(new BlockItem("purple_candle", builder())); - public static final Item BLUE_CANDLE = register(new BlockItem("blue_candle", builder())); - public static final Item BROWN_CANDLE = register(new BlockItem("brown_candle", builder())); - public static final Item GREEN_CANDLE = register(new BlockItem("green_candle", builder())); - public static final Item RED_CANDLE = register(new BlockItem("red_candle", builder())); - public static final Item BLACK_CANDLE = register(new BlockItem("black_candle", builder())); - public static final Item SMALL_AMETHYST_BUD = register(new BlockItem("small_amethyst_bud", builder())); - public static final Item MEDIUM_AMETHYST_BUD = register(new BlockItem("medium_amethyst_bud", builder())); - public static final Item LARGE_AMETHYST_BUD = register(new BlockItem("large_amethyst_bud", builder())); - public static final Item AMETHYST_CLUSTER = register(new BlockItem("amethyst_cluster", builder())); - public static final Item POINTED_DRIPSTONE = register(new BlockItem("pointed_dripstone", builder())); - public static final Item OCHRE_FROGLIGHT = register(new BlockItem("ochre_froglight", builder())); - public static final Item VERDANT_FROGLIGHT = register(new BlockItem("verdant_froglight", builder())); - public static final Item PEARLESCENT_FROGLIGHT = register(new BlockItem("pearlescent_froglight", builder())); - public static final Item FROGSPAWN = register(new BlockItem("frogspawn", builder())); + public static final Item BEE_NEST = register(new BlockItem(builder(), Blocks.BEE_NEST)); + public static final Item BEEHIVE = register(new BlockItem(builder(), Blocks.BEEHIVE)); + public static final Item HONEY_BOTTLE = register(new Item("honey_bottle", builder())); + public static final Item HONEYCOMB_BLOCK = register(new BlockItem(builder(), Blocks.HONEYCOMB_BLOCK)); + public static final Item LODESTONE = register(new BlockItem(builder(), Blocks.LODESTONE)); + public static final Item CRYING_OBSIDIAN = register(new BlockItem(builder(), Blocks.CRYING_OBSIDIAN)); + public static final Item BLACKSTONE = register(new BlockItem(builder(), Blocks.BLACKSTONE)); + public static final Item BLACKSTONE_SLAB = register(new BlockItem(builder(), Blocks.BLACKSTONE_SLAB)); + public static final Item BLACKSTONE_STAIRS = register(new BlockItem(builder(), Blocks.BLACKSTONE_STAIRS)); + public static final Item GILDED_BLACKSTONE = register(new BlockItem(builder(), Blocks.GILDED_BLACKSTONE)); + public static final Item POLISHED_BLACKSTONE = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE)); + public static final Item POLISHED_BLACKSTONE_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_SLAB)); + public static final Item POLISHED_BLACKSTONE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_STAIRS)); + public static final Item CHISELED_POLISHED_BLACKSTONE = register(new BlockItem(builder(), Blocks.CHISELED_POLISHED_BLACKSTONE)); + public static final Item POLISHED_BLACKSTONE_BRICKS = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_BRICKS)); + public static final Item POLISHED_BLACKSTONE_BRICK_SLAB = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_BRICK_SLAB)); + public static final Item POLISHED_BLACKSTONE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_BLACKSTONE_BRICK_STAIRS)); + public static final Item CRACKED_POLISHED_BLACKSTONE_BRICKS = register(new BlockItem(builder(), Blocks.CRACKED_POLISHED_BLACKSTONE_BRICKS)); + public static final Item RESPAWN_ANCHOR = register(new BlockItem(builder(), Blocks.RESPAWN_ANCHOR)); + public static final Item CANDLE = register(new BlockItem(builder(), Blocks.CANDLE)); + public static final Item WHITE_CANDLE = register(new BlockItem(builder(), Blocks.WHITE_CANDLE)); + public static final Item ORANGE_CANDLE = register(new BlockItem(builder(), Blocks.ORANGE_CANDLE)); + public static final Item MAGENTA_CANDLE = register(new BlockItem(builder(), Blocks.MAGENTA_CANDLE)); + public static final Item LIGHT_BLUE_CANDLE = register(new BlockItem(builder(), Blocks.LIGHT_BLUE_CANDLE)); + public static final Item YELLOW_CANDLE = register(new BlockItem(builder(), Blocks.YELLOW_CANDLE)); + public static final Item LIME_CANDLE = register(new BlockItem(builder(), Blocks.LIME_CANDLE)); + public static final Item PINK_CANDLE = register(new BlockItem(builder(), Blocks.PINK_CANDLE)); + public static final Item GRAY_CANDLE = register(new BlockItem(builder(), Blocks.GRAY_CANDLE)); + public static final Item LIGHT_GRAY_CANDLE = register(new BlockItem(builder(), Blocks.LIGHT_GRAY_CANDLE)); + public static final Item CYAN_CANDLE = register(new BlockItem(builder(), Blocks.CYAN_CANDLE)); + public static final Item PURPLE_CANDLE = register(new BlockItem(builder(), Blocks.PURPLE_CANDLE)); + public static final Item BLUE_CANDLE = register(new BlockItem(builder(), Blocks.BLUE_CANDLE)); + public static final Item BROWN_CANDLE = register(new BlockItem(builder(), Blocks.BROWN_CANDLE)); + public static final Item GREEN_CANDLE = register(new BlockItem(builder(), Blocks.GREEN_CANDLE)); + public static final Item RED_CANDLE = register(new BlockItem(builder(), Blocks.RED_CANDLE)); + public static final Item BLACK_CANDLE = register(new BlockItem(builder(), Blocks.BLACK_CANDLE)); + public static final Item SMALL_AMETHYST_BUD = register(new BlockItem(builder(), Blocks.SMALL_AMETHYST_BUD)); + public static final Item MEDIUM_AMETHYST_BUD = register(new BlockItem(builder(), Blocks.MEDIUM_AMETHYST_BUD)); + public static final Item LARGE_AMETHYST_BUD = register(new BlockItem(builder(), Blocks.LARGE_AMETHYST_BUD)); + public static final Item AMETHYST_CLUSTER = register(new BlockItem(builder(), Blocks.AMETHYST_CLUSTER)); + public static final Item POINTED_DRIPSTONE = register(new BlockItem(builder(), Blocks.POINTED_DRIPSTONE)); + public static final Item OCHRE_FROGLIGHT = register(new BlockItem(builder(), Blocks.OCHRE_FROGLIGHT)); + public static final Item VERDANT_FROGLIGHT = register(new BlockItem(builder(), Blocks.VERDANT_FROGLIGHT)); + public static final Item PEARLESCENT_FROGLIGHT = register(new BlockItem(builder(), Blocks.PEARLESCENT_FROGLIGHT)); + public static final Item FROGSPAWN = register(new BlockItem(builder(), Blocks.FROGSPAWN)); public static final Item ECHO_SHARD = register(new Item("echo_shard", builder())); - public static final Item BRUSH = register(new Item("brush", builder().stackSize(1).maxDamage(64))); + public static final Item BRUSH = register(new Item("brush", builder())); public static final Item NETHERITE_UPGRADE_SMITHING_TEMPLATE = register(new Item("netherite_upgrade_smithing_template", builder())); public static final Item SENTRY_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("sentry_armor_trim_smithing_template", builder())); public static final Item DUNE_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("dune_armor_trim_smithing_template", builder())); @@ -1312,6 +1492,8 @@ public final class Items { public static final Item SILENCE_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("silence_armor_trim_smithing_template", builder())); public static final Item RAISER_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("raiser_armor_trim_smithing_template", builder())); public static final Item HOST_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("host_armor_trim_smithing_template", builder())); + public static final Item FLOW_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("flow_armor_trim_smithing_template", builder())); + public static final Item BOLT_ARMOR_TRIM_SMITHING_TEMPLATE = register(new Item("bolt_armor_trim_smithing_template", builder())); public static final Item ANGLER_POTTERY_SHERD = register(new Item("angler_pottery_sherd", builder())); public static final Item ARCHER_POTTERY_SHERD = register(new Item("archer_pottery_sherd", builder())); public static final Item ARMS_UP_POTTERY_SHERD = register(new Item("arms_up_pottery_sherd", builder())); @@ -1320,7 +1502,9 @@ public final class Items { public static final Item BURN_POTTERY_SHERD = register(new Item("burn_pottery_sherd", builder())); public static final Item DANGER_POTTERY_SHERD = register(new Item("danger_pottery_sherd", builder())); public static final Item EXPLORER_POTTERY_SHERD = register(new Item("explorer_pottery_sherd", builder())); + public static final Item FLOW_POTTERY_SHERD = register(new Item("flow_pottery_sherd", builder())); public static final Item FRIEND_POTTERY_SHERD = register(new Item("friend_pottery_sherd", builder())); + public static final Item GUSTER_POTTERY_SHERD = register(new Item("guster_pottery_sherd", builder())); public static final Item HEART_POTTERY_SHERD = register(new Item("heart_pottery_sherd", builder())); public static final Item HEARTBREAK_POTTERY_SHERD = register(new Item("heartbreak_pottery_sherd", builder())); public static final Item HOWL_POTTERY_SHERD = register(new Item("howl_pottery_sherd", builder())); @@ -1328,28 +1512,48 @@ public final class Items { public static final Item MOURNER_POTTERY_SHERD = register(new Item("mourner_pottery_sherd", builder())); public static final Item PLENTY_POTTERY_SHERD = register(new Item("plenty_pottery_sherd", builder())); public static final Item PRIZE_POTTERY_SHERD = register(new Item("prize_pottery_sherd", builder())); + public static final Item SCRAPE_POTTERY_SHERD = register(new Item("scrape_pottery_sherd", builder())); public static final Item SHEAF_POTTERY_SHERD = register(new Item("sheaf_pottery_sherd", builder())); public static final Item SHELTER_POTTERY_SHERD = register(new Item("shelter_pottery_sherd", builder())); public static final Item SKULL_POTTERY_SHERD = register(new Item("skull_pottery_sherd", builder())); public static final Item SNORT_POTTERY_SHERD = register(new Item("snort_pottery_sherd", builder())); - public static final Item COPPER_GRATE = register(new BlockItem("copper_grate", builder())); - public static final Item EXPOSED_COPPER_GRATE = register(new BlockItem("exposed_copper_grate", builder())); - public static final Item WEATHERED_COPPER_GRATE = register(new BlockItem("weathered_copper_grate", builder())); - public static final Item OXIDIZED_COPPER_GRATE = register(new BlockItem("oxidized_copper_grate", builder())); - public static final Item WAXED_COPPER_GRATE = register(new BlockItem("waxed_copper_grate", builder())); - public static final Item WAXED_EXPOSED_COPPER_GRATE = register(new BlockItem("waxed_exposed_copper_grate", builder())); - public static final Item WAXED_WEATHERED_COPPER_GRATE = register(new BlockItem("waxed_weathered_copper_grate", builder())); - public static final Item WAXED_OXIDIZED_COPPER_GRATE = register(new BlockItem("waxed_oxidized_copper_grate", builder())); - public static final Item COPPER_BULB = register(new BlockItem("copper_bulb", builder())); - public static final Item EXPOSED_COPPER_BULB = register(new BlockItem("exposed_copper_bulb", builder())); - public static final Item WEATHERED_COPPER_BULB = register(new BlockItem("weathered_copper_bulb", builder())); - public static final Item OXIDIZED_COPPER_BULB = register(new BlockItem("oxidized_copper_bulb", builder())); - public static final Item WAXED_COPPER_BULB = register(new BlockItem("waxed_copper_bulb", builder())); - public static final Item WAXED_EXPOSED_COPPER_BULB = register(new BlockItem("waxed_exposed_copper_bulb", builder())); - public static final Item WAXED_WEATHERED_COPPER_BULB = register(new BlockItem("waxed_weathered_copper_bulb", builder())); - public static final Item WAXED_OXIDIZED_COPPER_BULB = register(new BlockItem("waxed_oxidized_copper_bulb", builder())); - public static final Item TRIAL_SPAWNER = register(new BlockItem("trial_spawner", builder())); + public static final Item COPPER_GRATE = register(new BlockItem(builder(), Blocks.COPPER_GRATE)); + public static final Item EXPOSED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_GRATE)); + public static final Item WEATHERED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_GRATE)); + public static final Item OXIDIZED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_GRATE)); + public static final Item WAXED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.WAXED_COPPER_GRATE)); + public static final Item WAXED_EXPOSED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_GRATE)); + public static final Item WAXED_WEATHERED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_GRATE)); + public static final Item WAXED_OXIDIZED_COPPER_GRATE = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_GRATE)); + public static final Item COPPER_BULB = register(new BlockItem(builder(), Blocks.COPPER_BULB)); + public static final Item EXPOSED_COPPER_BULB = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_BULB)); + public static final Item WEATHERED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_BULB)); + public static final Item OXIDIZED_COPPER_BULB = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_BULB)); + public static final Item WAXED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_COPPER_BULB)); + public static final Item WAXED_EXPOSED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_BULB)); + public static final Item WAXED_WEATHERED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_BULB)); + public static final Item WAXED_OXIDIZED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_BULB)); + public static final Item COPPER_CHEST = register(new BlockItem(builder(), Blocks.COPPER_CHEST)); + public static final Item EXPOSED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_CHEST)); + public static final Item WEATHERED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_CHEST)); + public static final Item OXIDIZED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_CHEST)); + public static final Item WAXED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_COPPER_CHEST)); + public static final Item WAXED_EXPOSED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_CHEST)); + public static final Item WAXED_WEATHERED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_CHEST)); + public static final Item WAXED_OXIDIZED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_CHEST)); + public static final Item COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.COPPER_GOLEM_STATUE)); + public static final Item EXPOSED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_GOLEM_STATUE)); + public static final Item WEATHERED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_GOLEM_STATUE)); + public static final Item OXIDIZED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_GOLEM_STATUE)); + public static final Item WAXED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_COPPER_GOLEM_STATUE)); + public static final Item WAXED_EXPOSED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_GOLEM_STATUE)); + public static final Item WAXED_WEATHERED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_GOLEM_STATUE)); + public static final Item WAXED_OXIDIZED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_GOLEM_STATUE)); + public static final Item TRIAL_SPAWNER = register(new BlockItem(builder(), Blocks.TRIAL_SPAWNER)); public static final Item TRIAL_KEY = register(new Item("trial_key", builder())); + public static final Item OMINOUS_TRIAL_KEY = register(new Item("ominous_trial_key", builder())); + public static final Item VAULT = register(new BlockItem(builder(), Blocks.VAULT)); + public static final Item OMINOUS_BOTTLE = register(new OminousBottleItem("ominous_bottle", builder())); public static final int AIR_ID = AIR.javaId(); @@ -1359,11 +1563,7 @@ public final class Items { public static T register(T item, int id) { item.setJavaId(id); - // This makes sure that the array is large enough to put the java item at the correct location - if (Registries.JAVA_ITEMS.get().size() <= id) { - Registries.JAVA_ITEMS.get().addAll(Collections.nCopies(id - Registries.JAVA_ITEMS.get().size() + 1, AIR)); - } - Registries.JAVA_ITEMS.get().set(id, item); + Registries.JAVA_ITEMS.registerWithAnyIndex(id, item, AIR); Registries.JAVA_ITEM_IDENTIFIERS.register(item.javaIdentifier(), item); return item; } diff --git a/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java b/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java new file mode 100644 index 000000000..2fa9af789 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item; + +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay; + +@FunctionalInterface +public interface TooltipOptions { + + TooltipOptions ALL_SHOWN = component -> true; + + TooltipOptions ALL_HIDDEN = component -> false; + + boolean showInTooltip(DataComponentType component); + + static TooltipOptions fromComponents(DataComponents components) { + TooltipDisplay display = components.get(DataComponentTypes.TOOLTIP_DISPLAY); + if (display == null) { + return ALL_SHOWN; + } else if (display.hideTooltip()) { + return ALL_HIDDEN; + } else if (display.hiddenComponents().isEmpty()) { + return ALL_SHOWN; + } + + return component -> !display.hiddenComponents().contains(component); + } + + static boolean hideTooltip(DataComponents components) { + TooltipDisplay display = components.get(DataComponentTypes.TOOLTIP_DISPLAY); + return display != null && display.hideTooltip(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java b/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java new file mode 100644 index 000000000..437fc5391 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.components; + +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.NonNull; + +@Getter +public enum Rarity { + COMMON("common", 'f'), + UNCOMMON("uncommon", 'e'), + RARE("rare", 'b'), + EPIC("epic", 'd'); + + private final String name; + private final char color; + + Rarity(final String name, char chatColor) { + this.name = name; + this.color = chatColor; + } + + private static final Rarity[] VALUES = values(); + + public static @NonNull Rarity fromId(Integer id) { + return VALUES.length > id ? VALUES[id] : VALUES[0]; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java b/core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java deleted file mode 100644 index a8832df1e..000000000 --- a/core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.item.components; - -import com.google.common.base.Suppliers; -import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.type.Item; - -import java.util.Collections; -import java.util.Locale; -import java.util.Set; -import java.util.function.Supplier; - -public enum ToolTier { - WOODEN(2, () -> Set.of(Items.OAK_PLANKS, Items.SPRUCE_PLANKS, Items.BIRCH_PLANKS, Items.JUNGLE_PLANKS, Items.ACACIA_PLANKS, Items.DARK_OAK_PLANKS, Items.CRIMSON_PLANKS, Items.WARPED_PLANKS, Items.MANGROVE_PLANKS)), // PLANKS tag // TODO ? - STONE(4, () -> Set.of(Items.COBBLESTONE, Items.BLACKSTONE, Items.COBBLED_DEEPSLATE)), // STONE_TOOL_MATERIALS tag - IRON(6, () -> Collections.singleton(Items.IRON_INGOT)), - GOLDEN(12, () -> Collections.singleton(Items.GOLD_INGOT)), - DIAMOND(8, () -> Collections.singleton(Items.DIAMOND)), - NETHERITE(9, () -> Collections.singleton(Items.NETHERITE_INGOT)); - - private static final ToolTier[] VALUES = values(); - - private final int speed; - private final Supplier> repairIngredients; - - ToolTier(int speed, Supplier> repairIngredients) { - this.speed = speed; - // Lazily initialize as this will likely be called as items are loading - this.repairIngredients = Suppliers.memoize(repairIngredients::get); - } - - public Set getRepairIngredients() { - return repairIngredients.get(); - } - - @Override - public String toString() { - return this.name().toLowerCase(Locale.ROOT); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java new file mode 100644 index 000000000..8ad58c9c3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.enchantment; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.inventory.item.BedrockEnchantment; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.session.cache.tags.GeyserHolderSet; +import org.geysermc.geyser.translator.text.MessageTranslator; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @param description only populated if {@link #bedrockEnchantment()} is null. + * @param anvilCost also as a rarity multiplier + */ +public record Enchantment(Set effects, + GeyserHolderSet supportedItems, + int maxLevel, + String description, + int anvilCost, + GeyserHolderSet exclusiveSet, + @Nullable BedrockEnchantment bedrockEnchantment) { + + public static Enchantment read(RegistryEntryContext context) { + NbtMap data = context.data(); + Set effects = readEnchantmentComponents(data.getCompound("effects")); + + GeyserHolderSet supportedItems = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ITEM, data.get("supported_items")); + + int maxLevel = data.getInt("max_level"); + int anvilCost = data.getInt("anvil_cost"); + + GeyserHolderSet exclusiveSet = GeyserHolderSet.readHolderSet(JavaRegistries.ENCHANTMENT, data.get("exclusive_set"), context::getNetworkId); + + BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(context.id().asString()); + + String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(context.session(), data) : null; + + return new Enchantment(effects, supportedItems, maxLevel, description, anvilCost, exclusiveSet, bedrockEnchantment); + } + + private static Set readEnchantmentComponents(NbtMap effects) { + Set components = new HashSet<>(); + for (Map.Entry entry : effects.entrySet()) { + switch (entry.getKey()) { + case "minecraft:prevent_armor_change" -> components.add(EnchantmentComponent.PREVENT_ARMOR_CHANGE); + } + } + return Set.copyOf(components); // Also ensures any empty sets are consolidated + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/EnchantmentComponent.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/EnchantmentComponent.java new file mode 100644 index 000000000..66d110f98 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/EnchantmentComponent.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.enchantment; + +public class EnchantmentComponent { + /** + * Singleton with no additional data + */ + public static final EnchantmentComponent PREVENT_ARMOR_CHANGE = new EnchantmentComponent(); +} diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java new file mode 100644 index 000000000..913312e8e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.KeybindComponent; +import net.kyori.adventure.text.NBTComponent; +import net.kyori.adventure.text.ScoreComponent; +import net.kyori.adventure.text.SelectorComponent; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import java.util.function.Supplier; + +/** + * This interface contains various {@link MinecraftHasher}s used to encode properties of {@link Component}s. Usually, you'll only need {@link ComponentHasher#COMPONENT}. + */ +public interface ComponentHasher { + + MinecraftHasher COMPONENT = MinecraftHasher.lazyInitialize(new Supplier<>() { + @Override + public MinecraftHasher get() { + return ACTUAL_COMPONENT; + } + }); + + MinecraftHasher NAMED_COLOR = MinecraftHasher.STRING.cast(NamedTextColor::toString); + + MinecraftHasher DIRECT_COLOR = MinecraftHasher.STRING.cast(TextColor::asHexString); + + MinecraftHasher COLOR = (value, encoder) -> { + if (value instanceof NamedTextColor named) { + return NAMED_COLOR.hash(named, encoder); + } + return DIRECT_COLOR.hash(value, encoder); + }; + + MinecraftHasher DECORATION_STATE = MinecraftHasher.BOOL.cast(state -> switch (state) { + case NOT_SET -> null; // Should never happen since we're using .optional() with NOT_SET as default value below + case FALSE -> false; + case TRUE -> true; + }); + + MinecraftHasher CLICK_EVENT_ACTION = MinecraftHasher.STRING.cast(ClickEvent.Action::toString); + + MinecraftHasher CLICK_EVENT_TEXT_PAYLOAD = MinecraftHasher.STRING.cast(ClickEvent.Payload.Text::value); + + MinecraftHasher CLICK_EVENT_INT_PAYLOAD = MinecraftHasher.INT.cast(ClickEvent.Payload.Int::integer); + + // Both dialog and custom click event types are not possible to hash within Geyser, because: + // - Dialog has no proper implementation within Adventure yet. Once it does, we'd probably only hash dialog holders with a resource location, because setting up + // hashers for the full dialog structure can be a lot of work. + // - Custom uses BinaryTagHolder to store NBT data, which essentially only stores a string representation. This won't work with hashing, we need a NBT tag to hash. + MinecraftHasher CLICK_EVENT = CLICK_EVENT_ACTION.dispatch("action", ClickEvent::action, action -> switch (action) { + case OPEN_URL -> builder -> builder.accept("url", CLICK_EVENT_TEXT_PAYLOAD, event -> (ClickEvent.Payload.Text) event.payload()); + case OPEN_FILE -> builder -> builder.accept("path", CLICK_EVENT_TEXT_PAYLOAD, event -> (ClickEvent.Payload.Text) event.payload()); + case RUN_COMMAND, SUGGEST_COMMAND -> builder -> builder.accept("command", CLICK_EVENT_TEXT_PAYLOAD, event -> (ClickEvent.Payload.Text) event.payload()); + case CHANGE_PAGE -> builder -> builder.accept("page", CLICK_EVENT_INT_PAYLOAD, event -> (ClickEvent.Payload.Int) event.payload()); + case COPY_TO_CLIPBOARD -> builder -> builder.accept("value", CLICK_EVENT_TEXT_PAYLOAD, event -> (ClickEvent.Payload.Text) event.payload()); + case SHOW_DIALOG, CUSTOM -> MapBuilder.unit(); + }); + + MinecraftHasher> HOVER_EVENT_ACTION = MinecraftHasher.STRING.cast(HoverEvent.Action::toString); + + MinecraftHasher> HOVER_EVENT = HOVER_EVENT_ACTION.dispatch("action", HoverEvent::action, action -> { + if (action == HoverEvent.Action.SHOW_TEXT) { + return builder -> builder.accept("value", COMPONENT, event -> (Component) event.value()); + } else if (action == HoverEvent.Action.SHOW_ITEM) { + return builder -> builder + .accept("id", MinecraftHasher.KEY, event -> ((HoverEvent.ShowItem) event.value()).item()) + .accept("count", MinecraftHasher.INT, event -> ((HoverEvent.ShowItem) event.value()).count()); // Data components are probably not possible + } + return builder -> builder + .accept("id", MinecraftHasher.KEY, event -> ((HoverEvent.ShowEntity) event.value()).type()) + .accept("uuid", MinecraftHasher.UUID, event -> ((HoverEvent.ShowEntity) event.value()).id()) + .optionalNullable("name", COMPONENT, event -> ((HoverEvent.ShowEntity) event.value()).name()); + }); + + MapBuilder