diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..198db6e8a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,42 @@
+[*]
+charset=utf-8
+end_of_line=lf
+insert_final_newline=true
+indent_style=space
+indent_size=4
+ij_any_block_comment_add_space = false
+ij_any_block_comment_at_first_column = false
+ij_any_line_comment_at_first_column = false
+ij_any_line_comment_add_space = true
+
+[*.tiny]
+indent_style=tab
+
+[*.bat]
+end_of_line=crlf
+
+[*.yml]
+indent_size=2
+
+[*.patch]
+trim_trailing_whitespace=false
+
+[*.java]
+ij_continuation_indent_size = 4
+ij_java_class_count_to_use_import_on_demand = 999999
+ij_java_insert_inner_class_imports = false
+ij_java_names_count_to_use_import_on_demand = 999999
+ij_java_imports_layout = *,|,$*
+ij_java_generate_final_locals = true
+ij_java_generate_final_parameters = true
+ij_java_method_parameters_new_line_after_left_paren = true
+ij_java_method_parameters_right_paren_on_new_line = true
+
+[test-plugin/**/*.java]
+ij_java_use_fq_class_names = false
+
+[Paper-Server/src/main/resources/data/**/*.json]
+indent_size = 2
+
+[paper-api-generator/generated/**/*.java]
+ij_java_imports_layout = $*,|,*
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..2ebd41bdf
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+* text=auto eol=lf
+
+*.sh text eol=lf
+gradlew text eol=lf
+*.bat text eol=crlf
+
+*.jar binary
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..ef9fc1fe9
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+* @PaperMC/paper-maintainers
+/.github/CODEOWNERS @PaperMC/core-team
diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml
new file mode 100644
index 000000000..a7e347172
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/ideas.yml
@@ -0,0 +1,42 @@
+labels: ["status: needs triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for filling out a feature request for Paper! Please be as detailed as possible so that we may consider and review the request easier.
+ We ask that you search all the issues to avoid a duplicate feature request. If one exists, please reply if you have anything to add.
+ Before requesting a new feature, please make sure you are using the latest version and that the feature you are requesting is not already in Paper.
+
+ - type: textarea
+ attributes:
+ label: Is your feature request related to a problem?
+ description: Please give some context for this request. Why do you want it added?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe the solution you'd like.
+ description: A clear and concise description of what you want.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe alternatives you've considered.
+ description: List any alternatives you might have tried to get the feature you want.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Other
+ description: Add any other context or screenshots about the feature request below.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this feature request, please search our issue tracker to ensure your feature has not
+ already been requested.
diff --git a/.github/ISSUE_TEMPLATE/bug-or-incompatibility.yml b/.github/ISSUE_TEMPLATE/bug-or-incompatibility.yml
new file mode 100644
index 000000000..5f25e871c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-or-incompatibility.yml
@@ -0,0 +1,79 @@
+name: "🐛 Bug or Incompatibility"
+description: Report issues related to unexpected behavior or vanilla/plugin incompatibility.
+type: "Bug"
+labels:
+ - "status: needs triage"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this issue, please ensure the following:
+
+ 1. You are using the latest version of Paper, available on our [our downloads page](https://papermc.io/downloads/paper).
+ 2. You have searched to confirm there isn’t [an existing open issue](https://github.com/PaperMC/Paper/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) on this topic.
+ 3. Your version of Minecraft is supported by Paper.
+
+ If you're unsure whether you've encountered a bug, feel free to ask in the `#paper-help` channel on our
+ [Discord](https://discord.gg/papermc).
+
+ - type: textarea
+ attributes:
+ label: Expected behavior
+ description: What you expected to see.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Observed/Actual behavior
+ description: What you actually saw.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Steps/models to reproduce
+ description: This may include a build schematic, a video, or detailed instructions to help reconstruct the issue.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Plugin and Datapack List
+ description: |
+ All plugins and datapacks running on your server.
+ To list plugins, run `/plugins`. For datapacks, run `/datapack list`.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Paper version
+ description: |
+ Run `/version` on your server and **paste** the full, unmodified output here.
+ "latest" is *not* a version; we require the output of `/version` so we can adequately track down the issue.
+ Additionally, do NOT provide a screenshot, you MUST paste the entire output.
+
+ Example
+
+ ```
+ > version
+ [20:34:42 INFO]: Checking version, please wait...
+ [20:34:42 INFO]: This server is running Paper version 1.21-105-master@7e91a2c (2024-07-20T21:04:31Z) (Implementing API version 1.21-R0.1-SNAPSHOT)
+ [20:34:42 INFO]: You are running the latest version
+ [20:34:42 INFO]: Previous version: 1.21-103-aa3b356 (MC: 1.21)
+ ```
+
+
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Other
+ description: |
+ Please include other helpful information below.
+ The more information we receive, the quicker and more effective we can be at finding the solution to the issue.
+ validations:
+ required: false
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..7cf5bb011
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,10 @@
+blank_issues_enabled: false
+contact_links:
+ - name: "❗Exploits"
+ url: https://discord.gg/papermc
+ about: |
+ Since GitHub doesn’t currently support private issues, exploit reports are managed through our Discord.
+ To report an exploit, please visit the #paper-exploit-report channel.
+ - name: "🗨 Questions"
+ url: https://discord.gg/papermc
+ about: If you have questions or need help with any minor issues, feel free to ask us on our Discord server!
diff --git a/.github/ISSUE_TEMPLATE/crash-or-stacktrace.yml b/.github/ISSUE_TEMPLATE/crash-or-stacktrace.yml
new file mode 100644
index 000000000..badab09bb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash-or-stacktrace.yml
@@ -0,0 +1,78 @@
+name: "💥 Crash or Stacktrace"
+description: Report any server crashes or alarming stack traces.
+type: "Bug"
+labels:
+ - "status: needs triage"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this issue, please ensure the following:
+
+ 1. You are running the latest version of Paper from [our downloads page](https://papermc.io/downloads/paper).
+ 2. Your version of Minecraft is supported by Paper.
+
+ If your server crash log contains `DO NOT REPORT THIS TO PAPER`, please ask in our
+ [Discord](https://discord.gg/papermc) before opening this issue. These messages are informing you of server
+ lag and providing debug information.
+
+ - type: textarea
+ attributes:
+ label: Stack trace
+ description: |
+ We need all of the stack trace! Do not cut off parts of it. Please do not use attachments.
+ If you prefer, you can use a paste site like https://mclo.gs.
+ value: |
+ ```
+ paste your stack trace or a mclo.gs link here!
+ ```
+ placeholder: Please don't remove the backticks; it makes your issue a lot harder to read!
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Plugin and Datapack List
+ description: |
+ All plugins and datapacks running on your server.
+ To list plugins, run `/plugins`. For datapacks, run `/datapack list`.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Actions to reproduce (if known)
+ description: This may include a build schematic, a video, or detailed instructions to help reconstruct the issue. Anything helps!
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: Paper version
+ description: |
+ Run `/version` on your server and **paste** the full, unmodified output here.
+ "latest" is *not* a version; we require the output of `/version` so we can adequately track down the issue.
+ Additionally, do NOT provide a screenshot, you MUST paste the entire output.
+
+ Example
+
+ ```
+ > version
+ [20:34:42 INFO]: Checking version, please wait...
+ [20:34:42 INFO]: This server is running Paper version 1.21-105-master@7e91a2c (2024-07-20T21:04:31Z) (Implementing API version 1.21-R0.1-SNAPSHOT)
+ [20:34:42 INFO]: You are running the latest version
+ [20:34:42 INFO]: Previous version: 1.21-103-aa3b356 (MC: 1.21)
+ ```
+
+
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Other
+ description: |
+ Please include other helpful information below, if any.
+ The more information we receive, the quicker and more effective we can be at finding the solution to the issue.
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/new-feature.yml b/.github/ISSUE_TEMPLATE/new-feature.yml
new file mode 100644
index 000000000..731b4a2f5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new-feature.yml
@@ -0,0 +1,53 @@
+name: "💡 New Feature"
+description: Propose a new idea for Paper.
+type: "Feature"
+labels:
+ - "status: needs triage"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for submitting a feature request for Paper! Please be as detailed as possible to help us review and consider your request effectively.
+ Before submitting, please ensure the following:
+
+ 1. You are using a supported version of Paper.
+ 2. The feature you’re requesting isn’t already included in the version you’re using.
+ 3. You’ve searched for and confirmed there isn’t already [an open request](https://github.com/PaperMC/Paper/issues?q=is%3Aissue%20is%3Aopen%20type%3AFeature) for this feature.
+ - If a similar request exists, feel free to add any additional details you think are helpful.
+
+ If you have any questions, feel free to ask in the `#paper-help` or `#paper-dev` channels on our [Discord](https://discord.gg/papermc).
+
+ - type: textarea
+ attributes:
+ label: Is your feature request related to a problem?
+ description: Please provide some context for this request. Why do you want it added?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe the solution you'd like.
+ description: A clear and concise description of what you want.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe alternatives you've considered.
+ description: List any alternatives you might have tried to get the feature you want.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Other
+ description: Add any other context or screenshots about the feature request below.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this feature request, please search our issue tracker to ensure your feature has not
+ already been requested.
+
diff --git a/.github/ISSUE_TEMPLATE/performance-problem.yml b/.github/ISSUE_TEMPLATE/performance-problem.yml
new file mode 100644
index 000000000..e13221132
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/performance-problem.yml
@@ -0,0 +1,91 @@
+name: "🐌 Performance Problem"
+description: Report any performance issues.
+type: "Bug"
+labels:
+ - "scope: performance"
+ - "status: needs triage"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this issue, please ensure the following:
+
+ 1. You are running the latest version of Paper from [our downloads page](https://papermc.io/downloads/paper).
+ 2. You searched for and ensured there isn't already [an open issue](https://github.com/PaperMC/Paper/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) regarding this.
+ 3. Your version of Minecraft is supported by Paper.
+
+ - type: markdown
+ attributes:
+ value: |
+ Before creating an issue regarding server performance, please consider reaching out for support in the
+ `#paper-help` channel of [our Discord](https://discord.gg/papermc)!
+
+ - type: input
+ attributes:
+ label: Spark Profile
+ description: |
+ Please provide all profiles as links rather than screenshots. Screenshots limit our ability to investigate the root cause of the issue.
+
+ For more information, see our [profiling documentation](https://docs.papermc.io/paper/profiling).
+ placeholder: "Example: https://spark.lucko.me/XsN0hxGfsi"
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Description of issue
+ description: If applicable, please describe your issue.
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: Plugin and Datapack List
+ description: |
+ All plugins and datapacks running on your server.
+ To list plugins, run `/plugins`. For datapacks, run `/datapack list`.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Server config files
+ description: We need bukkit.yml, spigot.yml, paper-global.yml, paper-world-defaults.yml and server.properties. If you use per-world Paper configs, make sure to include them. You can paste it below or use a paste site like https://mclo.gs.
+ value: |
+ ```
+ Paste configs or mclo.gs link here!
+ ```
+ placeholder: Please don't remove the backticks; it makes your issue a lot harder to read!
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Paper version
+ description: |
+ Run `/version` on your server and **paste** the full, unmodified output here.
+ "latest" is *not* a version; we require the output of `/version` so we can adequately track down the issue.
+ Additionally, do NOT provide a screenshot, you MUST paste the entire output.
+
+ Example
+
+ ```
+ > version
+ [20:34:42 INFO]: Checking version, please wait...
+ [20:34:42 INFO]: This server is running Paper version 1.21-105-master@7e91a2c (2024-07-20T21:04:31Z) (Implementing API version 1.21-R0.1-SNAPSHOT)
+ [20:34:42 INFO]: You are running the latest version
+ [20:34:42 INFO]: Previous version: 1.21-103-aa3b356 (MC: 1.21)
+ ```
+
+
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Other
+ description: |
+ Please include other helpful links below.
+ The more information we receive, the quicker and more effective we can be at finding the solution to the issue.
+ validations:
+ required: false
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..994ade949
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,116 @@
+# Here lie dragons!
+#
+# This action either builds the server or
+# builds a paperclip jar to be updated in the body
+# of the PR relating to this action.
+
+name: Build Paper
+on:
+ push:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - labeled
+
+jobs:
+ build:
+ # Run on all label events (won't be duplicated) or all push events or on PR syncs not from the same repo
+ if: (github.event_name == 'pull_request' && github.event.action == 'labeled') || github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java: [21]
+ fail-fast: true
+ steps:
+ - if: ${{ github.event_name == 'push' }}
+ uses: actions/checkout@v4
+ - if: ${{ github.event_name == 'pull_request' }}
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ - name: JDK ${{ matrix.java }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ matrix.java }}
+ distribution: 'zulu'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Configure Build
+ uses: actions/github-script@v7
+ id: determine
+ env:
+ REF_NAME: "${{ github.ref_name }}"
+ REF_TYPE: "${{ github.ref_type }}"
+ EVENT: "${{ toJSON(github.event) }}"
+ EVENT_TYPE: "${{ github.event_name }}"
+ with:
+ script: |
+ const {owner, repo} = context.repo;
+ const event_name = `${process.env.EVENT_TYPE}`;
+ const event = JSON.parse(`${process.env.EVENT}`);
+ const ref_type = `${process.env.REF_TYPE}`;
+ const ref_name = `${process.env.REF_NAME}`;
+ const result = {
+ action: "build"
+ };
+
+ if (event_name === "push" && ref_type === "branch") {
+ const {data: pulls} = await github.rest.pulls.list({ owner, repo, head: `${owner}:${ref_name}`, state: "open" });
+ const pull = pulls.find((pr) => !!pr.labels.find((l) => l.name === "build-pr-jar"));
+ if (pull) {
+ result["pr"] = pull.number;
+ result["action"] = "paperclip";
+ core.notice(`This is a push action but to a branch with an open PR with the build paperclip label (${JSON.stringify(result)})`);
+ return result;
+ }
+ } else if (event_name === "pull_request" && event.pull_request.labels.find((l) => l.name === "build-pr-jar")) {
+ result["pr"] = event.pull_request.number;
+ result["action"] = "paperclip";
+ core.notice(`This is a pull request action with a build paperclip label (${JSON.stringify(result)})`);
+ return result;
+ }
+ core.notice("This will not build a paperclip jar");
+ return result;
+
+ - name: Apply Patches
+ run: |
+ git config --global user.email "no-reply@github.com"
+ git config --global user.name "GitHub Actions"
+ ./gradlew applyPatches --stacktrace
+
+ - name: Build
+ run: ./gradlew build --stacktrace
+
+ - name: Upload Test Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: Test Results (${{ matrix.java }})
+ path: |
+ **/build/test-results/test/TEST-*.xml
+
+ - name: Create Paperclip Jar
+ if: fromJSON(steps.determine.outputs.result).action == 'paperclip'
+ run: ./gradlew createMojmapPaperclipJar --stacktrace
+
+ - name: Upload Paperclip Jar
+ if: fromJSON(steps.determine.outputs.result).action == 'paperclip'
+ uses: actions/upload-artifact@v4
+ with:
+ name: paper-${{ fromJSON(steps.determine.outputs.result).pr }}
+ path: build/libs/paper-paperclip-*-mojmap.jar
+ event_file:
+ name: "Event File"
+ # Only run on PRs if the source branch is on someone else's repo
+ if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: Event File
+ path: ${{ github.event_path }}
diff --git a/.github/workflows/close_invalid_prs.yml b/.github/workflows/close_invalid_prs.yml
new file mode 100644
index 000000000..ef572c25c
--- /dev/null
+++ b/.github/workflows/close_invalid_prs.yml
@@ -0,0 +1,27 @@
+name: Close invalid PRs
+
+on:
+ pull_request_target:
+ types: [ opened ]
+
+jobs:
+ run:
+ if: |
+ github.repository != github.event.pull_request.head.repo.full_name &&
+ (
+ github.head_ref == 'master' ||
+ github.event.pull_request.head.repo.owner.type != 'User'
+ )
+ runs-on: ubuntu-latest
+ steps:
+ - uses: superbrothers/close-pull-request@v3
+ id: "master_branch"
+ if: github.head_ref == 'master'
+ with:
+ comment: "Please do not open pull requests from the `master` branch, create a new branch instead."
+
+ - uses: superbrothers/close-pull-request@v3
+ id: "org_account"
+ if: github.event.pull_request.head.repo.owner.type != 'User' && steps.master_branch.outcome == 'skipped'
+ with:
+ comment: "Please do not open pull requests from non-user accounts like organizations. Create a fork on a user account instead."
diff --git a/.github/workflows/pr_comment.yml b/.github/workflows/pr_comment.yml
new file mode 100644
index 000000000..60bed3fd3
--- /dev/null
+++ b/.github/workflows/pr_comment.yml
@@ -0,0 +1,84 @@
+# This workflow run on the completion of the
+# build workflow but only does anything if the
+# triggering workflow uploaded an artifact.
+#
+# Do note that it is then the trigger workflow that
+# determines if this will update the PR text body. All
+# this workflow does is check if an uploaded artifact
+# exists and there is a PR tied to the previous workflow.
+
+name: Comment on pull request
+on:
+ workflow_run:
+ workflows: ['Build Paper']
+ types: [completed]
+jobs:
+ pr_comment:
+ if: github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/github-script@v7
+ env:
+ BRANCH_NAME: "${{ github.event.workflow_run.head_branch }}"
+ PR_OWNER: "${{ github.event.workflow_run.head_repository.owner.login }}"
+ PR_SHA: "${{ github.event.workflow_run.head_sha }}"
+ RUN_ID: "${{ github.event.workflow_run.id }}"
+ REPO_ID: "${{ github.event.repository.id }}"
+ EVENT_TYPE: "${{ github.event.workflow_run.event}}"
+ PULL_REQUESTS: "${{ toJSON(github.event.workflow_run.pull_requests) }}"
+ with:
+ # This snippet is public-domain, taken from
+ # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
+ # Modified extensively by Machine_Maker
+ script: |
+ async function updatePR(owner, repo, issue_number, purpose, body) {
+ const { data } = await github.rest.issues.get({ owner, repo, issue_number });
+ core.debug(JSON.stringify(data, null, 2));
+
+ const marker = ``;
+
+ let new_body = data.body ? data.body.trim().split(marker)[0].trim() : "";
+ new_body += `\n${marker}\n---\n${body}`;
+
+ core.info(`Updating the text body of PR #${issue_number} in ${owner}/${repo}`);
+ await github.rest.issues.update({ owner, repo, issue_number, body: new_body });
+ }
+
+ const { owner, repo } = context.repo;
+ const run_id = `${process.env.RUN_ID}`;
+ const repo_id = `${process.env.REPO_ID}`;
+
+ let pulls = [];
+ const event_type = `${process.env.EVENT_TYPE}`;
+ if (event_type === "push") { // if push, it's from the same repo which means `pull_requests` is populated
+ pulls = JSON.parse(`${process.env.PULL_REQUESTS}`);
+ } else {
+ const pr_branch = `${process.env.BRANCH_NAME}`;
+ const pr_sha = `${process.env.PR_SHA}`;
+ const pr_owner = `${process.env.PR_OWNER}`;
+ const { data } = await github.rest.pulls.list({ owner, repo, head: `${pr_owner}:${pr_branch}`, state: "open" });
+ core.debug(JSON.stringify(data, null, 2));
+ pulls = data.filter((pr) => pr.head.sha === pr_sha && pr.labels.find((l) => l.name === "build-pr-jar"));
+ }
+
+ if (!pulls.length) {
+ return core.notice("This workflow doesn't have any pull requests!");
+ } else if (pulls.length > 1) {
+ core.info(JSON.stringify(pulls, null, 2));
+ return core.error("Found multiple matching PRs");
+ }
+ const pull_request = pulls[0];
+
+ const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { owner, repo, run_id });
+ if (!artifacts.length) {
+ return core.info("Skipping comment due to no artifact found");
+ }
+ const artifact = artifacts.find((art) => art.name === `paper-${pull_request.number}`);
+ if (!artifact) {
+ return core.info("Skipping comment to no matching artifact found");
+ }
+
+ const link = `https://nightly.link/${owner}/${repo}/actions/artifacts/${artifact.id}.zip`;
+ const body = `Download the paperclip jar for this pull request: [${artifact.name}.zip](${link})`;
+ core.info(`Adding a link to ${link}`);
+ await updatePR(owner, repo, pull_request.number, "paperclip-pr-build", body);
diff --git a/.github/workflows/projects.yml b/.github/workflows/projects.yml
new file mode 100644
index 000000000..5a42cedc1
--- /dev/null
+++ b/.github/workflows/projects.yml
@@ -0,0 +1,76 @@
+name: Update Projects
+
+on:
+ issues:
+ types:
+ - labeled
+ - unlabeled
+ - closed
+ - reopened
+
+jobs:
+ bugs:
+ if: "github.event_name == 'issues' && contains(github.event.*.labels.*.name, 'type: bug')"
+ concurrency:
+ group: update-bugs-project-${{ github.event.issue.number }}
+ cancel-in-progress: true
+ runs-on: ubuntu-latest
+ steps:
+ - name: "authenticate"
+ id: "authenticate"
+ uses: "tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92" # v1
+ with:
+ app_id: "${{ secrets.PROJECTS_APP_ID }}"
+ installation_id: "36153445"
+ private_key: "${{ secrets.PROJECTS_PRIVATE_KEY }}"
+
+ - uses: PaperMC/update-projects-action@v0.2.0
+ name: Update open issue
+ if: github.event.issue.state == 'open'
+ with:
+ github-token: "${{ steps.authenticate.outputs.token }}"
+ project-url: https://github.com/orgs/PaperMC/projects/5/views/2
+ column-field: Status
+ label-to-column-map: |
+ {
+ "resolution: awaiting response": "⌛ Awaiting Response",
+ "resolution: cannot reproduce": "Invalid",
+ "resolution: duplicate": "Invalid",
+ "resolution: incomplete": "Invalid",
+ "resolution: invalid": "Invalid",
+ "resolution: superseded": "Invalid",
+ "resolution: unsupported": "Invalid",
+ "resolution: won't fix": "Invalid",
+ "resolution: works as intended": "Invalid",
+ "status: accepted": "✅ Accepted",
+ "status: blocked": "Needs Work",
+ "status: defer upstream": "Needs Work",
+ "status: in progress": "Needs Work",
+ "status: input wanted": "Needs Work",
+ "status: needs triage": "🕑 Needs Triage",
+ "status: rebase required": "Needs Work",
+ "status: unlikely": "✅ Accepted"
+ }
+
+ - uses: PaperMC/update-projects-action@v0.2.0
+ name: Update closed issue
+ if: github.event.issue.state == 'closed'
+ with:
+ github-token: "${{ steps.authenticate.outputs.token }}"
+ project-url: https://github.com/orgs/PaperMC/projects/5/views/2
+ column-field: Status
+ clear-on-no-match: false
+ # include "status: needs triage" below to catch any closed issues without setting a resolution
+ label-to-column-map: |
+ {
+ "resolution: cannot reproduce": "Invalid",
+ "resolution: duplicate": "Invalid",
+ "resolution: incomplete": "Invalid",
+ "resolution: invalid": "Invalid",
+ "resolution: superseded": "Invalid",
+ "resolution: unsupported": "Invalid",
+ "resolution: won't fix": "Invalid",
+ "resolution: works as intended": "Invalid",
+ "status: accepted": "Done",
+ "status: needs triage": "Invalid"
+ }
diff --git a/.github/workflows/test_results.yml b/.github/workflows/test_results.yml
new file mode 100644
index 000000000..f3c63cf8f
--- /dev/null
+++ b/.github/workflows/test_results.yml
@@ -0,0 +1,32 @@
+name: Test Results
+
+on:
+ workflow_run:
+ workflows: [ "Build Paper" ]
+ types:
+ - completed
+permissions: { }
+
+jobs:
+ test-results:
+ name: Test Results
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.conclusion != 'skipped'
+ permissions:
+ checks: write
+ # for downloading test result artifacts
+ actions: read
+ steps:
+ - name: Download and Extract Artifacts
+ uses: dawidd6/action-download-artifact@v6
+ with:
+ run_id: ${{ github.event.workflow_run.id }}
+ path: artifacts
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ commit: ${{ github.event.workflow_run.head_sha }}
+ event_file: artifacts/Event File/event.json
+ event_name: ${{ github.event.workflow_run.event }}
+ files: "artifacts/**/*.xml"
+ comment_mode: off
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..a2adff58b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,76 @@
+.gradle/
+build/
+
+# Eclipse stuff
+.classpath
+.project
+.settings/
+
+# VSCode stuff
+.vscode/
+
+# netbeans
+nbproject/
+nbactions.xml
+
+# we use maven!
+build.xml
+
+# maven
+target/
+dependency-reduced-pom.xml
+
+# vim
+.*.sw[a-p]
+
+# various other potential build files
+bin/
+dist/
+manifest.mf
+
+/work/Temp/
+work/1.*
+work/Minecraft
+work/BuildData
+work/Bukkit
+work/CraftBukkit
+work/Paperclip
+work/Spigot
+work/Spigot-Server
+work/Spigot-API
+work/*.jar
+work/test-server
+work/ForgeFlower
+
+# Mac filesystem dust
+.DS_Store/
+.DS_Store
+
+# intellij
+*.iml
+*.ipr
+*.iws
+.idea/
+out/
+
+# JetBrains Fleet
+.fleet/
+
+# Linux temp files
+*~
+
+# other stuff
+run/
+logs/
+
+Paper-Server
+Paper-API
+Paperclip.jar
+paperclip.jar
+paperclip-*.jar
+paperclip.properties
+
+!gradle/wrapper/gradle-wrapper.jar
+
+test-plugin.settings.gradle.kts
+paper-api-generator.settings.gradle.kts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..475d77bbb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,516 @@
+Contributing to Paper
+==========================
+PaperMC is happy you're willing to contribute to our projects. We are usually
+very lenient with all submitted PRs, but there are still some guidelines you
+can follow to make the approval process go more smoothly.
+
+## Use a Personal Fork and not an Organization
+
+Paper will routinely modify your PR, whether it's a quick rebase or to take care
+of any minor nitpicks we might have. Often, it's better for us to solve these
+problems for you than make you go back and forth trying to fix them yourself.
+
+Unfortunately, if you use an organization for your PR, it prevents Paper from
+modifying it. This requires us to manually merge your PR, resulting in us
+closing the PR instead of marking it as merged.
+
+We much prefer to have PRs show as merged, so please do not use repositories
+on organizations for PRs.
+
+See for more information on the
+issue.
+
+## Requirements
+
+To get started with PRing changes, you'll need the following software, most of
+which can be obtained in (most) package managers such as `apt` (Debian / Ubuntu;
+you will most likely use this for WSL), `homebrew` (macOS / Linux), and more:
+
+- `git` (package `git` everywhere);
+- A Java 21 or later JDK (packages vary, use Google/DuckDuckGo/etc.).
+ - [Adoptium](https://adoptium.net/) has builds for most operating systems.
+ - Paper requires JDK 21 to build, however, makes use of Gradle's
+ [Toolchains](https://docs.gradle.org/current/userguide/toolchains.html)
+ feature to allow building with only JRE 11 or later installed. (Gradle will
+ automatically provision JDK 21 for compilation if it cannot find an existing
+ install).
+
+If you're on Windows, check
+[the section on WSL](#patching-and-building-is-really-slow-what-can-i-do).
+
+If you're compiling with Docker, you can use Adoptium's
+[`eclipse-temurin`](https://hub.docker.com/_/eclipse-temurin/) images like so:
+
+```console
+# docker run -it -v "$(pwd)":/data --rm eclipse-temurin:21.0.3_9-jdk bash
+Pulling image...
+
+root@abcdefg1234:/# javac -version
+javac 21.0.3
+```
+
+## Understanding Patches
+
+Paper is mostly patches and extensions to Spigot. These patches/extensions are
+split into different directories which target certain parts of the code. These
+directories are:
+
+- `Paper-API` - Modifications to `Spigot-API`/`Bukkit`;
+- `Paper-Server` - Modifications to `Spigot`/`CraftBukkit`.
+
+Because the entire structure is based on patches and git, a basic understanding
+of how to use git is required. A basic tutorial can be found here:
+.
+
+Assuming you have already forked the repository:
+
+1. Clone your fork to your local machine;
+2. Type `./gradlew applyPatches` in a terminal to apply the changes from upstream.
+On Windows, replace the `./` with `.\` at the beginning for all `gradlew` commands;
+3. cd into `Paper-Server` for server changes, and `Paper-API` for API changes.
+
+
+`Paper-Server` and `Paper-API` aren't git repositories in the traditional sense:
+
+- `base` points to the unmodified source before Paper patches have been applied.
+- Each commit after `base` is a patch.
+
+## Adding Patches
+
+Adding patches to Paper is very simple:
+
+1. Modify `Paper-Server` and/or `Paper-API` with the appropriate changes;
+1. Type `git add .` inside these directories to add your changes;
+1. Run `git commit` with the desired patch message;
+1. Run `./gradlew rebuildPatches` in the main directory to convert your commit into a new
+patch;
+1. PR the generated patch file(s) back to this repository.
+
+Your commit will be converted into a patch that you can then PR into Paper.
+
+> ❗ Please note that if you have some specific implementation detail you'd like
+> to document, you should do so in the patch message *or* in comments.
+
+## Modifying Patches
+
+Modifying previous patches is a bit more complex.
+Similar to adding patches, the methods to modify a patch are applied inside
+the `Paper-Server` and/or `Paper-API` folders.
+
+### Method 1
+
+This method works by temporarily resetting your `HEAD` to the desired commit to
+edit it using `git rebase`.
+
+> ❗ While in the middle of an edit, you will not be able to compile unless you
+> *also* reset the opposing module(s) to a related commit. In the API's case,
+> you must reset the Server, and reset the API if you're editing the Server.
+> Note also that either module _may_ not compile when doing so. This is not
+> ideal nor intentional, but it happens. Feel free to fix this in a PR to us!
+
+1. If you have changes you are working on, type `git stash` to store them for
+later;
+ - You can type `git stash pop` to get them back at any point.
+1. Type `git rebase -i base`;
+ - It should show something like
+ [this](https://gist.github.com/zachbr/21e92993cb99f62ffd7905d7b02f3159) in
+ the text editor you get.
+ - If your editor does not have a "menu" at the bottom, you're using `vim`.
+ If you don't know how to use `vim` and don't want to
+ learn, enter `:q!` and press enter. Before redoing this step, do
+ `export EDITOR=nano` for an easier editor to use.
+1. Replace `pick` with `edit` for the commit/patch you want to modify, and
+"save" the changes;
+ - Only do this for **one** commit at a time.
+1. Make the changes you want to make to the patch;
+1. Type `git add .` to add your changes;
+1. Type `git commit --amend` to commit;
+ - **Make sure to add `--amend`** or else a new patch will be created.
+ - You can also modify the commit message and author here.
+1. Type `git rebase --continue` to finish rebasing;
+1. Type `./gradlew rebuildPatches` in the root directory;
+ - This will modify the appropriate patches based on your commits.
+1. PR your modified patch file(s) back to this repository.
+
+### Method 2 - Fixup commits
+
+If you are simply editing a more recent commit or your change is small, simply
+making the change at HEAD and then moving the commit after you have tested it
+may be easier.
+
+This method has the benefit of being able to compile to test your change without
+messing with your HEADs.
+
+#### Manual method
+
+1. Make your change while at HEAD;
+1. Make a temporary commit. You don't need to make a message for this;
+1. Type `git rebase -i base`, move (cut) your temporary commit and
+move it under the line of the patch you wish to modify;
+1. Change the `pick` to the appropriate action:
+ 1. `f`/`fixup`: Merge your changes into the patch without touching the
+ message.
+ 1. `s`/`squash`: Merge your changes into the patch and use your commit message
+ and subject.
+1. Type `./gradlew rebuildPatches` in the root directory;
+ - This will modify the appropriate patches based on your commits.
+1. PR your modified patch file(s) back to this repository.
+
+#### Automatic method
+
+1. Make your change while at HEAD;
+1. Make a fixup commit. `git commit -a --fixup `;
+ - You can also use `--squash` instead of `--fixup` if you want the commit
+ message to also be changed.
+ - You can get the hash by looking at `git log` or `git blame`; your IDE can
+ assist you too.
+ - Alternatively, if you only know the name of the patch, you can do
+ `git commit -a --fixup "Subject of Patch name"`.
+1. Rebase with autosquash: `git rebase -i --autosquash base`.
+This will automatically move your fixup commit to the right place, and you just
+need to "save" the changes.
+1. Type `./gradlew rebuildPatches` in the root directory;
+ - This will modify the appropriate patches based on your commits.
+1. PR your modified patch file(s) back to this repository.
+
+## Rebasing PRs
+
+Steps to rebase a PR to include the latest changes from `master`.
+These steps assume the `origin` remote is your fork of this repository and `upstream` is the official PaperMC repository.
+
+1. Pull the latest changes from upstreams master: `git checkout master && git pull upstream master`.
+1. Checkout feature/fix branch and rebase on master: `git checkout patch-branch && git rebase master`.
+1. Apply updated patches: `./gradlew applyPatches`.
+1. If there are conflicts, fix them.
+ * If there are conflicts within `Paper-API`, after fixing them run `./gradlew rebuildApiPatches` before re-running `./gradlew applyPatches`.
+ * The API patches are applied first, so if there are conflicts in the API patches, the server patches will not be applied.
+1. If your PR creates new patches instead of modifying existing ones, in both the `Paper-Server` and `Paper-API` directories, ensure your newly-created patch is the last commit by either:
+ * Renaming the patch file with a large 4-digit number in front (e.g. 9999-Patch-to-add-some-new-stuff.patch), and re-applying patches.
+ * Running `git rebase --interactive base` and moving the commits to the end.
+1. Rebuild patches: `./gradlew rebuildPatches`.
+1. Commit modified patches.
+1. Force push changes: `git push --force`.
+
+## PR Policy
+
+We'll accept changes that make sense. You should be able to justify their
+existence, along with any maintenance costs that come with them. Using
+[obfuscation helpers](#obfuscation-helpers) aids in the maintenance costs.
+Remember that these changes will affect everyone who runs Paper, not just you
+and your server.
+
+While we will fix minor formatting issues, you should stick to the guide below
+when making and submitting changes.
+
+## Formatting
+
+All modifications to non-Paper files should be marked. The one exception to this is
+when modifying javadoc comments, which should not have these markers.
+
+- You need to add a comment with a short and identifiable description of the patch:
+ `// Paper start - `
+ - The comments should generally be about the reason the change was made, what
+ it was before, or what the change is.
+ - After the general commit description, you can add additional information either
+ after a `;` or in the next line.
+- Multi-line changes start with `// Paper start - ` and end
+ with `// Paper end - `.
+- One-line changes should have `// Paper - ` at the end of the line.
+
+Here's an example of how to mark changes by Paper:
+
+```java
+entity.getWorld().dontBeStupid(); // Paper - Was beStupid(), which is bad
+entity.getFriends().forEach(Entity::explode);
+entity.updateFriends();
+
+// Paper start - Use plugin-set spawn
+// entity.getWorld().explode(entity.getWorld().getSpawn());
+Location spawnLocation = ((CraftWorld)entity.getWorld()).getSpawnLocation();
+entity.getWorld().explode(new BlockPosition(spawnLocation.getX(), spawnLocation.getY(), spawnLocation.getZ()));
+// Paper end - Use plugin-set spawn
+```
+
+We generally follow the usual Java style (aka. Oracle style), or what is programmed
+into most IDEs and formatters by default. There are a few notes, however:
+- It is fine to go over 80 lines as long as it doesn't hurt readability.
+There are exceptions, especially in Spigot-related files
+- When in doubt or the code around your change is in a clearly different style,
+use the same style as the surrounding code.
+- Usage of the `var` keyword is heavily discouraged, as it makes reading patch files
+a lot harder and can lead to confusion during updates due to changed return types.
+The only exception to this is if a line would otherwise be way too long/filled with
+hard to parse generics in a case where the base type itself is already obvious
+
+### Imports
+When adding new imports to a class in a file not created by the current patch, use the fully qualified class name
+instead of adding a new import to the top of the file. If you are using a type a significant number of times, you
+can add an import with a comment. However, if its only used a couple of times, the FQN is preferred to prevent future
+patch conflicts in the import section of the file.
+
+### Nullability annotations
+
+We are in the process of switching nullability annotation libraries, so you might need to use one or the other:
+
+**For classes we add**: Fields, method parameters and return types that are nullable should be marked via the
+`@Nullable` annotation from `org.jspecify.annotations`. Whenever you create a new class, add `@NullMarked`, meaning types
+are assumed to be non-null by default. For less obvious placing such as on generics or arrays, see the [JSpecify docs](https://jspecify.dev/docs/user-guide/).
+
+**For classes added by upstream**: Keep using both `@Nullable` and `@NotNull` from `org.jetbrains.annotations`. These
+will be replaced later.
+
+```java
+import org.bukkit.event.Event;
+// don't add import here, use FQN like below
+
+public class SomeEvent extends Event {
+ public final org.bukkit.Location newLocation; // Paper - add location
+}
+```
+
+## Access Transformers
+Sometimes, vanilla or CraftBukkit code already contains a field, method, or type you want to access
+but the visibility is too low (e.g. a private field in an entity class). Paper can use access transformers
+to change the visibility or remove the final modifier from fields, methods, and classes. Inside the `build-data/paper.at`
+file, you can add ATs that are applied when you `./gradlew applyPatches`. You can read about the format of ATs
+[here](https://mcforge.readthedocs.io/en/latest/advanced/accesstransformers/#access-modifiers).
+
+### Important
+ATs should be included in the patch file which requires them within the commit message. Do not commit any changes to the
+`build-data/paper.at` file, just use it to initially change the visibility of members until you have finalized what you
+need. Then, in the commit message for the patch which requires the ATs, add a header at the bottom of the commit message
+before any co-authors. It should look like the following after you `./gradlew rebuildPatches`.
+```
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic
+Date: Wed, 8 Jun 2022 22:20:16 -0700
+Subject: [PATCH] Paper config files
+
+This patch adds Paper configuration files.
+Access transformers for this patch are below, but before the co-authors.
+
+== AT ==
+public org.spigotmc.SpigotWorldConfig getBoolean(Ljava/lang/String;Z)Z
+public net.minecraft.world.level.NaturalSpawner SPAWNING_CATEGORIES
+
+Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+...
+```
+
+## Patch Notes
+
+When submitting patches to Paper, we may ask you to add notes to the patch
+header. While we do not require it for all changes, you should add patch notes
+when the changes you're making are technical, complex, or require an explanation
+of some kind. It is very likely that your patch will remain long after we've all
+forgotten about the details of your PR; patch notes will help us maintain it
+without having to dig back through GitHub history looking for your PR.
+
+These notes should express the intent of your patch, as well as any pertinent
+technical details we should keep in mind long-term. Ultimately, they exist to
+make it easier for us to maintain the patch across major version changes.
+
+If you add a message to your commit in the `Paper-Server`/`Paper-API`
+directories, the rebuild patches script will handle these patch notes
+automatically as part of generating the patch file. If you are not
+extremely careful, you should always just `squash` or `amend` a patch (see the
+above sections on modifying patches) and rebuild.
+
+Editing messages and patches by hand is possible, but you should patch and
+rebuild afterwards to make sure you did it correctly. This is slower than just
+modifying the patches properly after a few times, so you will not really gain
+anything but headaches from doing it by hand.
+
+Underneath is an example patch header/note:
+
+```patch
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Shane Freeder
+Date: Sun, 15 Oct 2017 00:29:07 +0100
+Subject: [PATCH] revert serverside behavior of keepalives
+
+This patch intends to bump up the time that a client has to reply to the
+server back to 30 seconds as per pre 1.12.2, which allowed clients
+more than enough time to reply potentially allowing them to be less
+temperamental due to lag spikes on the network thread, e.g. that caused
+by plugins that are interacting with netty.
+
+We also add a system property to allow people to tweak how long the server
+will wait for a reply. There is a compromise here between lower and higher
+values, lower values will mean that dead connections can be closed sooner,
+whereas higher values will make this less sensitive to issues such as spikes
+from networking or during connections flood of chunk packets on slower clients,
+ at the cost of dead connections being kept open for longer.
+
+diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
+index a92bf8967..d0ab87d0f 100644
+--- a/src/main/java/net/minecraft/server/PlayerConnection.java
++++ b/src/main/java/net/minecraft/server/PlayerConnection.java
+```
+
+## Obfuscation Helpers
+
+While rarely needed, obfuscation helpers are sometimes useful when it comes
+to unmapped local variables, or poorly named method parameters. In an effort
+to make future updates easier on ourselves, Paper tries to use obfuscation
+helpers wherever it makes sense. The purpose of these helpers is to make the
+code more readable and maintainable. These helpers should be made easy to
+inline by the JVM wherever possible.
+
+An example of an obfuscation helper for a local variable:
+```java
+double d0 = entity.getX(); final double fromX = d0; // Paper - OBFHELPER
+// ...
+this.someMethod(fromX); // Paper
+```
+
+While they may not always be done in exactly the same way, the general goal is
+always to improve readability and maintainability. Use your best judgment and do
+what fits best in your situation.
+
+## Configuration files
+
+To use a configurable value in your patch, add a new field in either the
+`GlobalConfiguration` or `WorldConfiguration` classes (inside the
+`io.papermc.paper.configuration` package). Use `GlobalConfiguration` if a value
+must remain the same throughout all worlds, or the latter if it can change
+between worlds. World-specific configuration options are preferred whenever
+possible.
+
+### Example
+This is adding a new miscellaneous setting that doesn't seem to fit in other categories.
+Try to check and see if an existing category (inner class) exists that matches
+whatever configuration option you are adding.
+```java
+public class GlobalConfiguration {
+ // other sections
+ public class Misc extends ConfigurationPart {
+ // other settings
+ public boolean lagCompensateBlockBreaking = true;
+ public boolean useDimensionTypeForCustomSpawners = false;
+ public int maxNumOfPlayers = 20; // This is the new setting
+ }
+}
+```
+You set the type of the setting as the field type, and the default value is the
+initial field value. The name of the setting defaults to the snake-case of the
+field name, so in this case it would be `misc.max-num-of-players`. You can use
+the `@Setting` annotation to override that, but generally just try to set the
+field name to what you want the setting to be called.
+
+#### Accessing the value
+If you added a new global config value, you can access it in the code just by
+doing
+```java
+int maxPlayers = GlobalConfiguration.get().misc.maxNumOfPlayers;
+```
+Generally for global config values you will use the fully qualified class name,
+`io.papermc.paper.configuration.GlobalConfiguration` since it's not imported in
+most places.
+---
+If you are adding a new world config value, you must have access to an instance
+of the `net.minecraft.world.level.Level` which you can then access the config by doing
+```java
+int maxPlayers = level.paperConfig().misc.maxNumOfPlayers;
+```
+
+#### Committing changes
+All changes to the `GlobalConfiguration` and `WorldConfiguration` files
+should be done in the commit that created them. So do an interactive rebase
+or fixup to apply just those changes to that commit, then add a new commit
+that includes the logic that uses that option in the server somewhere.
+
+## Testing API changes
+
+### Using the Paper Test Plugin
+
+The Paper project has a `test-plugin` module for easily testing out API changes
+and additions. To use the test plugin, enable it in `test-plugin.settings.gradle.kts`,
+which will be generated after running Gradle at least once. After this, you can edit
+the test plugin, and run a server with the plugin using `./gradlew runDev` (or any
+of the other Paper run tasks).
+
+### Publishing to Maven local (use in external plugins)
+
+To build and install the Paper APIs and Server to your local Maven repository, do the following:
+
+- Run `./gradlew publishToMavenLocal` in the base directory.
+
+If you use Gradle to build your plugin:
+- Add `mavenLocal()` as a repository. Gradle checks repositories in the order they are declared,
+ so if you also have the Paper repository added, put the local repository above Paper's.
+- Make sure to remove `mavenLocal()` when you are done testing, see the [Gradle docs](https://docs.gradle.org/current/userguide/declaring_repositories.html#sec:case-for-maven-local)
+ for more details.
+
+If you use Maven to build your plugin:
+- If you later need to use the Paper-API, you might want to remove the jar
+ from your local Maven repository.
+ If you use Windows and don't usually build using WSL, you might not need to
+ do this.
+
+## Frequently Asked Questions
+
+### I can't find the NMS file I need!
+
+By default, Paper (and upstream) only import files we make changes to. If you
+would like to make changes to a file that isn't present in `Paper-Server`'s
+source directory, you just need to add it to our import script ran during the
+patching process.
+
+1. Save (rebuild) any patches you are in the middle of working on! Their
+progress will be lost if you do not;
+1. Identify the name(s) of the file(s) you want to import.
+ - A complete list of all possible file names can be found at
+ `./Paper-Server/.gradle/caches/paperweight/mc-dev-sources/net/minecraft/`. You might find
+ [MappingViewer] useful if you need to translate between Mojang and Spigot mapped names.
+1. Open the file at `./build-data/dev-imports.txt` and add the name of your file to
+the script. Follow the instructions there;
+1. Re-patch the server `./gradlew applyPatches`;
+1. Edit away!
+
+> ❗ This change is temporary! **DO NOT COMMIT CHANGES TO THIS FILE!**
+> Once you have made your changes to the new file, and rebuilt patches, you may
+> undo your changes to `dev-imports.txt`.
+
+Any file modified in a patch file gets automatically imported, so you only need
+this temporarily to import it to create the first patch.
+
+To undo your changes to the file, type `git checkout build-data/dev-imports.txt`.
+
+### My commit doesn't need a build, what do I do?
+
+Well, quite simple: You add `[ci skip]` to the start of your commit subject.
+
+This case most often applies to changes to files like `README.md`, this very
+file (`CONTRIBUTING.md`), the `LICENSE.md` file, and so forth.
+
+### Patching and building is *really* slow, what can I do?
+
+This only applies if you're running Windows. If you're running a prior Windows
+release, either update to Windows 10/11 or move to macOS/Linux/BSD.
+
+In order to speed up patching process on Windows, it's recommended you get WSL
+2. This is available in Windows 10 v2004, build 19041 or higher. (You can check
+your version by running `winver` in the run window (Windows key + R)). If you're
+using an out of date version of Windows 10, update your system with the
+[Windows 10 Update Assistant](https://www.microsoft.com/en-us/software-download/windows10) or [Windows 11 Update Assistant](https://www.microsoft.com/en-us/software-download/windows11).
+
+To set up WSL 2, follow the information here:
+
+
+You will most likely want to use the Ubuntu apps. Once it's set up, install the
+required tools with `sudo apt-get update && sudo apt-get install $TOOL_NAMES
+-y`. Replace `$TOOL_NAMES` with the packages found in the
+[requirements](#requirements). You can now clone the repository and do
+everything like usual.
+
+> ❗ Do not use the `/mnt/` directory in WSL! Instead, mount the WSL directories
+> in Windows like described here:
+>
+
+[MappingViewer]: https://mappings.cephx.dev/
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000..3a25fbbf2
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,68 @@
+Paper inherits its licensing from upstream projects.
+
+As such, Paper is licensed under the
+[GNU General Public License version 3](licenses/GPL.md); as it inherits it from Spigot,
+who in turn inherits it from the original Bukkit and Craftbukkit projects.
+
+Any author who is _not_ listed below should be presumed to have released their work
+under the original [GPL](licenses/GPL.md) license.
+
+In the interest of promoting a better Minecraft platform for everyone, contributors
+may choose to release their code under the more permissive [MIT License](licenses/MIT.md).
+
+The authors listed below have chosen to release their code under that more permissive
+[MIT License](licenses/MIT.md). Any contributor who wants their name added below
+should submit a pull request to this project to add their name.
+
+```text
+Zach Brown <1254957+zachbr@users.noreply.github.com>
+Daniel Ennis
+Riley Park
+Black Hole
+Mark Vainomaa
+Mystiflow
+Shane Freeder
+Gabscap
+Jadon Fowler
+chickeneer
+Minecrell
+Techcable
+BillyGalbreath
+MiniDigger | Martin
+Brokkonaut
+vemacs
+stonar96
+Hugo Manrique
+willies952002
+MicleBrick
+Trigary
+rickyboy320
+DoNotSpamPls <7570108+DoNotSpamPls@users.noreply.github.com>
+Josh Roy <10731363+JRoy@users.noreply.github.com>
+ysl3000
+Machine_Maker
+Ivan Pekov
+Camotoy <20743703+Camotoy@users.noreply.github.com>
+Bjarne Koll
+MeFisto94
+Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
+LemonCaramel
+Noah van der Aa
+Doc
+Nick Hensel
+vytskalt
+TheFruxz
+Kieran Wallbanks
+Denery
+Jakubk15
+Redned
+Luke Chambers
+Emily
+dawon
+Ollie <69084614+olijeffers0n@users.noreply.github.com>
+Oliwier Miodun
+aerulion
+Lukas Planz
+granny
+mja00
+```
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..db2db9ddd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,96 @@
+Paper [](https://github.com/PaperMC/Paper/actions)
+[](https://discord.gg/papermc)
+[](https://github.com/sponsors/PaperMC)
+[](https://opencollective.com/papermc)
+===========
+
+The most widely used, high-performance Minecraft server that aims to fix gameplay and mechanics inconsistencies.
+
+
+**Support and Project Discussion:**
+- [Our forums](https://forums.papermc.io/) or [Discord](https://discord.gg/papermc)
+
+How To (Server Admins)
+------
+Paperclip is a jar file that you can download and run just like a normal jar file.
+
+Download Paper from our [downloads page](https://papermc.io/downloads/paper).
+
+Run the Paperclip jar directly from your server. Just like old times
+
+* Documentation on using Paper: [docs.papermc.io](https://docs.papermc.io)
+* For a sneak peek at upcoming features, [see here](https://github.com/PaperMC/Paper/projects)
+
+How To (Plugin Developers)
+------
+* See our API patches [here](patches/api)
+* See upcoming, pending, and recently added API [here](https://github.com/orgs/PaperMC/projects/2/views/4)
+* Paper API javadocs here: [papermc.io/javadocs](https://papermc.io/javadocs/)
+#### Repository (for paper-api)
+##### Maven
+
+```xml
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+```
+
+```xml
+
+ io.papermc.paper
+ paper-api
+ 1.21.4-R0.1-SNAPSHOT
+ provided
+
+```
+##### Gradle
+```kotlin
+repositories {
+ maven {
+ url = uri("https://repo.papermc.io/repository/maven-public/")
+ }
+}
+
+dependencies {
+ compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
+```
+
+How To (Compiling Jar From Source)
+------
+To compile Paper, you need JDK 21 and an internet connection.
+
+Clone this repo, run `./gradlew applyPatches`, then `./gradlew createMojmapBundlerJar` from your terminal. You can find the compiled jar in the project root's `build/libs` directory.
+
+To get a full list of tasks, run `./gradlew tasks`.
+
+How To (Pull Request)
+------
+See [Contributing](CONTRIBUTING.md)
+
+Support Us
+------
+First of all, thank you for considering helping out, we really appreciate that!
+
+PaperMC has various recurring expenses, mostly related to infrastructure. Paper uses [Open Collective](https://opencollective.com/) via the [Open Source Collective fiscal host](https://opencollective.com/opensource) to manage expenses. Open Collective allows us to be extremely transparent, so you can always see how your donations are used. You can read more about financially supporting PaperMC [on our website](https://papermc.io/sponsors).
+
+You can find our collective [here](https://opencollective.com/papermc), or you can donate via GitHub Sponsors [here](https://github.com/sponsors/PaperMC), which will also go towards the collective.
+
+Special Thanks To:
+-------------
+
+[](https://www.yourkit.com/)
+
+[YourKit](https://www.yourkit.com/), makers of the outstanding java profiler, support open source projects of all kinds with their full featured [Java](https://www.yourkit.com/java/profiler) and [.NET](https://www.yourkit.com/.net/profiler) application profilers. We thank them for granting Paper an OSS license so that we can make our software the best it can be.
+
+[](https://www.jetbrains.com)
+
+[JetBrains](https://www.jetbrains.com/), creators of the IntelliJ IDEA, supports Paper with one of their [Open Source Licenses](https://www.jetbrains.com/opensource/). IntelliJ IDEA is the recommended IDE for working with Paper, and most of the Paper team uses it.
+
+All our sponsors!
+[](https://papermc.io/sponsors)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..139ea872d
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,14 @@
+# Security Policy
+
+## Supported Versions
+
+We generally only fully support the latest version, the same applies to exploits such as server crashes and item
+duplication bugs. In the transition period during larger Minecraft updates, we may still backport important fixes to the
+last minor or major release.
+
+## Reporting a Vulnerability
+
+For any issues that are NOT duplication bugs, server/client crashes, or otherwise serious exploits, please open an issue
+through the [Issues tab](https://github.com/PaperMC/Paper/issues).
+For exploits, please [join our Discord](https://discord.gg/papermc) and see the [#paper-exploit-report channel](https://discord.com/channels/289587909051416579/1208749386348101682) for
+further instructions.
diff --git a/build-data/dev-imports.txt b/build-data/dev-imports.txt
new file mode 100644
index 000000000..302359e8a
--- /dev/null
+++ b/build-data/dev-imports.txt
@@ -0,0 +1,15 @@
+# You can use this file to import files from minecraft libraries into the project
+# format:
+#
+# both fully qualified and a file based syntax are accepted for :
+# authlib com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java
+# datafixerupper com.mojang.datafixers.DataFixerBuilder
+# datafixerupper com/mojang/datafixers/util/Either.java
+# To import classes from the vanilla Minecraft jar use `minecraft` as the artifactId:
+# minecraft net.minecraft.world.level.entity.LevelEntityGetterAdapter
+# minecraft net/minecraft/world/level/entity/LevelEntityGetter.java
+# To import minecraft data files, like the default chat type, use `mc_data` as the prefix:
+# mc_data chat_type/chat.json
+# mc_data dimension_type/overworld.json
+#
+
diff --git a/build-data/mappings-patch.tiny b/build-data/mappings-patch.tiny
new file mode 100644
index 000000000..14a2a3582
--- /dev/null
+++ b/build-data/mappings-patch.tiny
@@ -0,0 +1,5 @@
+tiny 2 0 spigot mojang+yarn
+
+# Originally DistanceManager, which also implements DistanceManager, so clashes since the implemented class
+# is imported and not fully qualified. Easiest fix is to just change the name
+c net/minecraft/server/level/PlayerChunkMap$a net/minecraft/server/level/ChunkMap$ChunkDistanceManager
diff --git a/build-data/paper.at b/build-data/paper.at
new file mode 100644
index 000000000..48a44de36
--- /dev/null
+++ b/build-data/paper.at
@@ -0,0 +1,18 @@
+# You can use this file to change the access modifiers on a member
+# This line would make the field rollAmount public in Bee
+#public net.minecraft.world.entity.animal.Bee rollAmount
+# This line would make the field public and remove the final modifier
+#public-f net.minecraft.network.protocol.game.ClientboundChatPacket sender
+# Leave out the member and it will apply to the class itself
+# More info, see here https://mcforge.readthedocs.io/en/latest/advanced/accesstransformers/#access-modifiers
+
+# Remap/Decompile fix (unclear why this is happening)
+public net.minecraft.server.MinecraftServer doRunTask(Lnet/minecraft/server/TickTask;)V
+
+# AT remap issue? todo 1.18
+public net.minecraft.world.level.dimension.end.EndDragonFight findExitPortal()Lnet/minecraft/world/level/block/state/pattern/BlockPattern$BlockPatternMatch;
+public net.minecraft.nbt.TagParser readArrayTag()Lnet/minecraft/nbt/Tag;
+
+# TODO 1.20 remapSpigotAt.at doesn't remap the return type for this method for some reason
+public net/minecraft/world/entity/Display$TextDisplay getText()Lnet/minecraft/network/chat/Component;
+public net/minecraft/world/entity/Display$BlockDisplay getBlockState()Lnet/minecraft/world/level/block/state/BlockState;
diff --git a/build-data/reobf-mappings-patch.tiny b/build-data/reobf-mappings-patch.tiny
new file mode 100644
index 000000000..b14d9ed34
--- /dev/null
+++ b/build-data/reobf-mappings-patch.tiny
@@ -0,0 +1,28 @@
+# We would like for paperweight to generate 100% perfect reobf mappings (and deobf mappings for that matter).
+# But unfortunately it's not quite there yet - and it may be some time before that happens. Generating perfect mappings
+# from Spigot's mappings is extremely difficult due to Spigot's bad tooling and bad mappings. To add insult to injury
+# we remap Spigot's _source code_ which is a lot more complex and error-prone than bytecode remapping. So with all that
+# said, this file exists to help fill in the gap.
+#
+# We will continue to improve paperweight and will work on fixing these issues so they don't come up in the first place,
+# but these mappings exist to prevent these issues from holding everything else in Paper up while we work through all
+# of these issues. Due to the complex nature of mappings generation and the debugging difficulty involved it may take
+# a significant amount of time for us to track down every possible issue, so this file will likely be around and in
+# use - at least in some capacity - for a long time.
+
+tiny 2 0 mojang+yarn spigot
+
+# CraftBukkit changes type
+c net/minecraft/server/level/ServerLevel net/minecraft/server/level/WorldServer
+ f Lnet/minecraft/world/level/storage/PrimaryLevelData; serverLevelData L
+
+c net/minecraft/world/level/chunk/LevelChunk net/minecraft/world/level/chunk/Chunk
+ f Lnet/minecraft/server/level/ServerLevel; level r
+
+# See mappings-patch.tiny
+c net/minecraft/server/level/ChunkMap net/minecraft/server/level/PlayerChunkMap
+ f Lnet/minecraft/server/level/ChunkMap$ChunkDistanceManager; distanceManager G
+
+# The method is made public by Spigot, which then causes accidental overrides
+c net/minecraft/world/entity/Entity net/minecraft/world/entity/Entity
+ m ()Z isInRain isInRain0
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..cce2a91f7
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,296 @@
+import io.papermc.paperweight.PaperweightException
+import io.papermc.paperweight.tasks.BaseTask
+import io.papermc.paperweight.util.*
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+import java.io.ByteArrayOutputStream
+import java.nio.file.Path
+import java.util.regex.Pattern
+import kotlin.io.path.*
+
+plugins {
+ java
+ `maven-publish`
+ id("io.papermc.paperweight.core") version "1.7.7"
+}
+
+allprojects {
+ apply(plugin = "java")
+ apply(plugin = "maven-publish")
+
+ java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+ }
+}
+
+val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/"
+
+subprojects {
+ tasks.withType {
+ options.encoding = Charsets.UTF_8.name()
+ options.release = 21
+ options.isFork = true
+ }
+ tasks.withType {
+ options.encoding = Charsets.UTF_8.name()
+ }
+ tasks.withType {
+ filteringCharset = Charsets.UTF_8.name()
+ }
+ tasks.withType {
+ testLogging {
+ showStackTraces = true
+ exceptionFormat = TestExceptionFormat.FULL
+ events(TestLogEvent.STANDARD_OUT)
+ }
+ }
+
+ repositories {
+ mavenCentral()
+ maven(paperMavenPublicUrl)
+ }
+}
+
+val spigotDecompiler: Configuration by configurations.creating
+
+repositories {
+ mavenCentral()
+ maven(paperMavenPublicUrl) {
+ content {
+ onlyForConfigurations(
+ configurations.paperclip.name,
+ spigotDecompiler.name,
+ )
+ }
+ }
+}
+
+dependencies {
+ paramMappings("net.fabricmc:yarn:1.21.4+build.1:mergedv2")
+ remapper("net.fabricmc:tiny-remapper:0.10.3:fat")
+ decompiler("org.vineflower:vineflower:1.10.1")
+ spigotDecompiler("io.papermc:patched-spigot-fernflower:0.1+build.13")
+ paperclip("io.papermc:paperclip:3.0.3")
+}
+
+paperweight {
+ minecraftVersion = providers.gradleProperty("mcVersion")
+ serverProject = project(":paper-server")
+
+ paramMappingsRepo = paperMavenPublicUrl
+ remapRepo = paperMavenPublicUrl
+ decompileRepo = paperMavenPublicUrl
+
+ craftBukkit {
+ fernFlowerJar = layout.file(spigotDecompiler.elements.map { it.single().asFile })
+ }
+
+ paper {
+ spigotApiPatchDir = layout.projectDirectory.dir("patches/api")
+ spigotServerPatchDir = layout.projectDirectory.dir("patches/server")
+
+ mappingsPatch = layout.projectDirectory.file("build-data/mappings-patch.tiny")
+ reobfMappingsPatch = layout.projectDirectory.file("build-data/reobf-mappings-patch.tiny")
+
+ reobfPackagesToFix.addAll(
+ "co.aikar.timings",
+ "com.destroystokyo.paper",
+ "com.mojang",
+ "io.papermc.paper",
+ "ca.spottedleaf",
+ "net.kyori.adventure.bossbar",
+ "net.minecraft",
+ "org.bukkit.craftbukkit",
+ "org.spigotmc",
+ )
+ }
+}
+
+tasks.generateDevelopmentBundle {
+ apiCoordinates = "io.papermc.paper:paper-api"
+ libraryRepositories.addAll(
+ "https://repo.maven.apache.org/maven2/",
+ paperMavenPublicUrl,
+ )
+}
+
+publishing {
+ if (project.providers.gradleProperty("publishDevBundle").isPresent) {
+ publications.create("devBundle") {
+ artifact(tasks.generateDevelopmentBundle) {
+ artifactId = "dev-bundle"
+ }
+ }
+ }
+}
+
+allprojects {
+ publishing {
+ repositories {
+ maven("https://repo.papermc.io/repository/maven-snapshots/") {
+ name = "paperSnapshots"
+ credentials(PasswordCredentials::class)
+ }
+ }
+ }
+}
+
+tasks.register("printMinecraftVersion") {
+ doLast {
+ println(providers.gradleProperty("mcVersion").get().trim())
+ }
+}
+
+tasks.register("printPaperVersion") {
+ doLast {
+ println(project.version)
+ }
+}
+
+// see gradle.properties
+if (providers.gradleProperty("updatingMinecraft").getOrElse("false").toBoolean()) {
+ tasks.collectAtsFromPatches {
+ val dir = layout.projectDirectory.dir("patches/unapplied/server")
+ if (dir.path.isDirectory()) {
+ extraPatchDir = dir
+ }
+ }
+ tasks.withType().configureEach {
+ filterPatches = false
+ }
+ tasks.register("continueServerUpdate", RebasePatches::class) {
+ description = "Moves the next X patches from unapplied to applied, and applies them. X being the number of patches that apply cleanly, plus the terminal failure if any."
+ projectDir = project.projectDir
+ appliedPatches = file("patches/server")
+ unappliedPatches = file("patches/unapplied/server")
+ applyTaskName = "applyServerPatches"
+ patchedDir = "Paper-Server"
+ }
+}
+
+@UntrackedTask(because = "Does not make sense to track state")
+abstract class RebasePatches : BaseTask() {
+ @get:Internal
+ abstract val projectDir: DirectoryProperty
+
+ @get:InputFiles
+ abstract val appliedPatches: DirectoryProperty
+
+ @get:InputFiles
+ abstract val unappliedPatches: DirectoryProperty
+
+ @get:Input
+ abstract val applyTaskName: Property
+
+ @get:Input
+ abstract val patchedDir: Property
+
+ private fun unapplied(): List =
+ unappliedPatches.path.listDirectoryEntries("*.patch").sortedBy { it.name }
+
+ private fun appliedLoc(patch: Path): Path = appliedPatches.path.resolve(unappliedPatches.path.relativize(patch))
+
+ companion object {
+ val regex = Pattern.compile("Patch failed at ([0-9]{4}) (.*)")
+ val continuationRegex = Pattern.compile("^\\s{1}.+\$")
+ const val subjectPrefix = "Subject: [PATCH] "
+ }
+
+ @TaskAction
+ fun run() {
+ val patchedDirPath = projectDir.path.resolve(patchedDir.get())
+ if (patchedDirPath.isDirectory()) {
+ val status = Git(patchedDirPath)("status").getText()
+ if (status.contains("You are in the middle of an am session.")) {
+ throw PaperweightException("Cannot continue update when $patchedDirPath is in the middle of an am session.")
+ }
+ }
+
+ val unapplied = unapplied()
+ for (patch in unapplied) {
+ patch.copyTo(appliedLoc(patch))
+ }
+
+ val out = ByteArrayOutputStream()
+ val proc = ProcessBuilder()
+ .directory(projectDir.path)
+ .command("./gradlew", applyTaskName.get())
+ .redirectErrorStream(true)
+ .start()
+
+ val f = redirect(proc.inputStream, out)
+
+ val exit = proc.waitFor()
+ f.get()
+
+ if (exit != 0) {
+ val outStr = String(out.toByteArray())
+ val matcher = regex.matcher(outStr)
+ if (!matcher.find()) error("Could not determine failure point")
+ val failedSubjectFragment = matcher.group(2)
+ val failed = unapplied.single { p ->
+ p.useLines { lines ->
+ val collect = mutableListOf()
+ for (line in lines) {
+ if (line.startsWith(subjectPrefix)) {
+ collect += line
+ } else if (collect.size == 1) {
+ if (continuationRegex.matcher(line).matches()) {
+ collect += line
+ } else {
+ break
+ }
+ }
+ }
+ val subjectLine = collect.joinToString("").substringAfter(subjectPrefix)
+ subjectLine.startsWith(failedSubjectFragment)
+ }
+ }
+
+ // delete successful & failure point from unapplied patches dir
+ for (path in unapplied) {
+ path.deleteIfExists()
+ if (path == failed) {
+ break
+ }
+ }
+
+ // delete failed from patches dir
+ var started = false
+ for (path in unapplied) {
+ if (path == failed) {
+ started = true
+ continue
+ }
+ if (started) {
+ appliedLoc(path).deleteIfExists()
+ }
+ }
+
+ // Delete the build file before resetting the AM session in case it has compilation errors
+ patchedDirPath.resolve("build.gradle.kts").deleteIfExists()
+ // Apply again to reset the am session (so it ends on the failed patch, to allow us to rebuild after fixing it)
+ val apply2 = ProcessBuilder()
+ .directory(projectDir.path)
+ .command("./gradlew", applyTaskName.get())
+ .redirectErrorStream(true)
+ .start()
+
+ val f1 = redirect(apply2.inputStream, System.out)
+ apply2.waitFor()
+ f1.get()
+
+ logger.lifecycle(outStr)
+ logger.lifecycle("Patch failed at $failed; See Git output above.")
+ } else {
+ unapplied.forEach { it.deleteIfExists() }
+ logger.lifecycle("All patches applied!")
+ }
+
+ val git = Git(projectDir.path)
+ git("add", appliedPatches.path.toString() + "/*").runSilently()
+ git("add", unappliedPatches.path.toString() + "/*").runSilently()
+ }
+}
diff --git a/feature-patches/1038-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch b/feature-patches/1038-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch
new file mode 100644
index 000000000..bfa353b5d
--- /dev/null
+++ b/feature-patches/1038-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch
@@ -0,0 +1,99 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar
+Date: Thu, 3 Mar 2016 02:07:55 -0600
+Subject: [PATCH] Optimize isInWorldBounds and getBlockState for inlining
+
+Hot methods, so reduce # of instructions for the method.
+
+Move is valid location test to the BlockPosition class so that it can access local variables.
+
+Replace all calls to the new place to the unnecessary forward.
+
+Optimize getType and getBlockData to manually inline and optimize the calls
+
+diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/core/Vec3i.java
++++ b/src/main/java/net/minecraft/core/Vec3i.java
+@@ -0,0 +0,0 @@ public class Vec3i implements Comparable {
+ );
+ }
+
++ // Paper start
++ public final boolean isInsideBuildHeightAndWorldBoundsHorizontal(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) {
++ return getX() >= -30000000 && getZ() >= -30000000 && getX() < 30000000 && getZ() < 30000000 && !levelHeightAccessor.isOutsideBuildHeight(getY());
++ }
++ // Paper end
++
+ public Vec3i(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ // Paper end
+
+ public boolean isInWorldBounds(BlockPos pos) {
+- return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
++ return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - use better/optimized check
+ }
+
+ public static boolean isInSpawnableBounds(BlockPos pos) {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
+ return GameEventListenerRegistry.NOOP;
+ }
+
++ public abstract BlockState getBlockState(final int x, final int y, final int z); // Paper
+ @Nullable
+ public abstract BlockState setBlockState(BlockPos pos, BlockState state, boolean moved);
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+@@ -0,0 +0,0 @@ public class ImposterProtoChunk extends ProtoChunk {
+ public BlockState getBlockState(BlockPos pos) {
+ return this.wrapped.getBlockState(pos);
+ }
++ // Paper start
++ @Override
++ public final BlockState getBlockState(final int x, final int y, final int z) {
++ return this.wrapped.getBlockStateFinal(x, y, z);
++ }
++ // Paper end
+
+ @Override
+ public FluidState getFluidState(BlockPos pos) {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+@@ -0,0 +0,0 @@ public class ProtoChunk extends ChunkAccess {
+
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
+- int i = pos.getY();
+- if (this.isOutsideBuildHeight(i)) {
++ // Paper start
++ return getBlockState(pos.getX(), pos.getY(), pos.getZ());
++ }
++ public BlockState getBlockState(final int x, final int y, final int z) {
++ if (this.isOutsideBuildHeight(y)) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ } else {
+- LevelChunkSection levelChunkSection = this.getSection(this.getSectionIndex(i));
+- return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(pos.getX() & 15, i & 15, pos.getZ() & 15);
++ LevelChunkSection levelChunkSection = this.getSections()[this.getSectionIndex(y)];
++ return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(x & 15, y & 15, z & 15);
+ }
+ }
++ // Paper end
+
+ @Override
+ public FluidState getFluidState(BlockPos pos) {
diff --git a/feature-patches/1039-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch b/feature-patches/1039-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch
new file mode 100644
index 000000000..afafe8972
--- /dev/null
+++ b/feature-patches/1039-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch
@@ -0,0 +1,124 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar
+Date: Fri, 29 Apr 2016 20:02:00 -0400
+Subject: [PATCH] Improve Maps (in item frames) performance and bug fixes
+
+Maps used a modified version of rendering to support plugin controlled
+imaging on maps. The Craft Map Renderer is much slower than Vanilla,
+causing maps in item frames to cause a noticeable hit on server performance.
+
+This updates the map system to not use the Craft system if we detect that no
+custom renderers are in use, defaulting to the much simpler Vanilla system.
+
+Additionally, numerous issues to player position tracking on maps has been fixed.
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ {
+ if ( iter.next().player == entity )
+ {
++ map.decorations.remove(entity.getName().getString()); // Paper
+ iter.remove();
+ }
+ }
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -0,0 +0,0 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ this.awardStat(Stats.DROP);
+ }
+
++ // Paper start - remove player from map on drop
++ if (itemstack.getItem() == net.minecraft.world.item.Items.FILLED_MAP) {
++ net.minecraft.world.level.saveddata.maps.MapItemSavedData worldmap = net.minecraft.world.item.MapItem.getSavedData(itemstack, this.level());
++ if (worldmap != null) {
++ worldmap.tickCarriedBy(this, itemstack);
++ }
++ }
++ // Paper end
+ return entityitem;
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+ public final Map decorations = Maps.newLinkedHashMap();
+ private final Map frameMarkers = Maps.newHashMap();
+ private int trackedDecorationCount;
++ private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper
+
+ // CraftBukkit start
+ public final CraftMapView mapView;
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+ // CraftBukkit start
+ this.mapView = new CraftMapView(this);
+ this.server = (CraftServer) org.bukkit.Bukkit.getServer();
++ this.vanillaRender.buffer = colors; // Paper
+ // CraftBukkit end
+ }
+
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+ if (abyte.length == 16384) {
+ worldmap.colors = abyte;
+ }
++ worldmap.vanillaRender.buffer = abyte; // Paper
+
+ RegistryOps registryops = registries.createSerializationContext(NbtOps.INSTANCE);
+ List list = (List) MapBanner.LIST_CODEC.parse(registryops, nbt.get("banners")).resultOrPartial((s) -> {
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+ --this.trackedDecorationCount;
+ }
+
+- this.setDecorationsDirty();
++ if (mapicon != null) this.setDecorationsDirty(); // Paper - only mark dirty if a change occurs
+ }
+
+ public static void addTargetDecoration(ItemStack stack, BlockPos pos, String id, Holder decorationType) {
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+
+ public class HoldingPlayer {
+
++ // Paper start
++ private void addSeenPlayers(java.util.Collection icons) {
++ org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity();
++ MapItemSavedData.this.decorations.forEach((name, mapIcon) -> {
++ // If this cursor is for a player check visibility with vanish system
++ org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot
++ if (other == null || player.canSee(other)) {
++ icons.add(mapIcon);
++ }
++ });
++ }
++ private boolean shouldUseVanillaMap() {
++ return mapView.getRenderers().size() == 1 && mapView.getRenderers().get(0).getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class;
++ }
++ // Paper end
+ public final Player player;
+ private boolean dirtyData = true;
+ private int minDirtyX;
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+ @Nullable
+ Packet> nextUpdatePacket(MapId mapId) {
+ MapItemSavedData.MapPatch worldmap_c;
+- org.bukkit.craftbukkit.map.RenderData render = MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()); // CraftBukkit
++ if (!this.dirtyData && this.tick % 5 != 0) { this.tick++; return null; } // Paper - this won't end up sending, so don't render it!
++ boolean vanillaMaps = shouldUseVanillaMap(); // Paper
++ org.bukkit.craftbukkit.map.RenderData render = !vanillaMaps ? MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()) : MapItemSavedData.this.vanillaRender; // CraftBukkit // Paper
+
+ if (this.dirtyData) {
+ this.dirtyData = false;
+@@ -0,0 +0,0 @@ public class MapItemSavedData extends SavedData {
+ // CraftBukkit start
+ java.util.Collection icons = new java.util.ArrayList();
+
++ if (vanillaMaps) addSeenPlayers(icons); // Paper
++
+ for (org.bukkit.map.MapCursor cursor : render.cursors) {
+ if (cursor.isVisible()) {
+ icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption()))));
diff --git a/feature-patches/1040-Optimize-Network-Manager-and-add-advanced-packet-sup.patch b/feature-patches/1040-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
new file mode 100644
index 000000000..51ccefe73
--- /dev/null
+++ b/feature-patches/1040-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
@@ -0,0 +1,395 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar
+Date: Wed, 6 May 2020 04:53:35 -0400
+Subject: [PATCH] Optimize Network Manager and add advanced packet support
+
+Adds ability for 1 packet to bundle other packets to follow it
+Adds ability for a packet to delay sending more packets until a state is ready.
+
+Removes synchronization from sending packets
+Removes processing packet queue off of main thread
+ - for the few cases where it is allowed, order is not necessary nor
+ should it even be happening concurrently in first place (handshaking/login/status)
+
+Ensures packets sent asynchronously are dispatched on main thread
+
+This helps ensure safety for ProtocolLib as packet listeners
+are commonly accessing world state. This will allow you to schedule
+a packet to be sent async, but itll be dispatched sync for packet
+listeners to process.
+
+This should solve some deadlock risks
+
+Also adds Netty Channel Flush Consolidation to reduce the amount of flushing
+
+Also avoids spamming closed channel exception by rechecking closed state in dispatch
+and then catch exceptions and close if they fire.
+
+Part of this commit was authored by: Spottedleaf, sandtechnology
+
+diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/Connection.java
++++ b/src/main/java/net/minecraft/network/Connection.java
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
+ private final PacketFlow receiving;
+ private volatile boolean sendLoginDisconnect = true;
+- private final Queue> pendingActions = Queues.newConcurrentLinkedQueue();
++ private final Queue pendingActions = Queues.newConcurrentLinkedQueue(); // Paper
+ public Channel channel;
+ public SocketAddress address;
+ // Spigot Start
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ public java.net.InetSocketAddress virtualHost;
+ private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing
+ // Paper end
++ // Paper start - Optimize network
++ public boolean isPending = true;
++ public boolean queueImmunity;
++ // Paper end - Optimize network
+
+ // Paper start - add utility methods
+ public final net.minecraft.server.level.ServerPlayer getPlayer() {
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ }
+
+ public void send(Packet> packet, @Nullable PacketSendListener callbacks, boolean flush) {
+- if (this.isConnected()) {
+- this.flushQueue();
++ // Paper start - Optimize network: Handle oversized packets better
++ final boolean connected = this.isConnected();
++ if (!connected && !this.preparing) {
++ return;
++ }
++
++ packet.onPacketDispatch(this.getPlayer());
++ if (connected && (InnerUtil.canSendImmediate(this, packet)
++ || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty()
++ && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) {
+ this.sendPacket(packet, callbacks, flush);
+ } else {
+- this.pendingActions.add((networkmanager) -> {
+- networkmanager.sendPacket(packet, callbacks, flush);
+- });
+- }
++ // Write the packets to the queue, then flush - antixray hooks there already
++ final java.util.List> extraPackets = InnerUtil.buildExtraPackets(packet);
++ final boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty();
++ if (!hasExtraPackets) {
++ this.pendingActions.add(new PacketSendAction(packet, callbacks, flush));
++ } else {
++ final java.util.List actions = new java.util.ArrayList<>(1 + extraPackets.size());
++ actions.add(new PacketSendAction(packet, null, false)); // Delay the future listener until the end of the extra packets
++
++ for (int i = 0, len = extraPackets.size(); i < len;) {
++ final Packet> extraPacket = extraPackets.get(i);
++ final boolean end = ++i == len;
++ actions.add(new PacketSendAction(extraPacket, end ? callbacks : null, end)); // Append listener to the end
++ }
++
++ this.pendingActions.addAll(actions);
++ }
+
++ this.flushQueue();
++ // Paper end - Optimize network
++ }
+ }
+
+ public void runOnceConnected(Consumer task) {
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ this.flushQueue();
+ task.accept(this);
+ } else {
+- this.pendingActions.add(task);
++ this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network
+ }
+
+ }
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ }
+
+ private void doSendPacket(Packet> packet, @Nullable PacketSendListener callbacks, boolean flush) {
++ // Paper start - Optimize network
++ final net.minecraft.server.level.ServerPlayer player = this.getPlayer();
++ if (!this.isConnected()) {
++ packet.onPacketDispatchFinish(player, null);
++ return;
++ }
++ try {
++ // Paper end - Optimize network
+ ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet);
+
+ if (callbacks != null) {
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ });
+ }
+
++ // Paper start - Optimize network
++ if (packet.hasFinishListener()) {
++ channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture));
++ }
+ channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
++ } catch (final Exception e) {
++ LOGGER.error("NetworkException: {}", player, e);
++ this.disconnect(Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage()));
++ packet.onPacketDispatchFinish(player, null);
++ }
++ // Paper end - Optimize network
+ }
+
+ public void flushChannel() {
+ if (this.isConnected()) {
+ this.flush();
+ } else {
+- this.pendingActions.add(Connection::flush);
++ this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network
+ }
+
+ }
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+
+ }
+
+- private void flushQueue() {
+- if (this.channel != null && this.channel.isOpen()) {
+- Queue queue = this.pendingActions;
+-
++ // Paper start - Optimize network: Rewrite this to be safer if ran off main thread
++ private boolean flushQueue() {
++ if (!this.isConnected()) {
++ return true;
++ }
++ if (io.papermc.paper.util.MCUtil.isMainThread()) {
++ return this.processQueue();
++ } else if (this.isPending) {
++ // Should only happen during login/status stages
+ synchronized (this.pendingActions) {
+- Consumer consumer;
++ return this.processQueue();
++ }
++ }
++ return false;
++ }
++
++ private boolean processQueue() {
++ if (this.pendingActions.isEmpty()) {
++ return true;
++ }
+
+- while ((consumer = (Consumer) this.pendingActions.poll()) != null) {
+- consumer.accept(this);
++ // If we are on main, we are safe here in that nothing else should be processing queue off main anymore
++ // But if we are not on main due to login/status, the parent is synchronized on packetQueue
++ final java.util.Iterator iterator = this.pendingActions.iterator();
++ while (iterator.hasNext()) {
++ final WrappedConsumer queued = iterator.next(); // poll -> peek
++
++ // Fix NPE (Spigot bug caused by handleDisconnection())
++ if (queued == null) {
++ return true;
++ }
++
++ if (queued.isConsumed()) {
++ continue;
++ }
++
++ if (queued instanceof PacketSendAction packetSendAction) {
++ final Packet> packet = packetSendAction.packet;
++ if (!packet.isReady()) {
++ return false;
+ }
++ }
+
++ iterator.remove();
++ if (queued.tryMarkConsumed()) {
++ queued.accept(this);
+ }
+ }
++ return true;
+ }
++ // Paper end - Optimize network
+
+ private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
+ private static int joinAttemptsThisTick; // Paper - Buffer joins to world
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ public void disconnect(DisconnectionDetails disconnectionInfo) {
+ // Spigot Start
+ this.preparing = false;
++ this.clearPacketQueue(); // Paper - Optimize network
+ // Spigot End
+ if (this.channel == null) {
+ this.delayedDisconnect = disconnectionInfo;
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ public void handleDisconnection() {
+ if (this.channel != null && !this.channel.isOpen()) {
+ if (this.disconnectionHandled) {
+- Connection.LOGGER.warn("handleDisconnection() called twice");
++ // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message
+ } else {
+ this.disconnectionHandled = true;
+ PacketListener packetlistener = this.getPacketListener();
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+
+ packetlistener1.onDisconnect(disconnectiondetails);
+ }
+- this.pendingActions.clear(); // Free up packet queue.
++ this.clearPacketQueue(); // Paper - Optimize network
+ // Paper start - Add PlayerConnectionCloseEvent
+ final PacketListener packetListener = this.getPacketListener();
+ if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ public void setBandwidthLogger(LocalSampleLogger log) {
+ this.bandwidthDebugMonitor = new BandwidthDebugMonitor(log);
+ }
++
++ // Paper start - Optimize network
++ public void clearPacketQueue() {
++ final net.minecraft.server.level.ServerPlayer player = getPlayer();
++ for (final Consumer queuedAction : this.pendingActions) {
++ if (queuedAction instanceof PacketSendAction packetSendAction) {
++ final Packet> packet = packetSendAction.packet;
++ if (packet.hasFinishListener()) {
++ packet.onPacketDispatchFinish(player, null);
++ }
++ }
++ }
++ this.pendingActions.clear();
++ }
++
++ private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up.
++
++ @Nullable
++ private static java.util.List> buildExtraPackets(final Packet> packet) {
++ final java.util.List> extra = packet.getExtraPackets();
++ if (extra == null || extra.isEmpty()) {
++ return null;
++ }
++
++ final java.util.List> ret = new java.util.ArrayList<>(1 + extra.size());
++ buildExtraPackets0(extra, ret);
++ return ret;
++ }
++
++ private static void buildExtraPackets0(final java.util.List> extraPackets, final java.util.List> into) {
++ for (final Packet> extra : extraPackets) {
++ into.add(extra);
++ final java.util.List> extraExtra = extra.getExtraPackets();
++ if (extraExtra != null && !extraExtra.isEmpty()) {
++ buildExtraPackets0(extraExtra, into);
++ }
++ }
++ }
++
++ private static boolean canSendImmediate(final Connection networkManager, final net.minecraft.network.protocol.Packet> packet) {
++ return networkManager.isPending || networkManager.packetListener.protocol() != ConnectionProtocol.PLAY ||
++ packet instanceof net.minecraft.network.protocol.common.ClientboundKeepAlivePacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSystemChatPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundClearTitlesPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSoundPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundSoundEntityPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundStopSoundPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket ||
++ packet instanceof net.minecraft.network.protocol.game.ClientboundBossEventPacket;
++ }
++ }
++
++ private static class WrappedConsumer implements Consumer {
++ private final Consumer delegate;
++ private final java.util.concurrent.atomic.AtomicBoolean consumed = new java.util.concurrent.atomic.AtomicBoolean(false);
++
++ private WrappedConsumer(final Consumer delegate) {
++ this.delegate = delegate;
++ }
++
++ @Override
++ public void accept(final Connection connection) {
++ this.delegate.accept(connection);
++ }
++
++ public boolean tryMarkConsumed() {
++ return consumed.compareAndSet(false, true);
++ }
++
++ public boolean isConsumed() {
++ return consumed.get();
++ }
++ }
++
++ private static final class PacketSendAction extends WrappedConsumer {
++ private final Packet> packet;
++
++ private PacketSendAction(final Packet> packet, @Nullable final PacketSendListener packetSendListener, final boolean flush) {
++ super(connection -> connection.sendPacket(packet, packetSendListener, flush));
++ this.packet = packet;
++ }
++ }
++ // Paper end - Optimize network
+ }
+diff --git a/src/main/java/net/minecraft/network/protocol/Packet.java b/src/main/java/net/minecraft/network/protocol/Packet.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/protocol/Packet.java
++++ b/src/main/java/net/minecraft/network/protocol/Packet.java
+@@ -0,0 +0,0 @@ public interface Packet {
+ static > StreamCodec codec(StreamMemberEncoder encoder, StreamDecoder decoder) {
+ return StreamCodec.ofMember(encoder, decoder);
+ }
++
++ // Paper start
++ /**
++ * @param player Null if not at PLAY stage yet
++ */
++ default void onPacketDispatch(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player) {
++ }
++
++ /**
++ * @param player Null if not at PLAY stage yet
++ * @param future Can be null if packet was cancelled
++ */
++ default void onPacketDispatchFinish(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player, @org.jetbrains.annotations.Nullable io.netty.channel.ChannelFuture future) {}
++
++ default boolean hasFinishListener() {
++ return false;
++ }
++
++ default boolean isReady() {
++ return true;
++ }
++
++ @org.jetbrains.annotations.Nullable
++ default java.util.List> getExtraPackets() {
++ return null;
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
+@@ -0,0 +0,0 @@ public class ServerConnectionListener {
+ final List connections = Collections.synchronizedList(Lists.newArrayList());
+ // Paper start - prevent blocking on adding a new connection while the server is ticking
+ private final java.util.Queue pending = new java.util.concurrent.ConcurrentLinkedQueue<>();
++ private static final boolean disableFlushConsolidation = Boolean.getBoolean("Paper.disableFlushConsolidate"); // Paper - Optimize network
+ private final void addPending() {
+ Connection connection;
+ while ((connection = pending.poll()) != null) {
+ connections.add(connection);
++ connection.isPending = false; // Paper - Optimize network
+ }
+ }
+ // Paper end - prevent blocking on adding a new connection while the server is ticking
+@@ -0,0 +0,0 @@ public class ServerConnectionListener {
+ ;
+ }
+
++ if (!disableFlushConsolidation) channel.pipeline().addFirst(new io.netty.handler.flush.FlushConsolidationHandler()); // Paper - Optimize network
+ ChannelPipeline channelpipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30));
+
+ if (ServerConnectionListener.this.server.repliesToStatus()) {
diff --git a/feature-patches/1041-Allow-Saving-of-Oversized-Chunks.patch b/feature-patches/1041-Allow-Saving-of-Oversized-Chunks.patch
new file mode 100644
index 000000000..6c06181f4
--- /dev/null
+++ b/feature-patches/1041-Allow-Saving-of-Oversized-Chunks.patch
@@ -0,0 +1,202 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar
+Date: Fri, 15 Feb 2019 01:08:19 -0500
+Subject: [PATCH] Allow Saving of Oversized Chunks
+
+The Minecraft World Region File format has a hard cap of 1MB per chunk.
+This is due to the fact that the header of the file format only allocates
+a single byte for sector count, meaning a maximum of 256 sectors, at 4k per sector.
+
+This limit can be reached fairly easily with books, resulting in the chunk being unable
+to save to the world. Worse off, is that nothing printed when this occured, and silently
+performed a chunk rollback on next load.
+
+This leads to security risk with duplication and is being actively exploited.
+
+This patch catches the too large scenario, falls back and moves any large Entity
+or Tile Entity into a new compound, and this compound is saved into a different file.
+
+On Chunk Load, we check for oversized status, and if so, we load the extra file and
+merge the Entities and Tile Entities from the oversized chunk back into the level to
+then be loaded as normal.
+
+Once a chunk is returned back to normal size, the oversized flag will clear, and no
+extra data file will exist.
+
+This fix maintains compatability with all existing Anvil Region Format tools as it
+does not alter the save format. They will just not know about the extra entities.
+
+This fix also maintains compatability if someone switches server jars to one without
+this fix, as the data will remain in the oversized file. Once the server returns
+to a jar with this fix, the data will be restored.
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -0,0 +0,0 @@ import java.nio.file.LinkOption;
+ import java.nio.file.Path;
+ import java.nio.file.StandardCopyOption;
+ import java.nio.file.StandardOpenOption;
++import java.util.zip.InflaterInputStream; // Paper
+ import javax.annotation.Nullable;
+ import net.minecraft.Util;
+ import net.minecraft.resources.ResourceLocation;
+ import net.minecraft.util.profiling.jfr.JvmProfiler;
++import net.minecraft.nbt.CompoundTag; // Paper
++import net.minecraft.nbt.NbtIo; // Paper
+ import net.minecraft.world.level.ChunkPos;
+ import org.slf4j.Logger;
+
+@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
+ this.usedSectors = new RegionBitmap();
+ this.info = storageKey;
+ this.path = path;
++ initOversizedState(); // Paper
+ this.version = compressionFormat;
+ if (!Files.isDirectory(directory, new LinkOption[0])) {
+ throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath()));
+@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
+
+ }
+
++ // Paper start
++ private final byte[] oversized = new byte[1024];
++ private int oversizedCount;
++
++ private synchronized void initOversizedState() throws IOException {
++ Path metaFile = getOversizedMetaFile();
++ if (Files.exists(metaFile)) {
++ final byte[] read = java.nio.file.Files.readAllBytes(metaFile);
++ System.arraycopy(read, 0, oversized, 0, oversized.length);
++ for (byte temp : oversized) {
++ oversizedCount += temp;
++ }
++ }
++ }
++
++ private static int getChunkIndex(int x, int z) {
++ return (x & 31) + (z & 31) * 32;
++ }
++ synchronized boolean isOversized(int x, int z) {
++ return this.oversized[getChunkIndex(x, z)] == 1;
++ }
++ synchronized void setOversized(int x, int z, boolean oversized) throws IOException {
++ final int offset = getChunkIndex(x, z);
++ boolean previous = this.oversized[offset] == 1;
++ this.oversized[offset] = (byte) (oversized ? 1 : 0);
++ if (!previous && oversized) {
++ oversizedCount++;
++ } else if (!oversized && previous) {
++ oversizedCount--;
++ }
++ if (previous && !oversized) {
++ Path oversizedFile = getOversizedFile(x, z);
++ if (Files.exists(oversizedFile)) {
++ Files.delete(oversizedFile);
++ }
++ }
++ if (oversizedCount > 0) {
++ if (previous != oversized) {
++ writeOversizedMeta();
++ }
++ } else if (previous) {
++ Path oversizedMetaFile = getOversizedMetaFile();
++ if (Files.exists(oversizedMetaFile)) {
++ Files.delete(oversizedMetaFile);
++ }
++ }
++ }
++
++ private void writeOversizedMeta() throws IOException {
++ java.nio.file.Files.write(getOversizedMetaFile(), oversized);
++ }
++
++ private Path getOversizedMetaFile() {
++ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + ".oversized.nbt");
++ }
++
++ private Path getOversizedFile(int x, int z) {
++ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt");
++ }
++
++ synchronized CompoundTag getOversizedData(int x, int z) throws IOException {
++ Path file = getOversizedFile(x, z);
++ try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) {
++ return NbtIo.read((java.io.DataInput) out);
++ }
++
++ }
++ // Paper end
+ private class ChunkBuffer extends ByteArrayOutputStream {
+
+ private final ChunkPos pos;
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
+ }
+ }
+
++ // Paper start
++ private static void printOversizedLog(String msg, Path file, int x, int z) {
++ org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed.");
++ }
++
++ private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException {
++ synchronized (regionfile) {
++ try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) {
++ CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z);
++ CompoundTag chunk = NbtIo.read((DataInput) datainputstream);
++ if (oversizedData == null) {
++ return chunk;
++ }
++ CompoundTag oversizedLevel = oversizedData.getCompound("Level");
++
++ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "Entities", "Entities");
++ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "TileEntities", "TileEntities");
++
++ return chunk;
++ } catch (Throwable throwable) {
++ throwable.printStackTrace();
++ throw throwable;
++ }
++ }
++ }
++
++ private static void mergeChunkList(CompoundTag level, CompoundTag oversizedLevel, String key, String oversizedKey) {
++ net.minecraft.nbt.ListTag levelList = level.getList(key, net.minecraft.nbt.Tag.TAG_COMPOUND);
++ net.minecraft.nbt.ListTag oversizedList = oversizedLevel.getList(oversizedKey, net.minecraft.nbt.Tag.TAG_COMPOUND);
++
++ if (!oversizedList.isEmpty()) {
++ levelList.addAll(oversizedList);
++ level.put(key, levelList);
++ }
++ }
++ // Paper end
++
+ @Nullable
+ public CompoundTag read(ChunkPos pos) throws IOException {
+ // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
+@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
+ // CraftBukkit end
+ DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos);
+
++ // Paper start
++ if (regionfile.isOversized(pos.x, pos.z)) {
++ printOversizedLog("Loading Oversized Chunk!", regionfile.getPath(), pos.x, pos.z);
++ return readOversizedChunk(regionfile, pos);
++ }
++ // Paper end
+ CompoundTag nbttagcompound;
+ label43:
+ {
+@@ -0,0 +0,0 @@ public final class RegionFileStorage implements AutoCloseable {
+
+ try {
+ NbtIo.write(nbt, (DataOutput) dataoutputstream);
++ regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
+ } catch (Throwable throwable) {
+ if (dataoutputstream != null) {
+ try {
diff --git a/feature-patches/1042-Flat-bedrock-generator-settings.patch b/feature-patches/1042-Flat-bedrock-generator-settings.patch
new file mode 100644
index 000000000..4d8c54d24
--- /dev/null
+++ b/feature-patches/1042-Flat-bedrock-generator-settings.patch
@@ -0,0 +1,292 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Byteflux
+Date: Wed, 2 Mar 2016 02:17:54 -0600
+Subject: [PATCH] Flat bedrock generator settings
+
+== AT ==
+public net.minecraft.world.level.levelgen.SurfaceRules$Condition
+public net.minecraft.world.level.levelgen.SurfaceRules$Context
+public net.minecraft.world.level.levelgen.SurfaceRules$Context blockX
+public net.minecraft.world.level.levelgen.SurfaceRules$Context blockY
+public net.minecraft.world.level.levelgen.SurfaceRules$Context blockZ
+public net.minecraft.world.level.levelgen.SurfaceRules$Context context
+public net.minecraft.world.level.levelgen.SurfaceRules$Context randomState
+public net.minecraft.world.level.levelgen.SurfaceRules$LazyYCondition
+public net.minecraft.world.level.levelgen.SurfaceRules$LazyCondition
+public net.minecraft.world.level.levelgen.SurfaceRules$VerticalGradientConditionSource
+public net.minecraft.world.level.levelgen.SurfaceRules$SurfaceRule
+
+Co-authored-by: Noah van der Aa
+
+diff --git a/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.world.worldgen;
++
++import com.mojang.serialization.Codec;
++import com.mojang.serialization.MapCodec;
++import com.mojang.serialization.codecs.RecordCodecBuilder;
++import net.minecraft.core.Registry;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.util.KeyDispatchDataCodec;
++import net.minecraft.util.Mth;
++import net.minecraft.util.RandomSource;
++import net.minecraft.world.level.levelgen.PositionalRandomFactory;
++import net.minecraft.world.level.levelgen.SurfaceRules;
++import net.minecraft.world.level.levelgen.VerticalAnchor;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++// Modelled off of SurfaceRules$VerticalGradientConditionSource
++@DefaultQualifier(NonNull.class)
++public record OptionallyFlatBedrockConditionSource(ResourceLocation randomName, VerticalAnchor trueAtAndBelow, VerticalAnchor falseAtAndAbove, boolean isRoof) implements SurfaceRules.ConditionSource {
++
++ private static final ResourceKey> CODEC_RESOURCE_KEY = ResourceKey.create(
++ Registries.MATERIAL_CONDITION,
++ ResourceLocation.fromNamespaceAndPath(ResourceLocation.PAPER_NAMESPACE, "optionally_flat_bedrock_condition_source")
++ );
++ private static final KeyDispatchDataCodec CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> {
++ return instance.group(
++ ResourceLocation.CODEC.fieldOf("random_name").forGetter(OptionallyFlatBedrockConditionSource::randomName),
++ VerticalAnchor.CODEC.fieldOf("true_at_and_below").forGetter(OptionallyFlatBedrockConditionSource::trueAtAndBelow),
++ VerticalAnchor.CODEC.fieldOf("false_at_and_above").forGetter(OptionallyFlatBedrockConditionSource::falseAtAndAbove),
++ Codec.BOOL.fieldOf("is_roof").forGetter(OptionallyFlatBedrockConditionSource::isRoof)
++ ).apply(instance, OptionallyFlatBedrockConditionSource::new);
++ }));
++
++ public static void bootstrap() {
++ Registry.register(BuiltInRegistries.MATERIAL_CONDITION, CODEC_RESOURCE_KEY, CODEC.codec());
++ }
++
++ @Override
++ public KeyDispatchDataCodec extends SurfaceRules.ConditionSource> codec() {
++ return CODEC;
++ }
++
++ @Override
++ public SurfaceRules.Condition apply(final SurfaceRules.Context context) {
++ boolean hasFlatBedrock = context.context.getWorld().paperConfig().environment.generateFlatBedrock;
++ int tempTrueAtAndBelowY = this.trueAtAndBelow().resolveY(context.context);
++ int tempFalseAtAndAboveY = this.falseAtAndAbove().resolveY(context.context);
++
++ int flatYLevel = this.isRoof ? Math.max(tempFalseAtAndAboveY, tempTrueAtAndBelowY) - 1 : Math.min(tempFalseAtAndAboveY, tempTrueAtAndBelowY);
++ final int trueAtAndBelowY = hasFlatBedrock ? flatYLevel : tempTrueAtAndBelowY;
++ final int falseAtAndAboveY = hasFlatBedrock ? flatYLevel : tempFalseAtAndAboveY;
++
++ final PositionalRandomFactory positionalRandomFactory = context.randomState.getOrCreateRandomFactory(this.randomName());
++
++ class VerticalGradientCondition extends SurfaceRules.LazyYCondition {
++ VerticalGradientCondition(SurfaceRules.Context context) {
++ super(context);
++ }
++
++ @Override
++ protected boolean compute() {
++ int blockY = this.context.blockY;
++ if (blockY <= trueAtAndBelowY) {
++ return true;
++ } else if (blockY >= falseAtAndAboveY) {
++ return false;
++ } else {
++ double d = Mth.map(blockY, trueAtAndBelowY, falseAtAndAboveY, 1.0D, 0.0D);
++ RandomSource randomSource = positionalRandomFactory.at(this.context.blockX, blockY, this.context.blockZ);
++ return (double)randomSource.nextFloat() < d;
++ }
++ }
++ }
++
++ return new VerticalGradientCondition(context);
++ }
++}
+diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/Bootstrap.java
++++ b/src/main/java/net/minecraft/server/Bootstrap.java
+@@ -0,0 +0,0 @@ public class Bootstrap {
+ CauldronInteraction.bootStrap();
+ // Paper start
+ BuiltInRegistries.bootStrap(() -> {
++ io.papermc.paper.world.worldgen.OptionallyFlatBedrockConditionSource.bootstrap(); // Paper - Flat bedrock generator settings
+ });
+ // Paper end
+ CreativeModeTabs.validate();
+diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
++++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+ @Override
+ public void buildSurface(WorldGenRegion region, StructureManager structures, RandomState noiseConfig, ChunkAccess chunk) {
+ if (!SharedConstants.debugVoidTerrain(chunk.getPos())) {
+- WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region);
++ WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region, region.getMinecraftWorld()); // Paper - Flat bedrock generator settings
+
+ this.buildSurface(chunk, worldgenerationcontext, noiseConfig, structures, region.getBiomeManager(), region.registryAccess().lookupOrThrow(Registries.BIOME), Blender.of(region));
+ }
+@@ -0,0 +0,0 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
+ return this.createNoiseChunk(ichunkaccess1, structureAccessor, Blender.of(chunkRegion), noiseConfig);
+ });
+ Aquifer aquifer = noisechunk.aquifer();
+- CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule());
++ CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule(), chunkRegion.getMinecraftWorld()); // Paper - Flat bedrock generator settings
+ CarvingMask carvingmask = ((ProtoChunk) chunk).getOrCreateCarvingMask();
+
+ for (int j = -8; j <= 8; ++j) {
+diff --git a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java
++++ b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java
+@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
+ public class WorldGenerationContext {
+ private final int minY;
+ private final int height;
++ private final @javax.annotation.Nullable net.minecraft.world.level.Level level; // Paper - Flat bedrock generator settings
+
+- public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) {
++ public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) { this(generator, world, null); } // Paper - Flat bedrock generator settings
++ public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world, @org.jetbrains.annotations.Nullable net.minecraft.world.level.Level level) { // Paper - Flat bedrock generator settings
+ this.minY = Math.max(world.getMinY(), generator.getMinY());
+ this.height = Math.min(world.getHeight(), generator.getGenDepth());
++ this.level = level; // Paper - Flat bedrock generator settings
+ }
+
+ public int getMinGenY() {
+@@ -0,0 +0,0 @@ public class WorldGenerationContext {
+ public int getGenDepth() {
+ return this.height;
+ }
++
++ // Paper start - Flat bedrock generator settings
++ public net.minecraft.world.level.Level getWorld() {
++ if (this.level == null) {
++ throw new NullPointerException("WorldGenerationContext was initialized without a Level, but WorldGenerationContext#getWorld was called");
++ }
++ return this.level;
++ }
++ // Paper end - Flat bedrock generator settings
+ }
+diff --git a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java
++++ b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java
+@@ -0,0 +0,0 @@ public class CarvingContext extends WorldGenerationContext {
+ LevelHeightAccessor heightLimitView,
+ NoiseChunk chunkNoiseSampler,
+ RandomState noiseConfig,
+- SurfaceRules.RuleSource materialRule
++ SurfaceRules.RuleSource materialRule, @javax.annotation.Nullable net.minecraft.world.level.Level level // Paper - Flat bedrock generator settings
+ ) {
+- super(noiseChunkGenerator, heightLimitView);
++ super(noiseChunkGenerator, heightLimitView, level); // Paper - Flat bedrock generator settings
+ this.registryAccess = registryManager;
+ this.noiseChunk = chunkNoiseSampler;
+ this.randomState = noiseConfig;
+diff --git a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java
++++ b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java
+@@ -0,0 +0,0 @@ public class PlacementContext extends WorldGenerationContext {
+ private final Optional topFeature;
+
+ public PlacementContext(WorldGenLevel world, ChunkGenerator generator, Optional placedFeature) {
+- super(generator, world);
++ super(generator, world, world.getLevel()); // Paper - Flat bedrock generator settings
+ this.level = world;
+ this.generator = generator;
+ this.topFeature = placedFeature;
+diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json
++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json
+@@ -0,0 +0,0 @@
+ {
+ "type": "minecraft:condition",
+ "if_true": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": false,
+ "false_at_and_above": {
+ "above_bottom": 5
+ },
+diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json
++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json
+@@ -0,0 +0,0 @@
+ "if_true": {
+ "type": "minecraft:not",
+ "invert": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": true,
+ "false_at_and_above": {
+ "below_top": 0
+ },
+@@ -0,0 +0,0 @@
+ {
+ "type": "minecraft:condition",
+ "if_true": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": false,
+ "false_at_and_above": {
+ "above_bottom": 5
+ },
+diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json
++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json
+@@ -0,0 +0,0 @@
+ {
+ "type": "minecraft:condition",
+ "if_true": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": false,
+ "false_at_and_above": {
+ "above_bottom": 5
+ },
+diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json
++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json
+@@ -0,0 +0,0 @@
+ {
+ "type": "minecraft:condition",
+ "if_true": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": false,
+ "false_at_and_above": {
+ "above_bottom": 5
+ },
+@@ -0,0 +0,0 @@
+ "if_true": {
+ "type": "minecraft:not",
+ "invert": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": true,
+ "false_at_and_above": {
+ "below_top": 0
+ },
+diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json
++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json
+@@ -0,0 +0,0 @@
+ {
+ "type": "minecraft:condition",
+ "if_true": {
+- "type": "minecraft:vertical_gradient",
++ "type": "paper:optionally_flat_bedrock_condition_source",
++ "is_roof": false,
+ "false_at_and_above": {
+ "above_bottom": 5
+ },
diff --git a/feature-patches/1043-Entity-Activation-Range-2.0.patch b/feature-patches/1043-Entity-Activation-Range-2.0.patch
new file mode 100644
index 000000000..d9a21e090
--- /dev/null
+++ b/feature-patches/1043-Entity-Activation-Range-2.0.patch
@@ -0,0 +1,775 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar
+Date: Fri, 13 May 2016 01:38:06 -0400
+Subject: [PATCH] Entity Activation Range 2.0
+
+Optimizes performance of Activation Range
+
+Adds many new configurations and a new wake up inactive system
+
+Fixes and adds new Immunities to improve gameplay behavior
+
+Adds water Mobs to activation range config and nerfs fish
+Adds flying monsters to control ghast and phantoms
+Adds villagers as separate config
+
+== AT ==
+public net.minecraft.world.entity.Entity isInsidePortal
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+
+ public void tickNonPassenger(Entity entity) {
+ // Spigot start
+- if (!org.spigotmc.ActivationRange.checkIfActive(entity)) {
++ /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out EAR 2
+ entity.tickCount++;
+ entity.inactiveTick();
+ return;
+- }
++ }*/ // Paper - comment out EAR 2
+ // Spigot end
+ entity.setOldPosAndRot();
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
+ });
+ gameprofilerfiller.incrementCounter("tickNonPassenger");
++ final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); // Paper - EAR 2
++ if (isActive) { // Paper - EAR 2
+ entity.tick();
+ entity.postTick(); // CraftBukkit
++ } else { entity.inactiveTick(); } // Paper - EAR 2
+ gameprofilerfiller.pop();
+ Iterator iterator = entity.getPassengers().iterator();
+
+ while (iterator.hasNext()) {
+ Entity entity1 = (Entity) iterator.next();
+
+- this.tickPassenger(entity, entity1);
++ this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2
+ }
+
+ }
+
+- private void tickPassenger(Entity vehicle, Entity passenger) {
++ private void tickPassenger(Entity vehicle, Entity passenger, boolean isActive) { // Paper - EAR 2
+ if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) {
+ if (passenger instanceof Player || this.entityTickList.contains(passenger)) {
+ passenger.setOldPosAndRot();
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString();
+ });
+ gameprofilerfiller.incrementCounter("tickPassenger");
++ // Paper start - EAR 2
++ if (isActive) {
+ passenger.rideTick();
+ passenger.postTick(); // CraftBukkit
++ } else {
++ passenger.setDeltaMovement(Vec3.ZERO);
++ passenger.inactiveTick();
++ // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary
++ vehicle.positionRider(passenger);
++ }
++ // Paper end - EAR 2
+ gameprofilerfiller.pop();
+ Iterator iterator = passenger.getPassengers().iterator();
+
+ while (iterator.hasNext()) {
+ Entity entity2 = (Entity) iterator.next();
+
+- this.tickPassenger(passenger, entity2);
++ this.tickPassenger(passenger, entity2, isActive); // Paper - EAR 2
+ }
+
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ // Spigot end
+ protected int numCollisions = 0; // Paper - Cap entity collisions
+ public boolean fromNetherPortal; // Paper - Add option to nerf pigmen from nether portals
++ public long activatedImmunityTick = Integer.MIN_VALUE; // Paper - EAR
++ public boolean isTemporarilyActive; // Paper - EAR
+ public boolean spawnedViaMobSpawner; // Paper - Yes this name is similar to above, upstream took the better one
+ // Paper start - Entity origin API
+ @javax.annotation.Nullable
+@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ } else {
+ this.wasOnFire = this.isOnFire();
+ if (type == MoverType.PISTON) {
++ this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2
++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper - EAR 2
+ movement = this.limitPistonMovement(movement);
+ if (movement.equals(Vec3.ZERO)) {
+ return;
+@@ -0,0 +0,0 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+ this.stuckSpeedMultiplier = Vec3.ZERO;
+ this.setDeltaMovement(Vec3.ZERO);
+ }
++ // Paper start - ignore movement changes while inactive.
++ if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && type == MoverType.SELF) {
++ setDeltaMovement(Vec3.ZERO);
++ gameprofilerfiller.pop();
++ return;
++ }
++ // Paper end
+
+ movement = this.maybeBackOffFromEdge(movement, type);
+ Vec3 vec3d1 = this.collide(movement);
+diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/Mob.java
++++ b/src/main/java/net/minecraft/world/entity/Mob.java
+@@ -0,0 +0,0 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+ return this.lookControl;
+ }
+
++ // Paper start
++ @Override
++ public void inactiveTick() {
++ super.inactiveTick();
++ if (this.goalSelector.inactiveTick()) {
++ this.goalSelector.tick();
++ }
++ if (this.targetSelector.inactiveTick()) {
++ this.targetSelector.tick();
++ }
++ }
++ // Paper end
++
+ public MoveControl getMoveControl() {
+ Entity entity = this.getControlledVehicle();
+
+diff --git a/src/main/java/net/minecraft/world/entity/PathfinderMob.java b/src/main/java/net/minecraft/world/entity/PathfinderMob.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/PathfinderMob.java
++++ b/src/main/java/net/minecraft/world/entity/PathfinderMob.java
+@@ -0,0 +0,0 @@ public abstract class PathfinderMob extends Mob {
+ super(type, world);
+ }
+
++ public BlockPos movingTarget; public BlockPos getMovingTarget() { return movingTarget; } // Paper
++
+ public float getWalkTargetValue(BlockPos pos) {
+ return this.getWalkTargetValue(pos, this.level());
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java
++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java
+@@ -0,0 +0,0 @@ public class GoalSelector {
+ private final Map lockedFlags = new EnumMap<>(Goal.Flag.class);
+ private final Set availableGoals = new ObjectLinkedOpenHashSet<>();
+ private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class);
++ private int curRate; // Paper - EAR 2
+
+ public void addGoal(int priority, Goal goal) {
+ this.availableGoals.add(new WrappedGoal(priority, goal));
+@@ -0,0 +0,0 @@ public class GoalSelector {
+ this.availableGoals.removeIf(goal -> predicate.test(goal.getGoal()));
+ }
+
++ // Paper start - EAR 2
++ public boolean inactiveTick() {
++ this.curRate++;
++ return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct
++ }
++ public boolean hasTasks() {
++ for (WrappedGoal task : this.availableGoals) {
++ if (task.isRunning()) {
++ return true;
++ }
++ }
++ return false;
++ }
++ // Paper end - EAR 2
+ public void removeGoal(Goal goal) {
+ for (WrappedGoal wrappedGoal : this.availableGoals) {
+ if (wrappedGoal.getGoal() == goal && wrappedGoal.isRunning()) {
+diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
++++ b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
+@@ -0,0 +0,0 @@ public abstract class MoveToBlockGoal extends Goal {
+ public MoveToBlockGoal(PathfinderMob mob, double speed, int range) {
+ this(mob, speed, range, 1);
+ }
++ // Paper start - activation range improvements
++ @Override
++ public void stop() {
++ super.stop();
++ this.blockPos = BlockPos.ZERO;
++ this.mob.movingTarget = null;
++ }
++ // Paper end
+
+ public MoveToBlockGoal(PathfinderMob mob, double speed, int range, int maxYDifference) {
+ this.mob = mob;
+@@ -0,0 +0,0 @@ public abstract class MoveToBlockGoal extends Goal {
+ mutableBlockPos.setWithOffset(blockPos, m, k - 1, n);
+ if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) {
+ this.blockPos = mutableBlockPos;
++ this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper
+ return true;
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java
++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java
+@@ -0,0 +0,0 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
+ @Override
+ public void inactiveTick() {
+ // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :(
+- if (this.level().spigotConfig.tickInactiveVillagers && this.isEffectiveAi()) {
+- this.customServerAiStep((ServerLevel) this.level());
++ // Paper start
++ if (this.getUnhappyCounter() > 0) {
++ this.setUnhappyCounter(this.getUnhappyCounter() - 1);
++ }
++ if (this.isEffectiveAi()) {
++ if (this.level().spigotConfig.tickInactiveVillagers) {
++ this.customServerAiStep(this.level().getMinecraftWorld());
++ } else {
++ this.customServerAiStep(this.level().getMinecraftWorld(), true);
++ }
+ }
++ maybeDecayGossip();
++ // Paper end
+ super.inactiveTick();
+ }
+ // Spigot End
+
+ @Override
+ protected void customServerAiStep(ServerLevel world) {
++ // Paper start - EAR 2
++ this.customServerAiStep(world, false);
++ }
++ protected void customServerAiStep(ServerLevel world, final boolean inactive) {
++ // Paper end - EAR 2
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("villagerBrain");
+- this.getBrain().tick(world, this);
++ if (!inactive) this.getBrain().tick(world, this);
+ gameprofilerfiller.pop();
+ if (this.assignProfessionWhenSpawned) {
+ this.assignProfessionWhenSpawned = false;
+@@ -0,0 +0,0 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
+ this.lastTradedPlayer = null;
+ }
+
+- if (!this.isNoAi() && this.random.nextInt(100) == 0) {
++ if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper - EAR 2
+ Raid raid = world.getRaidAt(this.blockPosition());
+
+ if (raid != null && raid.isActive() && !raid.isOver()) {
+@@ -0,0 +0,0 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
+ if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) {
+ this.stopTrading();
+ }
++ if (inactive) return; // Paper - EAR 2
+
+ super.customServerAiStep(world);
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
+@@ -0,0 +0,0 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
+ if (bl != this.isEnabled()) {
+ this.setEnabled(bl);
+ }
++ this.immunize(); // Paper
+ }
+
+ public boolean isEnabled() {
+@@ -0,0 +0,0 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
+
+ public boolean suckInItems() {
+ if (HopperBlockEntity.suckInItems(this.level(), this)) {
++ this.immunize(); // Paper
+ return true;
+ } else {
+ for (ItemEntity itemEntity : this.level()
+ .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.25, 0.0, 0.25), EntitySelector.ENTITY_STILL_ALIVE)) {
+ if (HopperBlockEntity.addItem(this, itemEntity)) {
++ this.immunize(); // Paper
+ return true;
+ }
+ }
+@@ -0,0 +0,0 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
+ public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) {
+ return new HopperMenu(syncId, playerInventory, this);
+ }
++
++ // Paper start
++ public void immunize() {
++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20);
++ }
++ // Paper end
++
+ }
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates
+ public List captureDrops;
+ public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
++ // Paper start
++ public int wakeupInactiveRemainingAnimals;
++ public int wakeupInactiveRemainingFlying;
++ public int wakeupInactiveRemainingMonsters;
++ public int wakeupInactiveRemainingVillagers;
++ // Paper end
+ public boolean populating;
+ public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
+ // Paper start - add paper world config
+diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+@@ -0,0 +0,0 @@ public class PistonMovingBlockEntity extends BlockEntity {
+ }
+
+ entity.setDeltaMovement(e, g, h);
++ // Paper - EAR items stuck in in slime pushed by a piston
++ entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10);
++ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10);
++ // Paper end
+ break;
+ }
+ }
+diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/spigotmc/ActivationRange.java
++++ b/src/main/java/org/spigotmc/ActivationRange.java
+@@ -0,0 +0,0 @@
+ package org.spigotmc;
+
++import net.minecraft.core.BlockPos;
+ import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerChunkCache;
+ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.entity.ExperienceOrb;
++import net.minecraft.world.entity.FlyingMob;
+ import net.minecraft.world.entity.LightningBolt;
+ import net.minecraft.world.entity.LivingEntity;
++import net.minecraft.world.entity.Mob;
+ import net.minecraft.world.entity.PathfinderMob;
++import net.minecraft.world.entity.ai.Brain;
+ import net.minecraft.world.entity.ambient.AmbientCreature;
+ import net.minecraft.world.entity.animal.Animal;
++import net.minecraft.world.entity.animal.Bee;
+ import net.minecraft.world.entity.animal.Sheep;
++import net.minecraft.world.entity.animal.WaterAnimal;
++import net.minecraft.world.entity.animal.horse.Llama;
+ import net.minecraft.world.entity.boss.EnderDragonPart;
+ import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
+ import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+@@ -0,0 +0,0 @@ import net.minecraft.world.entity.boss.wither.WitherBoss;
+ import net.minecraft.world.entity.item.ItemEntity;
+ import net.minecraft.world.entity.item.PrimedTnt;
+ import net.minecraft.world.entity.monster.Creeper;
+-import net.minecraft.world.entity.monster.Monster;
+-import net.minecraft.world.entity.monster.Slime;
++import net.minecraft.world.entity.monster.Enemy;
++import net.minecraft.world.entity.monster.Pillager;
+ import net.minecraft.world.entity.npc.Villager;
+ import net.minecraft.world.entity.player.Player;
+ import net.minecraft.world.entity.projectile.AbstractArrow;
+ import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
++import net.minecraft.world.entity.projectile.EyeOfEnder;
+ import net.minecraft.world.entity.projectile.FireworkRocketEntity;
+ import net.minecraft.world.entity.projectile.ThrowableProjectile;
+ import net.minecraft.world.entity.projectile.ThrownTrident;
+@@ -0,0 +0,0 @@ public class ActivationRange
+
+ AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 );
+ }
++ // Paper start
++
++ static net.minecraft.world.entity.schedule.Activity[] VILLAGER_PANIC_IMMUNITIES = {
++ net.minecraft.world.entity.schedule.Activity.HIDE,
++ net.minecraft.world.entity.schedule.Activity.PRE_RAID,
++ net.minecraft.world.entity.schedule.Activity.RAID,
++ net.minecraft.world.entity.schedule.Activity.PANIC
++ };
++
++ private static int checkInactiveWakeup(Entity entity) {
++ Level world = entity.level();
++ SpigotWorldConfig config = world.spigotConfig;
++ long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
++ if (entity.activationType == ActivationType.VILLAGER) {
++ if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
++ world.wakeupInactiveRemainingVillagers--;
++ return config.wakeUpInactiveVillagersFor;
++ }
++ } else if (entity.activationType == ActivationType.ANIMAL) {
++ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
++ world.wakeupInactiveRemainingAnimals--;
++ return config.wakeUpInactiveAnimalsFor;
++ }
++ } else if (entity.activationType == ActivationType.FLYING_MONSTER) {
++ if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
++ world.wakeupInactiveRemainingFlying--;
++ return config.wakeUpInactiveFlyingFor;
++ }
++ } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
++ if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
++ world.wakeupInactiveRemainingMonsters--;
++ return config.wakeUpInactiveMonstersFor;
++ }
++ }
++ return -1;
++ }
++ // Paper end
+
+ static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 );
+
+@@ -0,0 +0,0 @@ public class ActivationRange
+ */
+ public static ActivationType initializeEntityActivationType(Entity entity)
+ {
++ if (entity instanceof WaterAnimal) { return ActivationType.WATER; } // Paper
++ else if (entity instanceof Villager) { return ActivationType.VILLAGER; } // Paper
++ else if (entity instanceof FlyingMob && entity instanceof Enemy) { return ActivationType.FLYING_MONSTER; } // Paper - doing & Monster incase Flying no longer includes monster in future
+ if ( entity instanceof Raider )
+ {
+ return ActivationType.RAIDER;
+- } else if ( entity instanceof Monster || entity instanceof Slime )
++ } else if ( entity instanceof Enemy ) // Paper - correct monster check
+ {
+ return ActivationType.MONSTER;
+ } else if ( entity instanceof PathfinderMob || entity instanceof AmbientCreature )
+@@ -0,0 +0,0 @@ public class ActivationRange
+ */
+ public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config)
+ {
+- if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange == 0 )
+- || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange == 0 )
+- || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange == 0 )
+- || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange == 0 )
++ if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange <= 0 )
++ || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange <= 0 )
++ || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange <= 0 )
++ || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange <= 0 )
++ || ( entity.activationType == ActivationType.VILLAGER && config.villagerActivationRange <= 0 ) // Paper
++ || ( entity.activationType == ActivationType.WATER && config.waterActivationRange <= 0 ) // Paper
++ || ( entity.activationType == ActivationType.FLYING_MONSTER && config.flyingMonsterActivationRange <= 0 ) // Paper
++ || entity instanceof EyeOfEnder // Paper
+ || entity instanceof Player
+ || entity instanceof ThrowableProjectile
+ || entity instanceof EnderDragon
+@@ -0,0 +0,0 @@ public class ActivationRange
+ final int raiderActivationRange = world.spigotConfig.raiderActivationRange;
+ final int animalActivationRange = world.spigotConfig.animalActivationRange;
+ final int monsterActivationRange = world.spigotConfig.monsterActivationRange;
++ // Paper start
++ final int waterActivationRange = world.spigotConfig.waterActivationRange;
++ final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
++ final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
++ world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
++ world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
++ world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
++ world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
++ final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource();
++ // Paper end
+
+ int maxRange = Math.max( monsterActivationRange, animalActivationRange );
+ maxRange = Math.max( maxRange, raiderActivationRange );
+ maxRange = Math.max( maxRange, miscActivationRange );
++ // Paper start
++ maxRange = Math.max( maxRange, flyingActivationRange );
++ maxRange = Math.max( maxRange, waterActivationRange );
++ maxRange = Math.max( maxRange, villagerActivationRange );
++ // Paper end
+ maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange );
+
+ for ( Player player : world.players() )
+@@ -0,0 +0,0 @@ public class ActivationRange
+ continue;
+ }
+
+- ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, 256, maxRange );
+- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, 256, miscActivationRange );
+- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, 256, raiderActivationRange );
+- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, 256, animalActivationRange );
+- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, 256, monsterActivationRange );
++ // Paper start
++ int worldHeight = world.getHeight();
++ ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange );
++ ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange );
++ ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange );
++ ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange );
++ ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange );
++ ActivationType.WATER.boundingBox = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange );
++ ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange );
++ ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange );
++ // Paper end
+
+- world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity);
++ // Paper start
++ java.util.List entities = world.getEntities((Entity)null, ActivationRange.maxBB, null);
++ boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking
++ for (Entity entity : entities) {
++ // Paper start - Configurable marker ticking
++ if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
++ continue;
++ }
++ // Paper end - Configurable marker ticking
++ ActivationRange.activateEntity(entity);
++ }
++ // Paper end
+ }
+ }
+
+@@ -0,0 +0,0 @@ public class ActivationRange
+ * @param entity
+ * @return
+ */
+- public static boolean checkEntityImmunities(Entity entity)
++ public static int checkEntityImmunities(Entity entity) // Paper - return # of ticks to get immunity
+ {
++ // Paper start
++ SpigotWorldConfig config = entity.level().spigotConfig;
++ int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
++ if (inactiveWakeUpImmunity > -1) {
++ return inactiveWakeUpImmunity;
++ }
++ if (entity.getRemainingFireTicks() > 0) {
++ return 2;
++ }
++ if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
++ return 1;
++ }
++ long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
++ // Paper end
+ // quick checks.
+- if ( entity.wasTouchingWater || entity.getRemainingFireTicks() > 0 )
++ if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper
+ {
+- return true;
++ return 100; // Paper
++ }
++ // Paper start
++ if ( !entity.onGround() || entity.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D )
++ {
++ return 100;
+ }
++ // Paper end
+ if ( !( entity instanceof AbstractArrow ) )
+ {
+- if ( !entity.onGround() || !entity.passengers.isEmpty() || entity.isPassenger() )
++ if ( (!entity.onGround() && !(entity instanceof FlyingMob)) ) // Paper - remove passengers logic
+ {
+- return true;
++ return 10; // Paper
+ }
+ } else if ( !( (AbstractArrow) entity ).isInGround() )
+ {
+- return true;
++ return 1; // Paper
+ }
+ // special cases.
+ if ( entity instanceof LivingEntity )
+ {
+ LivingEntity living = (LivingEntity) entity;
+- if ( /*TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTime > 0 || living.activeEffects.size() > 0 )
++ if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper
+ {
+- return true;
++ return 1; // Paper
+ }
+- if ( entity instanceof PathfinderMob && ( (PathfinderMob) entity ).getTarget() != null )
++ if ( entity instanceof Mob && ((Mob) entity ).getTarget() != null) // Paper
+ {
+- return true;
++ return 20; // Paper
+ }
+- if ( entity instanceof Villager && ( (Villager) entity ).canBreed() )
++ // Paper start
++ if (entity instanceof Bee) {
++ Bee bee = (Bee)entity;
++ BlockPos movingTarget = bee.getMovingTarget();
++ if (bee.isAngry() ||
++ (bee.getHivePos() != null && bee.getHivePos().equals(movingTarget)) ||
++ (bee.getSavedFlowerPos() != null && bee.getSavedFlowerPos().equals(movingTarget))
++ ) {
++ return 20;
++ }
++ }
++ if ( entity instanceof Villager ) {
++ Brain behaviorController = ((Villager) entity).getBrain();
++
++ if (config.villagersActiveForPanic) {
++ for (net.minecraft.world.entity.schedule.Activity activity : VILLAGER_PANIC_IMMUNITIES) {
++ if (behaviorController.isActive(activity)) {
++ return 20*5;
++ }
++ }
++ }
++
++ if (config.villagersWorkImmunityAfter > 0 && inactiveFor >= config.villagersWorkImmunityAfter) {
++ if (behaviorController.isActive(net.minecraft.world.entity.schedule.Activity.WORK)) {
++ return config.villagersWorkImmunityFor;
++ }
++ }
++ }
++ if ( entity instanceof Llama && ( (Llama) entity ).inCaravan() )
+ {
+- return true;
++ return 1;
+ }
++ // Paper end
+ if ( entity instanceof Animal )
+ {
+ Animal animal = (Animal) entity;
+ if ( animal.isBaby() || animal.isInLove() )
+ {
+- return true;
++ return 5; // Paper
+ }
+ if ( entity instanceof Sheep && ( (Sheep) entity ).isSheared() )
+ {
+- return true;
++ return 1; // Paper
+ }
+ }
+ if (entity instanceof Creeper && ((Creeper) entity).isIgnited()) { // isExplosive
+- return true;
++ return 20; // Paper
++ }
++ // Paper start
++ if (entity instanceof Mob && ((Mob) entity).targetSelector.hasTasks() ) {
++ return 0;
+ }
++ if (entity instanceof Pillager) {
++ Pillager pillager = (Pillager) entity;
++ // TODO:?
++ }
++ // Paper end
+ }
+ // SPIGOT-6644: Otherwise the target refresh tick will be missed
+ if (entity instanceof ExperienceOrb) {
+- return true;
++ return 20; // Paper
+ }
+- return false;
++ return -1; // Paper
+ }
+
+ /**
+@@ -0,0 +0,0 @@ public class ActivationRange
+ if (entity instanceof FireworkRocketEntity || (entity instanceof ItemEntity && (entity.tickCount + entity.getId()) % 4 == 0)) { // Paper - Needed for item gravity, see ItemEntity tick
+ return true;
+ }
++ // Paper start - special case always immunities
++ // immunize brand new entities, dead entities, and portal scenarios
++ if (entity.defaultActivationState || entity.tickCount < 20*10 || !entity.isAlive() || (entity.portalProcess != null && !entity.portalProcess.hasExpired()) || entity.portalCooldown > 0) {
++ return true;
++ }
++ // immunize leashed entities
++ if (entity instanceof Mob && ((Mob)entity).getLeashHolder() instanceof Player) {
++ return true;
++ }
++ // Paper end
+
+- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick || entity.defaultActivationState;
++ boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
++ entity.isTemporarilyActive = false; // Paper
+
+ // Should this entity tick?
+ if ( !isActive )
+@@ -0,0 +0,0 @@ public class ActivationRange
+ if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 )
+ {
+ // Check immunities every 20 ticks.
+- if ( ActivationRange.checkEntityImmunities( entity ) )
+- {
+- // Triggered some sort of immunity, give 20 full ticks before we check again.
+- entity.activatedTick = MinecraftServer.currentTick + 20;
++ // Paper start
++ int immunity = checkEntityImmunities(entity);
++ if (immunity >= 0) {
++ entity.activatedTick = MinecraftServer.currentTick + immunity;
++ } else {
++ entity.isTemporarilyActive = true;
+ }
++ // Paper end
+ isActive = true;
+ }
+ }
+diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
++++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
+@@ -0,0 +0,0 @@ public class SpigotWorldConfig
+ public int monsterActivationRange = 32;
+ public int raiderActivationRange = 64;
+ public int miscActivationRange = 16;
++ // Paper start
++ public int flyingMonsterActivationRange = 32;
++ public int waterActivationRange = 16;
++ public int villagerActivationRange = 32;
++ public int wakeUpInactiveAnimals = 4;
++ public int wakeUpInactiveAnimalsEvery = 60*20;
++ public int wakeUpInactiveAnimalsFor = 5*20;
++ public int wakeUpInactiveMonsters = 8;
++ public int wakeUpInactiveMonstersEvery = 20*20;
++ public int wakeUpInactiveMonstersFor = 5*20;
++ public int wakeUpInactiveVillagers = 4;
++ public int wakeUpInactiveVillagersEvery = 30*20;
++ public int wakeUpInactiveVillagersFor = 5*20;
++ public int wakeUpInactiveFlying = 8;
++ public int wakeUpInactiveFlyingEvery = 10*20;
++ public int wakeUpInactiveFlyingFor = 5*20;
++ public int villagersWorkImmunityAfter = 5*20;
++ public int villagersWorkImmunityFor = 20;
++ public boolean villagersActiveForPanic = true;
++ // Paper end
+ public boolean tickInactiveVillagers = true;
+ public boolean ignoreSpectatorActivation = false;
+ private void activationRange()
+ {
++ boolean hasAnimalsConfig = config.getInt("entity-activation-range.animals", this.animalActivationRange) != this.animalActivationRange; // Paper
+ this.animalActivationRange = this.getInt( "entity-activation-range.animals", this.animalActivationRange );
+ this.monsterActivationRange = this.getInt( "entity-activation-range.monsters", this.monsterActivationRange );
+ this.raiderActivationRange = this.getInt( "entity-activation-range.raiders", this.raiderActivationRange );
+ this.miscActivationRange = this.getInt( "entity-activation-range.misc", this.miscActivationRange );
++ // Paper start
++ this.waterActivationRange = this.getInt( "entity-activation-range.water", this.waterActivationRange );
++ this.villagerActivationRange = this.getInt( "entity-activation-range.villagers", hasAnimalsConfig ? this.animalActivationRange : this.villagerActivationRange );
++ this.flyingMonsterActivationRange = this.getInt( "entity-activation-range.flying-monsters", this.flyingMonsterActivationRange );
++
++ this.wakeUpInactiveAnimals = this.getInt("entity-activation-range.wake-up-inactive.animals-max-per-tick", this.wakeUpInactiveAnimals);
++ this.wakeUpInactiveAnimalsEvery = this.getInt("entity-activation-range.wake-up-inactive.animals-every", this.wakeUpInactiveAnimalsEvery);
++ this.wakeUpInactiveAnimalsFor = this.getInt("entity-activation-range.wake-up-inactive.animals-for", this.wakeUpInactiveAnimalsFor);
++
++ this.wakeUpInactiveMonsters = this.getInt("entity-activation-range.wake-up-inactive.monsters-max-per-tick", this.wakeUpInactiveMonsters);
++ this.wakeUpInactiveMonstersEvery = this.getInt("entity-activation-range.wake-up-inactive.monsters-every", this.wakeUpInactiveMonstersEvery);
++ this.wakeUpInactiveMonstersFor = this.getInt("entity-activation-range.wake-up-inactive.monsters-for", this.wakeUpInactiveMonstersFor);
++
++ this.wakeUpInactiveVillagers = this.getInt("entity-activation-range.wake-up-inactive.villagers-max-per-tick", this.wakeUpInactiveVillagers);
++ this.wakeUpInactiveVillagersEvery = this.getInt("entity-activation-range.wake-up-inactive.villagers-every", this.wakeUpInactiveVillagersEvery);
++ this.wakeUpInactiveVillagersFor = this.getInt("entity-activation-range.wake-up-inactive.villagers-for", this.wakeUpInactiveVillagersFor);
++
++ this.wakeUpInactiveFlying = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-max-per-tick", this.wakeUpInactiveFlying);
++ this.wakeUpInactiveFlyingEvery = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-every", this.wakeUpInactiveFlyingEvery);
++ this.wakeUpInactiveFlyingFor = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-for", this.wakeUpInactiveFlyingFor);
++
++ this.villagersWorkImmunityAfter = this.getInt( "entity-activation-range.villagers-work-immunity-after", this.villagersWorkImmunityAfter );
++ this.villagersWorkImmunityFor = this.getInt( "entity-activation-range.villagers-work-immunity-for", this.villagersWorkImmunityFor );
++ this.villagersActiveForPanic = this.getBoolean( "entity-activation-range.villagers-active-for-panic", this.villagersActiveForPanic );
++ // Paper end
+ this.tickInactiveVillagers = this.getBoolean( "entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers );
+ this.ignoreSpectatorActivation = this.getBoolean( "entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation );
+ this.log( "Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation );
diff --git a/feature-patches/1044-Anti-Xray.patch b/feature-patches/1044-Anti-Xray.patch
new file mode 100644
index 000000000..f002f2354
--- /dev/null
+++ b/feature-patches/1044-Anti-Xray.patch
@@ -0,0 +1,1628 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: stonar96
+Date: Thu, 25 Nov 2021 13:27:51 +0100
+Subject: [PATCH] Anti-Xray
+
+
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++public final class BitStorageReader {
++
++ private byte[] buffer;
++ private int bits;
++ private int mask;
++ private int longInBufferIndex;
++ private int bitInLongIndex;
++ private long current;
++
++ public void setBuffer(byte[] buffer) {
++ this.buffer = buffer;
++ }
++
++ public void setBits(int bits) {
++ this.bits = bits;
++ mask = (1 << bits) - 1;
++ }
++
++ public void setIndex(int index) {
++ longInBufferIndex = index;
++ bitInLongIndex = 0;
++ init();
++ }
++
++ private void init() {
++ if (buffer.length > longInBufferIndex + 7) {
++ current = ((((long) buffer[longInBufferIndex]) << 56)
++ | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
++ | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
++ | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
++ | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
++ | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
++ | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
++ | (((long) buffer[longInBufferIndex + 7] & 0xff)));
++ }
++ }
++
++ public int read() {
++ if (bitInLongIndex + bits > 64) {
++ bitInLongIndex = 0;
++ longInBufferIndex += 8;
++ init();
++ }
++
++ int value = (int) (current >>> bitInLongIndex) & mask;
++ bitInLongIndex += bits;
++ return value;
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++public final class BitStorageWriter {
++
++ private byte[] buffer;
++ private int bits;
++ private long mask;
++ private int longInBufferIndex;
++ private int bitInLongIndex;
++ private long current;
++ private boolean dirty;
++
++ public void setBuffer(byte[] buffer) {
++ this.buffer = buffer;
++ }
++
++ public void setBits(int bits) {
++ this.bits = bits;
++ mask = (1L << bits) - 1;
++ }
++
++ public void setIndex(int index) {
++ longInBufferIndex = index;
++ bitInLongIndex = 0;
++ init();
++ }
++
++ private void init() {
++ if (buffer.length > longInBufferIndex + 7) {
++ current = ((((long) buffer[longInBufferIndex]) << 56)
++ | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
++ | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
++ | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
++ | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
++ | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
++ | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
++ | (((long) buffer[longInBufferIndex + 7] & 0xff)));
++ }
++
++ dirty = false;
++ }
++
++ public void flush() {
++ if (dirty && buffer.length > longInBufferIndex + 7) {
++ buffer[longInBufferIndex] = (byte) (current >> 56 & 0xff);
++ buffer[longInBufferIndex + 1] = (byte) (current >> 48 & 0xff);
++ buffer[longInBufferIndex + 2] = (byte) (current >> 40 & 0xff);
++ buffer[longInBufferIndex + 3] = (byte) (current >> 32 & 0xff);
++ buffer[longInBufferIndex + 4] = (byte) (current >> 24 & 0xff);
++ buffer[longInBufferIndex + 5] = (byte) (current >> 16 & 0xff);
++ buffer[longInBufferIndex + 6] = (byte) (current >> 8 & 0xff);
++ buffer[longInBufferIndex + 7] = (byte) (current & 0xff);
++ }
++ }
++
++ public void write(int value) {
++ if (bitInLongIndex + bits > 64) {
++ flush();
++ bitInLongIndex = 0;
++ longInBufferIndex += 8;
++ init();
++ }
++
++ current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex;
++ dirty = true;
++ bitInLongIndex += bits;
++ }
++
++ public void skip() {
++ bitInLongIndex += bits;
++
++ if (bitInLongIndex > 64) {
++ flush();
++ bitInLongIndex = bits;
++ longInBufferIndex += 8;
++ init();
++ }
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Direction;
++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
++import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.server.level.ServerPlayerGameMode;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.LevelChunk;
++
++public class ChunkPacketBlockController {
++
++ public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController();
++
++ protected ChunkPacketBlockController() {
++
++ }
++
++ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) {
++ return null;
++ }
++
++ public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
++ return false;
++ }
++
++ public ChunkPacketInfo getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
++ return null;
++ }
++
++ public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) {
++ chunkPacket.setReady(true);
++ }
++
++ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
++
++ }
++
++ public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
++
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++import io.papermc.paper.configuration.WorldConfiguration;
++import io.papermc.paper.configuration.type.EngineMode;
++import java.util.ArrayList;
++import java.util.LinkedHashSet;
++import java.util.LinkedList;
++import java.util.List;
++import java.util.Set;
++import java.util.concurrent.Executor;
++import java.util.concurrent.ThreadLocalRandom;
++import java.util.function.IntSupplier;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Direction;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
++import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.server.level.ServerPlayerGameMode;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.biome.Biomes;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.EntityBlock;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.EmptyLevelChunk;
++import net.minecraft.world.level.chunk.GlobalPalette;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.MissingPaletteEntryException;
++import net.minecraft.world.level.chunk.Palette;
++import org.bukkit.Bukkit;
++
++public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController {
++
++ private static final Palette GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
++ private static final LevelChunkSection EMPTY_SECTION = null;
++ private final Executor executor;
++ private final EngineMode engineMode;
++ private final int maxBlockHeight;
++ private final int updateRadius;
++ private final boolean usePermission;
++ private final BlockState[] presetBlockStates;
++ private final BlockState[] presetBlockStatesFull;
++ private final BlockState[] presetBlockStatesStone;
++ private final BlockState[] presetBlockStatesDeepslate;
++ private final BlockState[] presetBlockStatesNetherrack;
++ private final BlockState[] presetBlockStatesEndStone;
++ private final int[] presetBlockStateBitsGlobal;
++ private final int[] presetBlockStateBitsStoneGlobal;
++ private final int[] presetBlockStateBitsDeepslateGlobal;
++ private final int[] presetBlockStateBitsNetherrackGlobal;
++ private final int[] presetBlockStateBitsEndStoneGlobal;
++ private final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
++ private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
++ private final LevelChunkSection[] emptyNearbyChunkSections = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION};
++ private final int maxBlockHeightUpdatePosition;
++
++ public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) {
++ this.executor = executor;
++ WorldConfiguration.Anticheat.AntiXray paperWorldConfig = level.paperConfig().anticheat.antiXray;
++ engineMode = paperWorldConfig.engineMode;
++ maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4;
++ updateRadius = paperWorldConfig.updateRadius;
++ usePermission = paperWorldConfig.usePermission;
++ List toObfuscate;
++
++ if (engineMode == EngineMode.HIDE) {
++ toObfuscate = paperWorldConfig.hiddenBlocks;
++ presetBlockStates = null;
++ presetBlockStatesFull = null;
++ presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()};
++ presetBlockStatesDeepslate = new BlockState[]{Blocks.DEEPSLATE.defaultBlockState()};
++ presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()};
++ presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()};
++ presetBlockStateBitsGlobal = null;
++ presetBlockStateBitsStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())};
++ presetBlockStateBitsDeepslateGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.DEEPSLATE.defaultBlockState())};
++ presetBlockStateBitsNetherrackGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())};
++ presetBlockStateBitsEndStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())};
++ } else {
++ toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
++ List presetBlockStateList = new LinkedList<>();
++
++ for (Block block : paperWorldConfig.hiddenBlocks) {
++
++ if (!(block instanceof EntityBlock)) {
++ toObfuscate.add(block);
++ presetBlockStateList.add(block.defaultBlockState());
++ }
++ }
++
++ // The doc of the LinkedHashSet(Collection extends E>) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation
++ Set presetBlockStateSet = new LinkedHashSet<>();
++ // Therefore addAll(Collection extends E>) is used, which guarantees this order in the doc
++ presetBlockStateSet.addAll(presetBlockStateList);
++ presetBlockStates = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateSet.toArray(new BlockState[0]);
++ presetBlockStatesFull = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateList.toArray(new BlockState[0]);
++ presetBlockStatesStone = null;
++ presetBlockStatesDeepslate = null;
++ presetBlockStatesNetherrack = null;
++ presetBlockStatesEndStone = null;
++ presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
++
++ for (int i = 0; i < presetBlockStatesFull.length; i++) {
++ presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
++ }
++
++ presetBlockStateBitsStoneGlobal = null;
++ presetBlockStateBitsDeepslateGlobal = null;
++ presetBlockStateBitsNetherrackGlobal = null;
++ presetBlockStateBitsEndStoneGlobal = null;
++ }
++
++ for (Block block : toObfuscate) {
++
++ // Don't obfuscate air because air causes unnecessary block updates and causes block updates to fail in the void
++ if (block != null && !block.defaultBlockState().isAir()) {
++ // Replace all block states of a specified block
++ for (BlockState blockState : block.getStateDefinition().getPossibleStates()) {
++ obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
++ }
++ }
++ }
++
++ EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS));
++ BlockPos zeroPos = new BlockPos(0, 0, 0);
++
++ for (int i = 0; i < solidGlobal.length; i++) {
++ BlockState blockState = GLOBAL_BLOCKSTATE_PALETTE.valueFor(i);
++
++ if (blockState != null) {
++ solidGlobal[i] = blockState.isRedstoneConductor(emptyChunk, zeroPos)
++ && blockState.getBlock() != Blocks.SPAWNER && blockState.getBlock() != Blocks.BARRIER && blockState.getBlock() != Blocks.SHULKER_BOX && blockState.getBlock() != Blocks.SLIME_BLOCK && blockState.getBlock() != Blocks.MANGROVE_ROOTS || paperWorldConfig.lavaObscures && blockState == Blocks.LAVA.defaultBlockState();
++ // Comparing blockState == Blocks.LAVA.defaultBlockState() instead of blockState.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used
++ // shulker box checks TE.
++ }
++ }
++
++ maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1;
++ }
++
++ private int getPresetBlockStatesFullLength() {
++ return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length;
++ }
++
++ @Override
++ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) {
++ // Return the block states to be added to the paletted containers so that they can be used for obfuscation
++ int bottomBlockY = chunkSectionY << 4;
++
++ if (bottomBlockY < maxBlockHeight) {
++ if (engineMode == EngineMode.HIDE) {
++ return switch (level.getWorld().getEnvironment()) {
++ case NETHER -> presetBlockStatesNetherrack;
++ case THE_END -> presetBlockStatesEndStone;
++ default -> bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone;
++ };
++ }
++
++ return presetBlockStates;
++ }
++
++ return null;
++ }
++
++ @Override
++ public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
++ return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass");
++ }
++
++ @Override
++ public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
++ // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
++ return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this);
++ }
++
++ @Override
++ public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) {
++ if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) {
++ chunkPacket.setReady(true);
++ return;
++ }
++
++ if (!Bukkit.isPrimaryThread()) {
++ // Plugins?
++ MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
++ return;
++ }
++
++ LevelChunk chunk = chunkPacketInfo.getChunk();
++ int x = chunk.getPos().x;
++ int z = chunk.getPos().z;
++ Level level = chunk.getLevel();
++ ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1));
++ executor.execute((Runnable) chunkPacketInfo);
++ }
++
++ // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal)
++ // If an ExecutorService with multiple threads is used, ThreadLocal must be used here
++ private final ThreadLocal presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]);
++ private static final ThreadLocal SOLID = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
++ private static final ThreadLocal OBFUSCATE = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
++ // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate
++ private static final ThreadLocal CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]);
++ private static final ThreadLocal NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
++ private static final ThreadLocal NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
++
++ public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
++ int[] presetBlockStateBits = this.presetBlockStateBits.get();
++ boolean[] solid = SOLID.get();
++ boolean[] obfuscate = OBFUSCATE.get();
++ boolean[][] current = CURRENT.get();
++ boolean[][] next = NEXT.get();
++ boolean[][] nextNext = NEXT_NEXT.get();
++ // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it
++ BitStorageReader bitStorageReader = new BitStorageReader();
++ BitStorageWriter bitStorageWriter = new BitStorageWriter();
++ LevelChunkSection[] nearbyChunkSections = new LevelChunkSection[4];
++ LevelChunk chunk = chunkPacketInfoAntiXray.getChunk();
++ Level level = chunk.getLevel();
++ int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSectionY(), chunk.getSectionsCount()) - 1;
++ boolean[] solidTemp = null;
++ boolean[] obfuscateTemp = null;
++ bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer());
++ bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer());
++ int numberOfBlocks = presetBlockStateBits.length;
++ // Keep the lambda expressions as simple as possible. They are used very frequently.
++ LayeredIntSupplier random = numberOfBlocks == 1 ? (() -> 0) : engineMode == EngineMode.OBFUSCATE_LAYER ? new LayeredIntSupplier() {
++ // engine-mode: 3
++ private int state;
++ private int next;
++
++ {
++ while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
++ }
++
++ @Override
++ public void nextLayer() {
++ // https://en.wikipedia.org/wiki/Xorshift
++ state ^= state << 13;
++ state ^= state >>> 17;
++ state ^= state << 5;
++ // https://www.pcg-random.org/posts/bounded-rands.html
++ next = (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
++ }
++
++ @Override
++ public int getAsInt() {
++ return next;
++ }
++ } : new LayeredIntSupplier() {
++ // engine-mode: 2
++ private int state;
++
++ {
++ while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
++ }
++
++ @Override
++ public int getAsInt() {
++ // https://en.wikipedia.org/wiki/Xorshift
++ state ^= state << 13;
++ state ^= state >>> 17;
++ state ^= state << 5;
++ // https://www.pcg-random.org/posts/bounded-rands.html
++ return (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
++ }
++ };
++
++ for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
++ if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) {
++ int[] presetBlockStateBitsTemp;
++
++ if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) {
++ if (engineMode == EngineMode.HIDE) {
++ presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) {
++ case NETHER -> presetBlockStateBitsNetherrackGlobal;
++ case THE_END -> presetBlockStateBitsEndStoneGlobal;
++ default -> chunkSectionIndex + chunk.getMinSectionY() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal;
++ };
++ } else {
++ presetBlockStateBitsTemp = presetBlockStateBitsGlobal;
++ }
++ } else {
++ // If it's presetBlockStates, use this.presetBlockStatesFull instead
++ BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex);
++ presetBlockStateBitsTemp = presetBlockStateBits;
++
++ for (int i = 0; i < presetBlockStateBitsTemp.length; i++) {
++ // This is thread safe because we only request IDs that are guaranteed to be in the palette and are visible
++ // For more details see the comments in the readPalette method
++ presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]);
++ }
++ }
++
++ bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
++
++ // Check if the chunk section below was not obfuscated
++ if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) {
++ // If so, initialize some stuff
++ bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
++ bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
++ solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), solid, solidGlobal);
++ obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
++ // Read the blocks of the upper layer of the chunk section below if it exists
++ LevelChunkSection belowChunkSection = null;
++ boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunk.getSections()[chunkSectionIndex - 1]) == EMPTY_SECTION;
++
++ for (int z = 0; z < 16; z++) {
++ for (int x = 0; x < 16; x++) {
++ current[z][x] = true;
++ next[z][x] = skipFirstLayer || isTransparent(belowChunkSection, x, 15, z);
++ }
++ }
++
++ // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
++ bitStorageWriter.setBits(0);
++ obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random);
++ }
++
++ bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
++ nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
++ nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
++ nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
++ nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex];
++
++ // Obfuscate all layers of the current chunk section except the upper one
++ for (int y = 0; y < 15; y++) {
++ boolean[][] temp = current;
++ current = next;
++ next = nextNext;
++ nextNext = temp;
++ random.nextLayer();
++ obfuscateLayer(y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
++ }
++
++ // Check if the chunk section above doesn't need obfuscation
++ if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex + 1) == null) {
++ // If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists
++ LevelChunkSection aboveChunkSection;
++
++ if (chunkSectionIndex != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != EMPTY_SECTION) {
++ boolean[][] temp = current;
++ current = next;
++ next = nextNext;
++ nextNext = temp;
++
++ for (int z = 0; z < 16; z++) {
++ for (int x = 0; x < 16; x++) {
++ if (isTransparent(aboveChunkSection, x, 0, z)) {
++ current[z][x] = true;
++ }
++ }
++ }
++
++ // There is nothing to read anymore
++ bitStorageReader.setBits(0);
++ solid[0] = true;
++ random.nextLayer();
++ obfuscateLayer(15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
++ }
++ } else {
++ // If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
++ bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex + 1));
++ bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex + 1));
++ solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), solid, solidGlobal);
++ obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
++ boolean[][] temp = current;
++ current = next;
++ next = nextNext;
++ nextNext = temp;
++ random.nextLayer();
++ obfuscateLayer(15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
++ }
++
++ bitStorageWriter.flush();
++ }
++ }
++
++ chunkPacketInfoAntiXray.getChunkPacket().setReady(true);
++ }
++
++ private void obfuscateLayer(int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) {
++ // First block of first line
++ int bits = bitStorageReader.read();
++
++ if (nextNext[0][0] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[0][1] = true;
++ next[1][0] = true;
++ } else {
++ if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[0][0] = true;
++ }
++
++ // First line
++ for (int x = 1; x < 15; x++) {
++ bits = bitStorageReader.read();
++
++ if (nextNext[0][x] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[0][x - 1] = true;
++ next[0][x + 1] = true;
++ next[1][x] = true;
++ } else {
++ if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[0][x] = true;
++ }
++ }
++
++ // Last block of first line
++ bits = bitStorageReader.read();
++
++ if (nextNext[0][15] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[0][14] = true;
++ next[1][15] = true;
++ } else {
++ if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[0][15] = true;
++ }
++
++ // All inner lines
++ for (int z = 1; z < 15; z++) {
++ // First block
++ bits = bitStorageReader.read();
++
++ if (nextNext[z][0] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[z][1] = true;
++ next[z - 1][0] = true;
++ next[z + 1][0] = true;
++ } else {
++ if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[z][0] = true;
++ }
++
++ // All inner blocks
++ for (int x = 1; x < 15; x++) {
++ bits = bitStorageReader.read();
++
++ if (nextNext[z][x] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[z][x - 1] = true;
++ next[z][x + 1] = true;
++ next[z - 1][x] = true;
++ next[z + 1][x] = true;
++ } else {
++ if (current[z][x]) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[z][x] = true;
++ }
++ }
++
++ // Last block
++ bits = bitStorageReader.read();
++
++ if (nextNext[z][15] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[z][14] = true;
++ next[z - 1][15] = true;
++ next[z + 1][15] = true;
++ } else {
++ if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[z][15] = true;
++ }
++ }
++
++ // First block of last line
++ bits = bitStorageReader.read();
++
++ if (nextNext[15][0] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[15][1] = true;
++ next[14][0] = true;
++ } else {
++ if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[15][0] = true;
++ }
++
++ // Last line
++ for (int x = 1; x < 15; x++) {
++ bits = bitStorageReader.read();
++
++ if (nextNext[15][x] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[15][x - 1] = true;
++ next[15][x + 1] = true;
++ next[14][x] = true;
++ } else {
++ if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[15][x] = true;
++ }
++ }
++
++ // Last block of last line
++ bits = bitStorageReader.read();
++
++ if (nextNext[15][15] = !solid[bits]) {
++ bitStorageWriter.skip();
++ next[15][14] = true;
++ next[14][15] = true;
++ } else {
++ if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) {
++ bitStorageWriter.skip();
++ } else {
++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
++ }
++ }
++
++ if (!obfuscate[bits]) {
++ next[15][15] = true;
++ }
++ }
++
++ private boolean isTransparent(LevelChunkSection chunkSection, int x, int y, int z) {
++ if (chunkSection == EMPTY_SECTION) {
++ return true;
++ }
++
++ try {
++ return !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(chunkSection.getBlockState(x, y, z))];
++ } catch (MissingPaletteEntryException e) {
++ // Race condition / visibility issue / no happens-before relationship
++ // We don't care and treat the block as transparent
++ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur
++ return true;
++ }
++ }
++
++ private boolean[] readPalette(Palette palette, boolean[] temp, boolean[] global) {
++ if (palette instanceof GlobalPalette) {
++ return global;
++ }
++
++ try {
++ for (int i = 0; i < palette.getSize(); i++) {
++ temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))];
++ }
++ } catch (MissingPaletteEntryException e) {
++ // Race condition / visibility issue / no happens-before relationship
++ // We don't care because we at least see the state as it was when the chunk packet was created
++ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here
++ // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data
++ }
++
++ return temp;
++ }
++
++ @Override
++ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
++ if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) {
++ updateNearbyBlocks(level, blockPos);
++ }
++ }
++
++ @Override
++ public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
++ if (blockPos.getY() <= maxBlockHeightUpdatePosition) {
++ updateNearbyBlocks(serverPlayerGameMode.level, blockPos);
++ }
++ }
++
++ private void updateNearbyBlocks(Level level, BlockPos blockPos) {
++ if (updateRadius >= 2) {
++ BlockPos temp = blockPos.west();
++ updateBlock(level, temp);
++ updateBlock(level, temp.west());
++ updateBlock(level, temp.below());
++ updateBlock(level, temp.above());
++ updateBlock(level, temp.north());
++ updateBlock(level, temp.south());
++ updateBlock(level, temp = blockPos.east());
++ updateBlock(level, temp.east());
++ updateBlock(level, temp.below());
++ updateBlock(level, temp.above());
++ updateBlock(level, temp.north());
++ updateBlock(level, temp.south());
++ updateBlock(level, temp = blockPos.below());
++ updateBlock(level, temp.below());
++ updateBlock(level, temp.north());
++ updateBlock(level, temp.south());
++ updateBlock(level, temp = blockPos.above());
++ updateBlock(level, temp.above());
++ updateBlock(level, temp.north());
++ updateBlock(level, temp.south());
++ updateBlock(level, temp = blockPos.north());
++ updateBlock(level, temp.north());
++ updateBlock(level, temp = blockPos.south());
++ updateBlock(level, temp.south());
++ } else if (updateRadius == 1) {
++ updateBlock(level, blockPos.west());
++ updateBlock(level, blockPos.east());
++ updateBlock(level, blockPos.below());
++ updateBlock(level, blockPos.above());
++ updateBlock(level, blockPos.north());
++ updateBlock(level, blockPos.south());
++ } else {
++ // Do nothing if updateRadius <= 0 (test mode)
++ }
++ }
++
++ private void updateBlock(Level level, BlockPos blockPos) {
++ BlockState blockState = level.getBlockStateIfLoaded(blockPos);
++
++ if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
++ ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
++ }
++ }
++
++ @FunctionalInterface
++ private interface LayeredIntSupplier extends IntSupplier {
++ default void nextLayer() {
++
++ }
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.Palette;
++
++public class ChunkPacketInfo {
++
++ private final ClientboundLevelChunkWithLightPacket chunkPacket;
++ private final LevelChunk chunk;
++ private final int[] bits;
++ private final Object[] palettes;
++ private final int[] indexes;
++ private final Object[][] presetValues;
++ private byte[] buffer;
++
++ public ChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
++ this.chunkPacket = chunkPacket;
++ this.chunk = chunk;
++ int sections = chunk.getSectionsCount();
++ bits = new int[sections];
++ palettes = new Object[sections];
++ indexes = new int[sections];
++ presetValues = new Object[sections][];
++ }
++
++ public ClientboundLevelChunkWithLightPacket getChunkPacket() {
++ return chunkPacket;
++ }
++
++ public LevelChunk getChunk() {
++ return chunk;
++ }
++
++ public byte[] getBuffer() {
++ return buffer;
++ }
++
++ public void setBuffer(byte[] buffer) {
++ this.buffer = buffer;
++ }
++
++ public int getBits(int chunkSectionIndex) {
++ return bits[chunkSectionIndex];
++ }
++
++ public void setBits(int chunkSectionIndex, int bits) {
++ this.bits[chunkSectionIndex] = bits;
++ }
++
++ @SuppressWarnings("unchecked")
++ public Palette getPalette(int chunkSectionIndex) {
++ return (Palette) palettes[chunkSectionIndex];
++ }
++
++ public void setPalette(int chunkSectionIndex, Palette palette) {
++ palettes[chunkSectionIndex] = palette;
++ }
++
++ public int getIndex(int chunkSectionIndex) {
++ return indexes[chunkSectionIndex];
++ }
++
++ public void setIndex(int chunkSectionIndex, int index) {
++ indexes[chunkSectionIndex] = index;
++ }
++
++ @SuppressWarnings("unchecked")
++ public T[] getPresetValues(int chunkSectionIndex) {
++ return (T[]) presetValues[chunkSectionIndex];
++ }
++
++ public void setPresetValues(int chunkSectionIndex, T[] presetValues) {
++ this.presetValues[chunkSectionIndex] = presetValues;
++ }
++
++ public boolean isWritten(int chunkSectionIndex) {
++ return bits[chunkSectionIndex] != 0;
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.LevelChunk;
++
++public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo implements Runnable {
++
++ private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
++ private LevelChunk[] nearbyChunks;
++
++ public ChunkPacketInfoAntiXray(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
++ super(chunkPacket, chunk);
++ this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
++ }
++
++ public LevelChunk[] getNearbyChunks() {
++ return nearbyChunks;
++ }
++
++ public void setNearbyChunks(LevelChunk... nearbyChunks) {
++ this.nearbyChunks = nearbyChunks;
++ }
++
++ @Override
++ public void run() {
++ chunkPacketBlockControllerAntiXray.obfuscate(this);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/FeatureHooks.java b/src/main/java/io/papermc/paper/FeatureHooks.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/FeatureHooks.java
++++ b/src/main/java/io/papermc/paper/FeatureHooks.java
+@@ -0,0 +0,0 @@ import it.unimi.dsi.fastutil.longs.LongSets;
+ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+ import it.unimi.dsi.fastutil.objects.ObjectSet;
+ import it.unimi.dsi.fastutil.objects.ObjectSets;
++import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Set;
+@@ -0,0 +0,0 @@ public final class FeatureHooks {
+ }
+
+ public static LevelChunkSection createSection(final Registry biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
+- return new LevelChunkSection(biomeRegistry);
++ return new LevelChunkSection(biomeRegistry, level, chunkPos, chunkSection); // Paper - Anti-Xray - Add parameters
+ }
+
+ public static void sendChunkRefreshPackets(final List playersInRange, final LevelChunk chunk) {
+- final ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null);
++ // Paper start - Anti-Xray
++ final Map